@@ -4,7 +4,9 @@ module SolidObjects
44 class Supervisor
55 # @rbs @components: Array[Worker | EffectExecutor | ReminderScheduler | BroadcastExecutor]
66 # @rbs @threads: Array[Thread]
7+ # @rbs @monitor: Thread?
78 # @rbs @started: bool
9+ # @rbs @cleaned_up_at: Float
810
911 # @rbs (?worker_count: Integer, ?effect_worker_count: Integer, ?broadcast_worker_count: Integer, ?reminder_scheduler_count: Integer) -> void
1012 def initialize (
@@ -20,7 +22,9 @@ def initialize(
2022 reminder_scheduler_count :
2123 )
2224 @threads = [ ]
25+ @monitor = nil
2326 @started = false
27+ @cleaned_up_at = nil
2428 end
2529
2630 # @rbs () -> void
@@ -36,7 +40,8 @@ def start
3640 return if @started
3741
3842 @started = true
39- @threads = components . map { |component | Thread . new { component . run } }
43+ @threads = components . map { |component | supervise ( component ) }
44+ @monitor = Thread . new { monitor_loop }
4045 SolidObjects . instrument ( :"supervisor.started" , component_count : components . length )
4146 end
4247
@@ -45,14 +50,16 @@ def stop
4550 return unless @started
4651
4752 begin
53+ @started = false
54+ @monitor &.join ( SolidObjects . configuration . supervisor_monitor_interval * 2 )
4855 components . each ( &:request_shutdown )
4956 join_until_timeout
5057 components . reject ( &:stopped? ) . each ( &:stop )
5158 ensure
5259 # Connections held outside the pool must be released even when a
5360 # component fails to stop, or they accumulate across restarts.
5461 release_wake_up
55- @started = false
62+ @monitor = nil
5663 SolidObjects . instrument ( :"supervisor.stopped" , component_count : components . length )
5764 end
5865 end
@@ -61,6 +68,59 @@ def stop
6168
6269 attr_reader :components , :threads
6370
71+ # A role that raises leaves its thread dead. Without replacement the
72+ # process keeps running while quietly doing less work, so the supervisor
73+ # watches its threads and restarts any that stopped before shutdown.
74+ # @rbs () -> void
75+ def monitor_loop
76+ while @started
77+ replace_dead_roles
78+ cleanup_dead_processes
79+ sleep SolidObjects . configuration . supervisor_monitor_interval
80+ end
81+ rescue
82+ retry if @started
83+ end
84+
85+ # @rbs () -> void
86+ def replace_dead_roles
87+ components . each_with_index do |component , index |
88+ thread = threads [ index ]
89+ next if thread &.alive?
90+ next if component . stopped?
91+
92+ threads [ index ] = supervise ( component )
93+ SolidObjects . instrument (
94+ :"supervisor.role_replaced" ,
95+ role : component . class . name ,
96+ error_class : thread_error ( thread )
97+ )
98+ end
99+ end
100+
101+ # @rbs (Thread?) -> String?
102+ def thread_error ( thread )
103+ thread &.join
104+ nil
105+ rescue => error
106+ error . class . name
107+ end
108+
109+ # @rbs () -> void
110+ def cleanup_dead_processes
111+ interval = SolidObjects . configuration . dead_process_cleanup_interval
112+ return unless interval . positive?
113+ return if @cleaned_up_at && monotonic_now - @cleaned_up_at < interval
114+
115+ @cleaned_up_at = monotonic_now
116+ ProcessRegistry . cleanup_dead
117+ end
118+
119+ # @rbs (untyped) -> Thread
120+ def supervise ( component )
121+ Thread . new { component . run }
122+ end
123+
64124 # A wake-up adapter may hold connections outside the pool, which would
65125 # otherwise accumulate across restarts in one process.
66126 # @rbs () -> void
0 commit comments