Skip to content

Commit 5788a4c

Browse files
committed
fix: replace roles that crash after self-cleanup
Worker and ReminderScheduler run their shutdown cleanup in an ensure, so a crashed role reports itself stopped exactly like one that was asked to stop. Skipping stopped components therefore skipped every real crash and left the process permanently below its role count, which is the case the feature exists for. Key replacement on the supervisor still running, and build a fresh instance because the crashed one has already released its process record. Reject a non-positive monitor interval during validation, and merge the duplicated unreleased changelog section.
1 parent 8fb82c9 commit 5788a4c

5 files changed

Lines changed: 41 additions & 10 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@
88
records on an interval as part of the same monitor. Both intervals are
99
configurable through `supervisor_monitor_interval` and
1010
`dead_process_cleanup_interval`.
11-
12-
## Unreleased
13-
1411
- Add `SolidObjects::WakeUpAdapters::Postgresql`, an optional cross-process
1512
wake-up using PostgreSQL notifications. In-process signalling cannot reach a
1613
worker process, so reactive delivery waited out `polling_interval`. With the

‎lib/solid_objects/configuration.rb‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,10 @@ def validate!
144144
raise ArgumentError, "table_name_prefix must contain lowercase letters, digits, and underscores"
145145
end
146146

147+
unless supervisor_monitor_interval.positive?
148+
raise ArgumentError, "supervisor_monitor_interval must be positive"
149+
end
150+
147151
unless lease_duration > lease_renewal_interval
148152
raise ArgumentError, "lease_duration must be greater than lease_renewal_interval"
149153
end

‎lib/solid_objects/supervisor.rb‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,17 +82,25 @@ def monitor_loop
8282
retry if @started
8383
end
8484

85+
# A role that raises runs its own shutdown cleanup on the way out, so a
86+
# crashed component reports itself stopped exactly like one that was asked
87+
# to stop. While the supervisor is still running, a dead thread can only
88+
# mean a crash, so replacement keys on the supervisor rather than on the
89+
# component. The crashed instance has already released its process record,
90+
# so a fresh one takes its place.
8591
# @rbs () -> void
8692
def replace_dead_roles
8793
components.each_with_index do |component, index|
8894
thread = threads[index]
8995
next if thread&.alive?
90-
next if component.stopped?
96+
break unless @started
9197

92-
threads[index] = supervise(component)
98+
replacement = component.class.new
99+
components[index] = replacement
100+
threads[index] = supervise(replacement)
93101
SolidObjects.instrument(
94102
:"supervisor.role_replaced",
95-
role: component.class.name,
103+
role: replacement.class.name,
96104
error_class: thread_error(thread)
97105
)
98106
end

‎sig/generated/lib/solid_objects/supervisor.rbs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ module SolidObjects
3636
# @rbs () -> void
3737
def monitor_loop: () -> void
3838

39+
# A role that raises runs its own shutdown cleanup on the way out, so a
40+
# crashed component reports itself stopped exactly like one that was asked
41+
# to stop. While the supervisor is still running, a dead thread can only
42+
# mean a crash, so replacement keys on the supervisor rather than on the
43+
# component. The crashed instance has already released its process record,
44+
# so a fresh one takes its place.
3945
# @rbs () -> void
4046
def replace_dead_roles: () -> void
4147

‎test/integration/supervisor_replacement_test.rb‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,29 @@
66
class SupervisorReplacementTest < ActiveSupport::TestCase
77
# A component that crashes on its first run and records every run after.
88
class CrashingRole
9+
class << self
10+
attr_accessor :shared_runs, :shared_crashes
11+
end
12+
913
attr_reader :runs
1014

11-
def initialize(crashes: 1)
15+
def initialize(crashes: self.class.shared_crashes || 1)
1216
@crashes = crashes
13-
@runs = Queue.new
17+
self.class.shared_runs ||= Queue.new
18+
@runs = self.class.shared_runs
1419
@stopped = false
1520
@shutdown = false
1621
end
1722

23+
# Mirrors Worker and ReminderScheduler, which run their shutdown cleanup in
24+
# an ensure and therefore report themselves stopped after a crash too.
1825
def run
1926
@runs << monotonic_now
2027
raise "role crashed" if @runs.size <= @crashes
2128

2229
sleep 0.01 until @shutdown
30+
ensure
31+
stop
2332
end
2433

2534
def request_shutdown = @shutdown = true
@@ -34,10 +43,14 @@ def monotonic_now = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
3443
end
3544

3645
class HealthyRole < CrashingRole
37-
def initialize = super(crashes: 0)
46+
def initialize(**) = super(crashes: 0)
3847
end
3948

4049
setup do
50+
[ CrashingRole, HealthyRole ].each do |role|
51+
role.shared_runs = nil
52+
role.shared_crashes = nil
53+
end
4154
SolidObjects.configuration.supervisor_monitor_interval = 0.02
4255
SolidObjects.configuration.dead_process_cleanup_interval = 0.05
4356
end
@@ -56,7 +69,8 @@ def initialize = super(crashes: 0)
5669
end
5770

5871
test "keeps replacing a role that keeps crashing" do
59-
role = CrashingRole.new(crashes: 3)
72+
CrashingRole.shared_crashes = 3
73+
role = CrashingRole.new
6074
@supervisor = supervisor_for(role)
6175

6276
@supervisor.start
@@ -135,6 +149,8 @@ def initialize = super(crashes: 0)
135149

136150
private
137151

152+
# Every instance of a role shares one run log, so replacement instances are
153+
# observable the way the supervisor creates them.
138154
def supervisor_for(*roles)
139155
supervisor = SolidObjects::Supervisor.new(
140156
worker_count: 0,

0 commit comments

Comments
 (0)