service_skeleton 0.0.0.49.g47046b9 → 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.git-blame-ignore-revs +2 -0
  3. data/.github/workflows/ci.yml +46 -0
  4. data/.gitignore +0 -7
  5. data/.rubocop.yml +11 -1
  6. data/README.md +6 -6
  7. data/lib/service_skeleton/config.rb +20 -13
  8. data/lib/service_skeleton/generator.rb +3 -3
  9. data/lib/service_skeleton/runner.rb +3 -3
  10. data/lib/service_skeleton/ultravisor_children.rb +4 -1
  11. data/lib/service_skeleton/ultravisor_loggerstash.rb +9 -1
  12. data/service_skeleton.gemspec +5 -7
  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 +481 -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 +107 -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 -29
  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,481 @@
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
+ else
385
+ @klass.new(*@args)
386
+ end.tap do |i|
387
+ if castcall_enabled?
388
+ i.singleton_class.prepend(Ultravisor::Child::ProcessCastCall)
389
+ i.instance_variable_set(:@ultravisor_child_castcall_queue, Queue.new)
390
+
391
+ r, @castcall_fd_writer = IO.pipe
392
+ i.instance_variable_set(:@ultravisor_child_castcall_fd, r)
393
+ end
394
+ end
395
+ end
396
+
397
+ def current_instance(wait: true)
398
+ @spawn_m.synchronize do
399
+ while wait && @instance.nil?
400
+ @spawn_cv.wait(@spawn_m)
401
+ end
402
+
403
+ return @instance
404
+ end
405
+ end
406
+
407
+ def blown_policy?
408
+ cumulative_runtime = 0
409
+ # This starts at 1 because we only check this during a restart, so
410
+ # by definition there must have been at least one recent restart
411
+ recent_restart_count = 1
412
+
413
+ @runtime_history.each do |t|
414
+ cumulative_runtime += t
415
+
416
+ if cumulative_runtime < @restart_policy[:period]
417
+ recent_restart_count += 1
418
+ end
419
+ end
420
+
421
+ logger.debug(logloc) { "@runtime_history: #{@runtime_history.inspect}, cumulative_runtime: #{cumulative_runtime}, recent_restart_count: #{recent_restart_count}, restart_policy: #{@restart_policy.inspect}" }
422
+
423
+ if recent_restart_count > @restart_policy[:max]
424
+ return true
425
+ end
426
+
427
+ @runtime_history = @runtime_history[0..recent_restart_count]
428
+
429
+ false
430
+ end
431
+
432
+ def termination_cleanup(term_queue = nil)
433
+ unless @spawn_m.owned?
434
+ #:nocov:
435
+ raise ThreadSafetyError,
436
+ "termination_cleanup must be called while holding the @spawn_m lock"
437
+ #:nocov:
438
+ end
439
+
440
+ if @start_time
441
+ @runtime_history.unshift(Time.now.to_f - @start_time.to_f)
442
+ @start_time = nil
443
+ end
444
+
445
+ term_queue << self if term_queue && !@shutting_down
446
+
447
+ if castcall_enabled?
448
+ cc_q = @instance.instance_variable_get(:@ultravisor_child_castcall_queue)
449
+ cc_q.close
450
+ x = 0
451
+ begin
452
+ loop do
453
+ cc_q.pop(true).child_restarted!
454
+ end
455
+ rescue ThreadError => ex
456
+ raise unless ex.message == "queue empty"
457
+ end
458
+
459
+ @instance.instance_variable_get(:@ultravisor_child_castcall_fd).close
460
+ @instance.instance_variable_set(:@ultravisor_child_castcall_fd, nil)
461
+ @castcall_fd_writer.close
462
+ @castcall_fd_writer = nil
463
+ end
464
+
465
+ @instance = nil
466
+
467
+ if @thread
468
+ @thread = nil
469
+ @spawn_cv.broadcast
470
+ end
471
+
472
+ @spawn_id = nil
473
+ end
474
+ end
475
+ end
476
+
477
+ require_relative "./child/call"
478
+ require_relative "./child/call_receiver"
479
+ require_relative "./child/cast"
480
+ require_relative "./child/cast_receiver"
481
+ require_relative "./child/process_cast_call"