""" T3.3: Multiple Concurrent Timers Test Tests that multiple timers with different intervals run independently without interfering with each other. Priority: LOW Duration: ~30 seconds """ import time import pytest from helpers.client import AttuneClient from helpers.fixtures import create_echo_action, create_interval_timer, unique_ref from helpers.polling import wait_for_execution_count @pytest.mark.tier3 @pytest.mark.timer @pytest.mark.performance def test_multiple_concurrent_timers(client: AttuneClient, test_pack): """ Test that multiple timers with different intervals run independently. Setup: - Timer A: every 3 seconds - Timer B: every 5 seconds - Timer C: every 7 seconds Run for 21 seconds (LCM of 3, 5, 7 is 105, but 21 gives us good data): - Timer A should fire ~7 times (21/3 = 7) - Timer B should fire ~4 times (21/5 = 4.2) - Timer C should fire ~3 times (21/7 = 3) """ print("\n" + "=" * 80) print("T3.3a: Multiple Concurrent Timers Test") print("=" * 80) pack_ref = test_pack["ref"] # Step 1: Create three timers with different intervals print("\n[STEP 1] Creating three interval timers...") timers = [] # Timer A: 3 seconds trigger_a = f"timer_3s_{unique_ref()}" create_interval_timer( client=client, pack_ref=pack_ref, trigger_ref=trigger_a, interval=3 ) timers.append({"trigger": trigger_a, "interval": 3, "name": "Timer A"}) print(f"✓ Timer A created: {trigger_a} (3 seconds)") # Timer B: 5 seconds trigger_b = f"timer_5s_{unique_ref()}" create_interval_timer( client=client, pack_ref=pack_ref, trigger_ref=trigger_b, interval=5 ) timers.append({"trigger": trigger_b, "interval": 5, "name": "Timer B"}) print(f"✓ Timer B created: {trigger_b} (5 seconds)") # Timer C: 7 seconds trigger_c = f"timer_7s_{unique_ref()}" create_interval_timer( client=client, pack_ref=pack_ref, trigger_ref=trigger_c, interval=7 ) timers.append({"trigger": trigger_c, "interval": 7, "name": "Timer C"}) print(f"✓ Timer C created: {trigger_c} (7 seconds)") # Step 2: Create actions for each timer print("\n[STEP 2] Creating actions for each timer...") action_a = create_echo_action( client=client, pack_ref=pack_ref, message="Timer A tick", suffix="_3s" ) print(f"✓ Action A created: {action_a}") action_b = create_echo_action( client=client, pack_ref=pack_ref, message="Timer B tick", suffix="_5s" ) print(f"✓ Action B created: {action_b}") action_c = create_echo_action( client=client, pack_ref=pack_ref, message="Timer C tick", suffix="_7s" ) print(f"✓ Action C created: {action_c}") actions = [ {"ref": action_a, "name": "Action A"}, {"ref": action_b, "name": "Action B"}, {"ref": action_c, "name": "Action C"}, ] # Step 3: Create rules linking timers to actions print("\n[STEP 3] Creating rules...") rule_ids = [] for i, (timer, action) in enumerate(zip(timers, actions)): rule_data = { "name": f"Concurrent Timer Rule {i + 1} {unique_ref()}", "trigger": timer["trigger"], "action": action["ref"], "enabled": True, } rule_response = client.create_rule(rule_data) rule_ids.append(rule_response["id"]) print( f"✓ Rule {i + 1} created: {timer['name']} → {action['name']} (every {timer['interval']}s)" ) # Step 4: Run for 21 seconds and monitor print("\n[STEP 4] Running for 21 seconds...") print(" Monitoring timer executions...") test_duration = 21 start_time = time.time() # Take snapshots at intervals snapshots = [] for i in range(8): # 0, 3, 6, 9, 12, 15, 18, 21 seconds if i > 0: time.sleep(3) elapsed = time.time() - start_time snapshot = {"time": elapsed, "counts": {}} for action in actions: executions = client.list_executions(action=action["ref"]) snapshot["counts"][action["name"]] = len(executions) snapshots.append(snapshot) print( f" t={elapsed:.1f}s: A={snapshot['counts']['Action A']}, " f"B={snapshot['counts']['Action B']}, C={snapshot['counts']['Action C']}" ) # Step 5: Verify final counts print("\n[STEP 5] Verifying execution counts...") final_counts = { "Action A": len(client.list_executions(action=action_a)), "Action B": len(client.list_executions(action=action_b)), "Action C": len(client.list_executions(action=action_c)), } expected_counts = { "Action A": {"min": 6, "max": 8, "ideal": 7}, # 21/3 = 7 "Action B": {"min": 3, "max": 5, "ideal": 4}, # 21/5 = 4.2 "Action C": {"min": 2, "max": 4, "ideal": 3}, # 21/7 = 3 } print(f"\nFinal execution counts:") results = {} for action_name, count in final_counts.items(): expected = expected_counts[action_name] in_range = expected["min"] <= count <= expected["max"] status = "✓" if in_range else "⚠" print( f" {status} {action_name}: {count} executions " f"(expected: {expected['ideal']}, range: {expected['min']}-{expected['max']})" ) results[action_name] = { "count": count, "expected": expected["ideal"], "in_range": in_range, } # Step 6: Check for timer drift print("\n[STEP 6] Checking for timer drift...") # Analyze timing consistency timing_ok = True if len(snapshots) > 2: # Check Timer A (should increase by 1 every 3 seconds) a_increases = [] for i in range(1, len(snapshots)): increase = ( snapshots[i]["counts"]["Action A"] - snapshots[i - 1]["counts"]["Action A"] ) a_increases.append(increase) # Should mostly be 1s (one execution per 3-second interval) if any(inc > 2 for inc in a_increases): print(f"⚠ Timer A may have drift: {a_increases}") timing_ok = False else: print(f"✓ Timer A consistent: {a_increases}") # Step 7: Verify no interference print("\n[STEP 7] Verifying no timer interference...") # Check that timers didn't affect each other's timing interference_detected = False # If all timers are within expected ranges, no interference if all(r["in_range"] for r in results.values()): print(f"✓ All timers within expected ranges (no interference)") else: print(f"⚠ Some timers outside expected ranges") interference_detected = True # Summary print("\n" + "=" * 80) print("CONCURRENT TIMERS TEST SUMMARY") print("=" * 80) print(f"✓ Test duration: {test_duration} seconds") print(f"✓ Timers created: 3 (3s, 5s, 7s intervals)") print(f"✓ Final counts:") print(f" Timer A (3s): {final_counts['Action A']} executions (expected ~7)") print(f" Timer B (5s): {final_counts['Action B']} executions (expected ~4)") print(f" Timer C (7s): {final_counts['Action C']} executions (expected ~3)") all_in_range = all(r["in_range"] for r in results.values()) if all_in_range and not interference_detected: print("\n✅ CONCURRENT TIMERS WORKING INDEPENDENTLY!") else: print("\n⚠️ Some timers outside expected ranges") print(" This may be due to system load or timing variations") print("=" * 80) # Allow some tolerance assert results["Action A"]["count"] >= 5, "Timer A fired too few times" assert results["Action B"]["count"] >= 3, "Timer B fired too few times" assert results["Action C"]["count"] >= 2, "Timer C fired too few times" @pytest.mark.tier3 @pytest.mark.timer @pytest.mark.performance def test_many_concurrent_timers(client: AttuneClient, test_pack): """ Test system can handle many concurrent timers (stress test). Creates 5 timers with 2-second intervals and verifies they all fire. """ print("\n" + "=" * 80) print("T3.3b: Many Concurrent Timers Test") print("=" * 80) pack_ref = test_pack["ref"] # Step 1: Create 5 timers print("\n[STEP 1] Creating 5 concurrent timers...") num_timers = 5 timers_and_actions = [] for i in range(num_timers): trigger_ref = f"multi_timer_{i}_{unique_ref()}" create_interval_timer( client=client, pack_ref=pack_ref, trigger_ref=trigger_ref, interval=2 ) action_ref = create_echo_action( client=client, pack_ref=pack_ref, message=f"Timer {i} tick", suffix=f"_multi{i}", ) rule_data = { "name": f"Multi Timer Rule {i} {unique_ref()}", "trigger": trigger_ref, "action": action_ref, "enabled": True, } rule = client.create_rule(rule_data) timers_and_actions.append( { "trigger": trigger_ref, "action": action_ref, "rule_id": rule["id"], "index": i, } ) print(f"✓ Timer {i} created (2s interval)") # Step 2: Wait for executions print(f"\n[STEP 2] Waiting 8 seconds for executions...") time.sleep(8) # Step 3: Check all timers fired print(f"\n[STEP 3] Checking execution counts...") all_fired = True total_executions = 0 for timer_info in timers_and_actions: executions = client.list_executions(action=timer_info["action"]) count = len(executions) total_executions += count status = "✓" if count >= 3 else "⚠" print(f" {status} Timer {timer_info['index']}: {count} executions") if count < 2: all_fired = False print(f"\nTotal executions: {total_executions}") print(f"Average per timer: {total_executions / num_timers:.1f}") # Summary print("\n" + "=" * 80) print("MANY CONCURRENT TIMERS TEST SUMMARY") print("=" * 80) print(f"✓ Timers created: {num_timers}") print(f"✓ Total executions: {total_executions}") print(f"✓ All timers fired: {all_fired}") if all_fired: print("\n✅ SYSTEM HANDLES MANY CONCURRENT TIMERS!") else: print("\n⚠️ Some timers did not fire as expected") print("=" * 80) assert total_executions >= num_timers * 2, ( f"Expected at least {num_timers * 2} total executions, got {total_executions}" ) @pytest.mark.tier3 @pytest.mark.timer @pytest.mark.performance def test_timer_precision_under_load(client: AttuneClient, test_pack): """ Test timer precision when multiple timers are running. Verifies that timer precision doesn't degrade with concurrent timers. """ print("\n" + "=" * 80) print("T3.3c: Timer Precision Under Load Test") print("=" * 80) pack_ref = test_pack["ref"] # Step 1: Create 3 timers print("\n[STEP 1] Creating 3 timers (2s interval each)...") triggers = [] actions = [] for i in range(3): trigger_ref = f"precision_timer_{i}_{unique_ref()}" create_interval_timer( client=client, pack_ref=pack_ref, trigger_ref=trigger_ref, interval=2 ) triggers.append(trigger_ref) action_ref = create_echo_action( client=client, pack_ref=pack_ref, message=f"Precision timer {i}", suffix=f"_prec{i}", ) actions.append(action_ref) rule_data = { "name": f"Precision Test Rule {i} {unique_ref()}", "trigger": trigger_ref, "action": action_ref, "enabled": True, } client.create_rule(rule_data) print(f"✓ Timer {i} created") # Step 2: Monitor timing print("\n[STEP 2] Monitoring timing precision...") start_time = time.time() measurements = [] for check in range(4): # Check at 0, 3, 6, 9 seconds if check > 0: time.sleep(3) elapsed = time.time() - start_time # Count executions for first timer execs = client.list_executions(action=actions[0]) count = len(execs) expected = int(elapsed / 2) delta = abs(count - expected) measurements.append( {"elapsed": elapsed, "count": count, "expected": expected, "delta": delta} ) print( f" t={elapsed:.1f}s: {count} executions (expected: {expected}, delta: {delta})" ) # Step 3: Calculate precision print("\n[STEP 3] Calculating timing precision...") max_delta = max(m["delta"] for m in measurements) avg_delta = sum(m["delta"] for m in measurements) / len(measurements) print(f" Maximum delta: {max_delta} executions") print(f" Average delta: {avg_delta:.1f} executions") precision_ok = max_delta <= 1 if precision_ok: print(f"✓ Timing precision acceptable (max delta ≤ 1)") else: print(f"⚠ Timing precision degraded (max delta > 1)") # Summary print("\n" + "=" * 80) print("TIMER PRECISION UNDER LOAD TEST SUMMARY") print("=" * 80) print(f"✓ Concurrent timers: 3") print(f"✓ Max timing delta: {max_delta}") print(f"✓ Avg timing delta: {avg_delta:.1f}") if precision_ok: print("\n✅ TIMER PRECISION MAINTAINED UNDER LOAD!") else: print("\n⚠️ Timer precision may degrade under concurrent load") print("=" * 80) assert max_delta <= 2, f"Timing precision too poor: max delta {max_delta}"