@@ -61,46 +61,11 @@ def checked(effect_id:, outcome:, arguments: nil, result: nil)
6161 end
6262
6363 test "a running effect keeps its process heartbeat fresh" do
64- SolidObjects . configuration . process_heartbeat_interval = 0.02
65- SolidObjects . configuration . process_alive_threshold = 0.1
66- ExportActor . ref ( "long-handler" ) . async . start_recoverable_export
67- worker = SolidObjects ::Worker . new
68- worker . run_until_idle
69- entered = Queue . new
70- release = Queue . new
71- heartbeats = Queue . new
72- executing = nil
73- registry = SolidObjects ::ProcessRegistry . new
74- registry . define_singleton_method ( :heartbeat ) do
75- updated = super ( )
76- heartbeats << Thread . current if updated && executing
77- updated
78- end
79- SolidObjects . register_effect ( :build_report ) do
80- executing = Thread . current
81- entered << true
82- release . pop
83- { "artifact_key" => "report.pdf" }
84- end
85- executor = SolidObjects ::EffectExecutor . new ( process_registry : registry )
86- effect_thread = Thread . new do
87- SolidObjects ::Record . connection_pool . with_connection { executor . run_once }
88- end
89- Timeout . timeout ( 5 ) { entered . pop }
90- heartbeat_thread = Timeout . timeout ( 2 ) { heartbeats . pop }
91- Timeout . timeout ( 2 ) { 7 . times { heartbeats . pop } }
92- SolidObjects ::EffectRecoveryCoordinator . new . recover_available
93- assert_equal "processing" , SolidObjects ::Effect . find_by! ( name : "build_report" ) . status
94- assert_empty SolidObjects ::Message . where ( operation : "retired" )
95- release << true
96- assert effect_thread . value
97- refute heartbeat_thread . alive?
98- assert_equal "completed" , SolidObjects ::Effect . find_by! ( name : "build_report" ) . status
99- ensure
100- release &.push ( true )
101- effect_thread &.join ( 5 )
102- executor &.stop
103- worker &.stop
64+ assert_running_effect_keeps_heartbeat_fresh ( heartbeat_delay : 0 )
65+ end
66+
67+ test "a running effect keeps its process heartbeat fresh when heartbeat writes are slow" do
68+ assert_running_effect_keeps_heartbeat_fresh ( heartbeat_delay : 0.15 )
10469 end
10570
10671 test "a failed handler stops its heartbeat before scheduling a retry" do
@@ -635,6 +600,61 @@ def retired(effect_id:, arguments:, outcome:)
635600
636601 private
637602
603+ def assert_running_effect_keeps_heartbeat_fresh ( heartbeat_delay :)
604+ SolidObjects . configuration . process_heartbeat_interval = 0.02
605+ SolidObjects . configuration . process_alive_threshold = 0.1
606+ ExportActor . ref ( "long-handler" ) . async . start_recoverable_export
607+ worker = SolidObjects ::Worker . new
608+ worker . run_until_idle
609+ entered = Queue . new
610+ release = Queue . new
611+ heartbeats = Queue . new
612+ executing = nil
613+ registry = SolidObjects ::ProcessRegistry . new
614+ registry . define_singleton_method ( :heartbeat ) do
615+ updated = super ( )
616+ sleep ( heartbeat_delay ) if updated && executing
617+ heartbeats << Thread . current if updated && executing
618+ updated
619+ end
620+ SolidObjects . register_effect ( :build_report ) do
621+ executing = Thread . current
622+ entered << true
623+ release . pop
624+ { "artifact_key" => "report.pdf" }
625+ end
626+ executor = SolidObjects ::EffectExecutor . new ( process_registry : registry )
627+ effect_thread = Thread . new do
628+ SolidObjects ::Record . connection_pool . with_connection { executor . run_once }
629+ end
630+ Timeout . timeout ( 5 ) { entered . pop }
631+ effect = SolidObjects ::Effect . find_by! ( name : "build_report" )
632+ owner = SolidObjects ::Process . find ( effect . claimed_by )
633+ threshold = SolidObjects . configuration . process_alive_threshold
634+ heartbeat_thread = Timeout . timeout ( 2 ) { heartbeats . pop }
635+ Timeout . timeout ( 5 ) { heartbeats . pop until owner . reload . last_heartbeat_at - effect . claimed_at > threshold }
636+ recover_at ( owner . last_heartbeat_at + ( threshold / 2 ) )
637+ assert_equal "processing" , effect . reload . status
638+ assert_empty SolidObjects ::Message . where ( operation : "retired" )
639+ release << true
640+ assert effect_thread . value
641+ refute heartbeat_thread . alive?
642+ assert_equal "completed" , effect . reload . status
643+ ensure
644+ release &.push ( true )
645+ effect_thread &.join ( 5 )
646+ executor &.stop
647+ worker &.stop
648+ end
649+
650+ def recover_at ( database_now )
651+ adapter = SolidObjects . database_adapter
652+ adapter . define_singleton_method ( :database_clock_now ) { database_now }
653+ SolidObjects ::EffectRecoveryCoordinator . new . recover_available
654+ ensure
655+ adapter . singleton_class . send ( :remove_method , :database_clock_now )
656+ end
657+
638658 def processing_export ( actor_id )
639659 ExportActor . ref ( actor_id ) . async . start_checked_export
640660 worker = SolidObjects ::Worker . new
0 commit comments