service_skeleton 1.0.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.git-blame-ignore-revs +2 -0
  3. data/.github/workflows/ci.yml +50 -0
  4. data/.gitignore +0 -7
  5. data/.rubocop.yml +11 -1
  6. data/README.md +1 -53
  7. data/lib/service_skeleton/config.rb +20 -13
  8. data/lib/service_skeleton/generator.rb +4 -4
  9. data/lib/service_skeleton/runner.rb +3 -3
  10. data/lib/service_skeleton/ultravisor_children.rb +2 -1
  11. data/lib/service_skeleton/ultravisor_loggerstash.rb +9 -1
  12. data/service_skeleton.gemspec +4 -14
  13. data/ultravisor/.yardopts +1 -0
  14. data/ultravisor/Guardfile +9 -0
  15. data/ultravisor/README.md +404 -0
  16. data/ultravisor/lib/ultravisor.rb +216 -0
  17. data/ultravisor/lib/ultravisor/child.rb +485 -0
  18. data/ultravisor/lib/ultravisor/child/call.rb +21 -0
  19. data/ultravisor/lib/ultravisor/child/call_receiver.rb +14 -0
  20. data/ultravisor/lib/ultravisor/child/cast.rb +16 -0
  21. data/ultravisor/lib/ultravisor/child/cast_receiver.rb +11 -0
  22. data/ultravisor/lib/ultravisor/child/process_cast_call.rb +39 -0
  23. data/ultravisor/lib/ultravisor/error.rb +25 -0
  24. data/ultravisor/lib/ultravisor/logging_helpers.rb +32 -0
  25. data/ultravisor/spec/example_group_methods.rb +19 -0
  26. data/ultravisor/spec/example_methods.rb +8 -0
  27. data/ultravisor/spec/spec_helper.rb +56 -0
  28. data/ultravisor/spec/ultravisor/add_child_spec.rb +79 -0
  29. data/ultravisor/spec/ultravisor/child/call_spec.rb +121 -0
  30. data/ultravisor/spec/ultravisor/child/cast_spec.rb +111 -0
  31. data/ultravisor/spec/ultravisor/child/id_spec.rb +21 -0
  32. data/ultravisor/spec/ultravisor/child/new_spec.rb +152 -0
  33. data/ultravisor/spec/ultravisor/child/restart_delay_spec.rb +40 -0
  34. data/ultravisor/spec/ultravisor/child/restart_spec.rb +70 -0
  35. data/ultravisor/spec/ultravisor/child/run_spec.rb +95 -0
  36. data/ultravisor/spec/ultravisor/child/shutdown_spec.rb +124 -0
  37. data/ultravisor/spec/ultravisor/child/spawn_spec.rb +216 -0
  38. data/ultravisor/spec/ultravisor/child/unsafe_instance_spec.rb +55 -0
  39. data/ultravisor/spec/ultravisor/child/wait_spec.rb +32 -0
  40. data/ultravisor/spec/ultravisor/new_spec.rb +71 -0
  41. data/ultravisor/spec/ultravisor/remove_child_spec.rb +49 -0
  42. data/ultravisor/spec/ultravisor/run_spec.rb +334 -0
  43. data/ultravisor/spec/ultravisor/shutdown_spec.rb +106 -0
  44. metadata +48 -64
  45. data/.travis.yml +0 -11
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ require_relative "./ultravisor/child"
6
+ require_relative "./ultravisor/error"
7
+ require_relative "./ultravisor/logging_helpers"
8
+
9
+ # A super-dooOOOoooper supervisor.
10
+ #
11
+ class Ultravisor
12
+ include LoggingHelpers
13
+
14
+ def initialize(children: [], strategy: :one_for_one, logger: Logger.new("/dev/null"))
15
+ @queue, @logger = Queue.new, logger
16
+
17
+ @strategy = strategy
18
+ validate_strategy
19
+
20
+ @op_m, @op_cv = Mutex.new, ConditionVariable.new
21
+ @running_thread = nil
22
+
23
+ initialize_children(children)
24
+ end
25
+
26
+ def run
27
+ logger.debug(logloc) { "called" }
28
+
29
+ @op_m.synchronize do
30
+ if @running_thread
31
+ raise AlreadyRunningError,
32
+ "This ultravisor is already running"
33
+ end
34
+
35
+ @queue.clear
36
+ @running_thread = Thread.current
37
+ Thread.current.name = "Ultravisor"
38
+ end
39
+
40
+ logger.debug(logloc) { "Going to start children #{@children.map(&:first).inspect}" }
41
+ @children.each { |c| c.last.spawn(@queue) }
42
+
43
+ process_events
44
+
45
+ @op_m.synchronize do
46
+ logger.debug(logloc) { "Shutdown time for #{@children.reverse.map(&:first).inspect}" }
47
+ @children.reverse.each { |c| c.last.shutdown }
48
+
49
+ @running_thread = nil
50
+ @op_cv.broadcast
51
+ end
52
+
53
+ self
54
+ end
55
+
56
+ def shutdown(wait: true, force: false)
57
+ @op_m.synchronize do
58
+ return self unless @running_thread
59
+ if force
60
+ @children.reverse.each { |c| c.last.shutdown(force: true) }
61
+ @running_thread.kill
62
+ @running_thread = nil
63
+ @op_cv.broadcast
64
+ else
65
+ @queue << :shutdown
66
+ if wait
67
+ @op_cv.wait(@op_m) while @running_thread
68
+ end
69
+ end
70
+ end
71
+ self
72
+ end
73
+
74
+ def [](id)
75
+ @children.assoc(id)&.last
76
+ end
77
+
78
+ def add_child(**args)
79
+ logger.debug(logloc) { "Adding child #{args[:id].inspect}" }
80
+ args[:logger] ||= logger
81
+
82
+ @op_m.synchronize do
83
+ c = Ultravisor::Child.new(**args)
84
+
85
+ if @children.assoc(c.id)
86
+ raise DuplicateChildError,
87
+ "Child with ID #{c.id.inspect} already exists"
88
+ end
89
+
90
+ @children << [c.id, c]
91
+
92
+ if @running_thread
93
+ logger.debug(logloc) { "Auto-starting new child #{args[:id].inspect}" }
94
+ c.spawn(@queue)
95
+ end
96
+ end
97
+ end
98
+
99
+ def remove_child(id)
100
+ logger.debug(logloc) { "Removing child #{id.inspect}" }
101
+
102
+ @op_m.synchronize do
103
+ c = @children.assoc(id)
104
+
105
+ return nil if c.nil?
106
+
107
+ @children.delete(c)
108
+ if @running_thread
109
+ logger.debug(logloc) { "Shutting down removed child #{id.inspect}" }
110
+ c.last.shutdown
111
+ end
112
+ end
113
+ end
114
+
115
+ private
116
+
117
+ def validate_strategy
118
+ unless %i{one_for_one all_for_one rest_for_one}.include?(@strategy)
119
+ raise ArgumentError,
120
+ "Invalid strategy #{@strategy.inspect}"
121
+ end
122
+ end
123
+
124
+ def initialize_children(children)
125
+ unless children.is_a?(Array)
126
+ raise ArgumentError,
127
+ "children must be an Array"
128
+ end
129
+
130
+ @children = []
131
+
132
+ children.each do |cfg|
133
+ cfg[:logger] ||= logger
134
+ c = Ultravisor::Child.new(**cfg)
135
+ if @children.assoc(c.id)
136
+ raise DuplicateChildError,
137
+ "Duplicate child ID: #{c.id.inspect}"
138
+ end
139
+
140
+ @children << [c.id, c]
141
+ end
142
+ end
143
+
144
+ def process_events
145
+ loop do
146
+ qe = @queue.pop
147
+
148
+ case qe
149
+ when Ultravisor::Child
150
+ logger.debug(logloc) { "Received Ultravisor::Child queue entry for #{qe.id}" }
151
+ @op_m.synchronize { child_exited(qe) }
152
+ when :shutdown
153
+ logger.debug(logloc) { "Received :shutdown queue entry" }
154
+ break
155
+ else
156
+ logger.error(logloc) { "Unknown queue entry: #{qe.inspect}" }
157
+ end
158
+ end
159
+ end
160
+
161
+ def child_exited(child)
162
+ if child.termination_exception
163
+ log_exception(child.termination_exception, "Ultravisor::Child(#{child.id.inspect})") { "Thread terminated by unhandled exception" }
164
+ end
165
+
166
+ if @running_thread.nil?
167
+ logger.debug(logloc) { "Child termination after shutdown" }
168
+ # Child termination processed after we've shut down... nope
169
+ return
170
+ end
171
+
172
+ begin
173
+ return unless child.restart?
174
+ rescue Ultravisor::BlownRestartPolicyError
175
+ # Uh oh...
176
+ logger.error(logloc) { "Child #{child.id} has exceeded its restart policy. Shutting down the Ultravisor." }
177
+ @queue << :shutdown
178
+ return
179
+ end
180
+
181
+ case @strategy
182
+ when :all_for_one
183
+ @children.reverse.each do |id, c|
184
+ # Don't need to shut down the child that has caused all this mess
185
+ next if child.id == id
186
+
187
+ c.shutdown
188
+ end
189
+ when :rest_for_one
190
+ @children.reverse.each do |id, c|
191
+ # Don't go past the child that caused the problems
192
+ break if child.id == id
193
+
194
+ c.shutdown
195
+ end
196
+ end
197
+
198
+ sleep child.restart_delay
199
+
200
+ case @strategy
201
+ when :all_for_one
202
+ @children.each do |_, c|
203
+ c.spawn(@queue)
204
+ end
205
+ when :rest_for_one
206
+ s = false
207
+ @children.each do |id, c|
208
+ s = true if child.id == id
209
+
210
+ c.spawn(@queue) if s
211
+ end
212
+ when :one_for_one
213
+ child.spawn(@queue)
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,485 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./logging_helpers"
4
+
5
+ class Ultravisor
6
+ class Child
7
+ include LoggingHelpers
8
+
9
+ attr_reader :id
10
+
11
+ def initialize(id:,
12
+ klass:,
13
+ args: [],
14
+ method:,
15
+ restart: :always,
16
+ restart_policy: {
17
+ period: 5,
18
+ max: 3,
19
+ delay: 1,
20
+ },
21
+ shutdown: {
22
+ method: nil,
23
+ timeout: 1,
24
+ },
25
+ logger: Logger.new("/dev/null"),
26
+ enable_castcall: false,
27
+ access: nil
28
+ )
29
+
30
+ @logger = logger
31
+ @id = id
32
+
33
+ @klass, @args, @method = klass, args, method
34
+ validate_kam
35
+
36
+ @restart = restart
37
+ validate_restart
38
+
39
+ @restart_policy = restart_policy
40
+ validate_restart_policy
41
+
42
+ @shutdown_spec = shutdown
43
+ validate_shutdown_spec
44
+
45
+ @access = access
46
+ validate_access
47
+
48
+ @enable_castcall = enable_castcall
49
+
50
+ @runtime_history = []
51
+
52
+ @spawn_m = Mutex.new
53
+ @spawn_cv = ConditionVariable.new
54
+
55
+ @shutdown_m = Mutex.new
56
+ end
57
+
58
+ def spawn(term_queue)
59
+ @spawn_m.synchronize do
60
+ @value = nil
61
+ @exception = nil
62
+ @start_time = Time.now
63
+ @instance = new_instance
64
+
65
+ @spawn_id = sid = rand
66
+
67
+ Thread.handle_interrupt(::Exception => :never, ::Numeric => :never) do
68
+ @thread = Thread.new do
69
+ Thread.current.name = "Ultravisor::Child(#{@id})"
70
+ logger.debug(logloc) { "Spawning new instance of #{@id}" }
71
+
72
+ begin
73
+ Thread.handle_interrupt(::Exception => :immediate, ::Numeric => :immediate) do
74
+ logger.debug(logloc) { "Calling #{@klass}##{@method} to start #{@id} running" }
75
+ @value = @instance.public_send(@method)
76
+ end
77
+ rescue Exception => ex
78
+ @exception = ex
79
+ ensure
80
+ @spawn_m.synchronize do
81
+ # Even if a thread gets whacked by Thread#kill, ensure blocks
82
+ # still get run. This is... wonderful! And terrifying!
83
+
84
+ termination_cleanup(term_queue) if @spawn_id == sid
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ @spawn_cv.broadcast
91
+ end
92
+
93
+ self
94
+ end
95
+
96
+ def shutdown(force: false)
97
+ @shutdown_m.synchronize do
98
+ th = nil
99
+ sid = nil
100
+
101
+ @spawn_m.synchronize do
102
+ return if @thread.nil? || @thread == Thread.current
103
+
104
+ # Take a reference to the running thread, so we don't need to
105
+ # keep acquiring spawn_m every time we want to do something
106
+ # with it -- which causes collisions when it comes time to
107
+ # wait on the terminating thread, which is itself is trying
108
+ # to acquire the same lock so it can cleanup.
109
+ th = @thread
110
+ sid = @spawn_id
111
+
112
+ # Let everyone know we're in shutdown mode
113
+ @shutting_down = true
114
+ end
115
+
116
+ if @shutdown_spec[:method] && !force
117
+ begin
118
+ @instance.public_send(@shutdown_spec[:method])
119
+ rescue Exception => ex
120
+ log_exception(ex) { "Unhandled exception when calling #{@shutdown_spec[:method].inspect} on child #{id}" }
121
+ th.kill
122
+ end
123
+ else
124
+ th.kill
125
+ end
126
+
127
+ unless th.join(@shutdown_spec[:timeout])
128
+ logger.info(logloc) { "Child instance for #{self.id} did not cleanly shutdown within #{@shutdown_spec[:timeout]} seconds; force-killing the thread" }
129
+ th.kill
130
+ end
131
+
132
+ # Last chance, bubs
133
+ unless th.join(0.1)
134
+ logger.error(logloc) { "Child thread for #{self.id} appears hung; abandoning thread #{th}" }
135
+
136
+ # If we get here, then the worker instance has seized up spectacularly,
137
+ # and the cleanup in the `spawn` ensure hasn't triggered, so we need
138
+ # to do the cleanup instead.
139
+ @spawn_m.synchronize do
140
+ termination_cleanup if @spawn_id == sid
141
+ end
142
+ end
143
+ end
144
+ end
145
+
146
+ def wait
147
+ @spawn_m.synchronize do
148
+ @spawn_cv.wait(@spawn_m) while @thread
149
+ end
150
+ end
151
+
152
+ def termination_exception
153
+ @spawn_m.synchronize do
154
+ @spawn_cv.wait(@spawn_m) while @thread
155
+ @exception
156
+ end
157
+ end
158
+
159
+ def termination_value
160
+ @spawn_m.synchronize do
161
+ @spawn_cv.wait(@spawn_m) while @thread
162
+ @value
163
+ end
164
+ end
165
+
166
+ def restart_delay
167
+ d = begin
168
+ case @restart_policy[:delay]
169
+ when Numeric
170
+ @restart_policy[:delay]
171
+ when Range
172
+ @restart_policy[:delay].first + (@restart_policy[:delay].last - @restart_policy[:delay].first) * rand
173
+ end
174
+ end
175
+
176
+ [0, d].max
177
+ end
178
+
179
+ def restart?
180
+ if blown_policy?
181
+ raise BlownRestartPolicyError,
182
+ "Child #{self.id} has restarted more than #{@restart_policy[:max]} times in #{@restart_policy[:period]} seconds."
183
+ end
184
+
185
+ !!(@restart == :always || (@restart == :on_failure && termination_exception))
186
+ end
187
+
188
+ def unsafe_instance(wait: true)
189
+ unless @access == :unsafe
190
+ raise Ultravisor::ThreadSafetyError,
191
+ "#unsafe_instance called on a child not declared with access: :unsafe"
192
+ end
193
+
194
+ current_instance(wait: wait)
195
+ end
196
+
197
+ def cast
198
+ unless castcall_enabled?
199
+ raise NoMethodError,
200
+ "undefined method `cast' for #{self}"
201
+ end
202
+
203
+ CastReceiver.new do |castback|
204
+ @spawn_m.synchronize do
205
+ while @instance.nil?
206
+ #:nocov:
207
+ @spawn_cv.wait(@spawn_m)
208
+ #:nocov:
209
+ end
210
+
211
+ unless @instance.respond_to? castback.method_name
212
+ raise NoMethodError,
213
+ "undefined method `#{castback.method_name}' for #{@instance}"
214
+ end
215
+
216
+ begin
217
+ @instance.instance_variable_get(:@ultravisor_child_castcall_queue) << castback
218
+ rescue ClosedQueueError
219
+ # casts aren't guaranteed to ever execute, so dropping it
220
+ # when the instance's queue has closed is perfectly valid
221
+ end
222
+
223
+ @castcall_fd_writer.putc "!"
224
+ end
225
+ end
226
+ end
227
+
228
+ def call
229
+ unless castcall_enabled?
230
+ raise NoMethodError,
231
+ "undefined method `call' for #{self}"
232
+ end
233
+
234
+ CallReceiver.new do |callback|
235
+ @spawn_m.synchronize do
236
+ while @instance.nil?
237
+ #:nocov:
238
+ @spawn_cv.wait(@spawn_m)
239
+ #:nocov:
240
+ end
241
+
242
+ unless @instance.respond_to? callback.method_name
243
+ raise NoMethodError,
244
+ "undefined method `#{callback.method_name}' for #{@instance}"
245
+ end
246
+
247
+ begin
248
+ @instance.instance_variable_get(:@ultravisor_child_castcall_queue) << callback
249
+ rescue ClosedQueueError
250
+ raise ChildRestartedError
251
+ end
252
+
253
+ @castcall_fd_writer.putc "!"
254
+ end
255
+ end
256
+ end
257
+
258
+ private
259
+
260
+ def validate_kam
261
+ if @klass.instance_method(:initialize).arity == 0 && @args != []
262
+ raise InvalidKAMError,
263
+ "#{@klass.to_s}.new takes no arguments, but args not empty."
264
+ end
265
+
266
+ begin
267
+ if @klass.instance_method(@method).arity != 0
268
+ raise InvalidKAMError,
269
+ "#{@klass.to_s}##{@method} must not take arguments"
270
+ end
271
+ rescue NameError
272
+ raise InvalidKAMError,
273
+ "#{@klass.to_s} has no instance method #{@method}"
274
+ end
275
+ end
276
+
277
+ def validate_restart
278
+ unless %i{never on_failure always}.include?(@restart)
279
+ raise ArgumentError,
280
+ "Invalid value for restart: #{@restart.inspect}"
281
+ end
282
+ end
283
+
284
+ def validate_restart_policy
285
+ unless @restart_policy.is_a?(Hash)
286
+ raise ArgumentError,
287
+ "restart_policy must be a hash (got #{@restart_policy.inspect})"
288
+ end
289
+
290
+ bad_keys = @restart_policy.keys - %i{period max delay}
291
+ unless bad_keys.empty?
292
+ raise ArgumentError,
293
+ "Invalid key(s) in restart_policy: #{bad_keys.inspect}"
294
+ end
295
+
296
+ # Restore any missing defaults
297
+ @restart_policy = { period: 5, max: 3, delay: 1 }.merge(@restart_policy)
298
+
299
+ unless @restart_policy[:period].is_a?(Numeric) && @restart_policy[:period].positive?
300
+ raise ArgumentError,
301
+ "Invalid restart_policy period #{@restart_policy[:period].inspect}: must be positive integer"
302
+ end
303
+
304
+ unless @restart_policy[:max].is_a?(Numeric) && !@restart_policy[:max].negative?
305
+ raise ArgumentError,
306
+ "Invalid restart_policy max #{@restart_policy[:period].inspect}: must be non-negative integer"
307
+ end
308
+
309
+ case @restart_policy[:delay]
310
+ when Numeric
311
+ if @restart_policy[:delay].negative?
312
+ raise ArgumentError,
313
+ "Invalid restart_policy delay #{@restart_policy[:delay].inspect}: must be non-negative integer or range"
314
+ end
315
+ when Range
316
+ if @restart_policy[:delay].first >= @restart_policy[:delay].last
317
+ raise ArgumentError,
318
+ "Invalid restart_policy delay #{@restart_policy[:delay].inspect}: must be non-negative integer or increasing range"
319
+ end
320
+
321
+ if @restart_policy[:delay].first.negative?
322
+ raise ArgumentError,
323
+ "Invalid restart_policy delay #{@restart_policy[:delay].inspect}: range must not be negative"
324
+ end
325
+ else
326
+ raise ArgumentError,
327
+ "Invalid restart_policy delay #{@restart_policy[:delay].inspect}: must be non-negative integer or range"
328
+ end
329
+ end
330
+
331
+ def validate_shutdown_spec
332
+ unless @shutdown_spec.is_a?(Hash)
333
+ raise ArgumentError,
334
+ "shutdown must be a hash (got #{@shutdown_spec.inspect})"
335
+ end
336
+
337
+ bad_keys = @shutdown_spec.keys - %i{method timeout}
338
+ unless bad_keys.empty?
339
+ raise ArgumentError,
340
+ "Invalid key(s) in shutdown specification: #{bad_keys.inspect}"
341
+ end
342
+
343
+ # Restore any missing defaults
344
+ @shutdown_spec = { method: nil, timeout: 1 }.merge(@shutdown_spec)
345
+
346
+ if @shutdown_spec[:method]
347
+ begin
348
+ unless @klass.instance_method(@shutdown_spec[:method]).arity == 0
349
+ raise ArgumentError,
350
+ "Shutdown method #{@klass.to_s}##{@shutdown_spec[:method]} must not take any arguments"
351
+ end
352
+ rescue NameError
353
+ raise ArgumentError,
354
+ "Shutdown method #{@klass.to_s}##{@shutdown_spec[:method]} is not defined"
355
+ end
356
+ end
357
+
358
+ unless @shutdown_spec[:timeout].is_a?(Numeric) && !@shutdown_spec[:timeout].negative?
359
+ raise ArgumentError,
360
+ "Invalid shutdown timeout #{@shutdown_spec[:timeout].inspect}: must be non-negative integer"
361
+ end
362
+ end
363
+
364
+ def validate_access
365
+ return if @access.nil?
366
+
367
+ unless %i{unsafe}.include? @access
368
+ raise ArgumentError,
369
+ "Invalid instance access specification: #{@access.inspect}"
370
+ end
371
+ end
372
+
373
+ def castcall_enabled?
374
+ !!@enable_castcall
375
+ end
376
+
377
+ def new_instance
378
+ # If there is anything that pisses me off about Ruby's varargs handling more
379
+ # than the fact that *[] is an empty array, and not a zero-length argument
380
+ # list, I don't know what it is. Everything else works *so well*, and this...
381
+ # urgh.
382
+ if @klass.instance_method(:initialize).arity == 0
383
+ @klass.new()
384
+ elsif @args.is_a?(Hash)
385
+ @klass.new(**@args)
386
+ elsif @args.last.is_a?(Hash)
387
+ @klass.new(*@args[0...-1], **@args.last)
388
+ else
389
+ @klass.new(*@args)
390
+ end.tap do |i|
391
+ if castcall_enabled?
392
+ i.singleton_class.prepend(Ultravisor::Child::ProcessCastCall)
393
+ i.instance_variable_set(:@ultravisor_child_castcall_queue, Queue.new)
394
+
395
+ r, @castcall_fd_writer = IO.pipe
396
+ i.instance_variable_set(:@ultravisor_child_castcall_fd, r)
397
+ end
398
+ end
399
+ end
400
+
401
+ def current_instance(wait: true)
402
+ @spawn_m.synchronize do
403
+ while wait && @instance.nil?
404
+ @spawn_cv.wait(@spawn_m)
405
+ end
406
+
407
+ return @instance
408
+ end
409
+ end
410
+
411
+ def blown_policy?
412
+ cumulative_runtime = 0
413
+ # This starts at 1 because we only check this during a restart, so
414
+ # by definition there must have been at least one recent restart
415
+ recent_restart_count = 1
416
+
417
+ @runtime_history.each do |t|
418
+ cumulative_runtime += t
419
+
420
+ if cumulative_runtime < @restart_policy[:period]
421
+ recent_restart_count += 1
422
+ end
423
+ end
424
+
425
+ logger.debug(logloc) { "@runtime_history: #{@runtime_history.inspect}, cumulative_runtime: #{cumulative_runtime}, recent_restart_count: #{recent_restart_count}, restart_policy: #{@restart_policy.inspect}" }
426
+
427
+ if recent_restart_count > @restart_policy[:max]
428
+ return true
429
+ end
430
+
431
+ @runtime_history = @runtime_history[0..recent_restart_count]
432
+
433
+ false
434
+ end
435
+
436
+ def termination_cleanup(term_queue = nil)
437
+ unless @spawn_m.owned?
438
+ #:nocov:
439
+ raise ThreadSafetyError,
440
+ "termination_cleanup must be called while holding the @spawn_m lock"
441
+ #:nocov:
442
+ end
443
+
444
+ if @start_time
445
+ @runtime_history.unshift(Time.now.to_f - @start_time.to_f)
446
+ @start_time = nil
447
+ end
448
+
449
+ term_queue << self if term_queue && !@shutting_down
450
+
451
+ if castcall_enabled?
452
+ cc_q = @instance.instance_variable_get(:@ultravisor_child_castcall_queue)
453
+ cc_q.close
454
+ x = 0
455
+ begin
456
+ loop do
457
+ cc_q.pop(true).child_restarted!
458
+ end
459
+ rescue ThreadError => ex
460
+ raise unless ex.message == "queue empty"
461
+ end
462
+
463
+ @instance.instance_variable_get(:@ultravisor_child_castcall_fd).close
464
+ @instance.instance_variable_set(:@ultravisor_child_castcall_fd, nil)
465
+ @castcall_fd_writer.close
466
+ @castcall_fd_writer = nil
467
+ end
468
+
469
+ @instance = nil
470
+
471
+ if @thread
472
+ @thread = nil
473
+ @spawn_cv.broadcast
474
+ end
475
+
476
+ @spawn_id = nil
477
+ end
478
+ end
479
+ end
480
+
481
+ require_relative "./child/call"
482
+ require_relative "./child/call_receiver"
483
+ require_relative "./child/cast"
484
+ require_relative "./child/cast_receiver"
485
+ require_relative "./child/process_cast_call"