Skip to content

Commit b2031d2

Browse files
authored
Merge pull request #101 from cardmagic/test/heartbeat-recovery-clock
test: make the heartbeat recovery test independent of write speed
2 parents e02aea1 + ebe3c11 commit b2031d2

1 file changed

Lines changed: 60 additions & 40 deletions

File tree

‎test/integration/effect_recovery_test.rb‎

Lines changed: 60 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)