supervisor_machines 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +28 -0
- data/Cargo.lock +286 -0
- data/Cargo.toml +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +140 -0
- data/ext/supervisor_machines_native/core/Cargo.toml +24 -0
- data/ext/supervisor_machines_native/core/src/lib.rs +128 -0
- data/ext/supervisor_machines_native/core/src/supervisor.rs +155 -0
- data/ext/supervisor_machines_native/extconf.rb +3 -0
- data/ext/supervisor_machines_native/ffi/Cargo.toml +19 -0
- data/ext/supervisor_machines_native/ffi/extconf.rb +21 -0
- data/ext/supervisor_machines_native/ffi/src/decider.rs +199 -0
- data/ext/supervisor_machines_native/ffi/src/lib.rs +135 -0
- data/lib/supervisor_machines/errors.rb +9 -0
- data/lib/supervisor_machines/supervisor.rb +320 -0
- data/lib/supervisor_machines/version.rb +5 -0
- data/lib/supervisor_machines.rb +14 -0
- data/sig/supervisor_machines.rbs +173 -0
- metadata +111 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SupervisorMachines
|
|
4
|
+
# A supervision tree over threads.
|
|
5
|
+
#
|
|
6
|
+
# Which children a failure takes down, and whether restarting is still worth
|
|
7
|
+
# trying, belongs entirely to {Decider}. This class owns the lifecycle:
|
|
8
|
+
# spawning children, noticing when one stops, waiting out the backoff
|
|
9
|
+
# +chrono_machines+ hands it, and taking the tree down in reverse start order
|
|
10
|
+
# inside a +deadline_machines+ budget.
|
|
11
|
+
#
|
|
12
|
+
# A tree is +registering+ until {#start} fixes its shape, +running+ while it
|
|
13
|
+
# watches its children, and ends either +stopped+ or +failed+. Both endings
|
|
14
|
+
# are final: {#start} refuses a tree that is not registering, and a +failed+
|
|
15
|
+
# tree answers every {#stop} with {Escalated}.
|
|
16
|
+
#
|
|
17
|
+
# tree = SupervisorMachines::Supervisor.new(strategy: :one_for_all, restarts: 3, window: 60.0)
|
|
18
|
+
# tree.supervise(:cache) { Thread.new { cache.run } }
|
|
19
|
+
# tree.supervise(:warmup, policy: :transient) { Thread.new { warm } }
|
|
20
|
+
# tree.start
|
|
21
|
+
# tree.stop(grace: 5.0)
|
|
22
|
+
class Supervisor
|
|
23
|
+
# A registered child and whichever thread currently runs it. +epoch+ rises
|
|
24
|
+
# every time the child is taken down, so a thread the tree has already
|
|
25
|
+
# replaced neither reports its exit nor takes the child's place when its
|
|
26
|
+
# starter returns late. +done+ marks a child the tree has dropped, which no
|
|
27
|
+
# group restart brings back.
|
|
28
|
+
Child = Struct.new(:name, :policy, :starter, :thread, :epoch, :done)
|
|
29
|
+
|
|
30
|
+
# Wakes the monitor when the tree is being taken down.
|
|
31
|
+
STOP = Object.new
|
|
32
|
+
private_constant :STOP
|
|
33
|
+
|
|
34
|
+
attr_reader :strategy
|
|
35
|
+
|
|
36
|
+
# +restarts+ within +window+ seconds is the restart intensity; past it the
|
|
37
|
+
# tree escalates. +backoff+ is asked for the delay between a restart
|
|
38
|
+
# decision and the respawn; its attempt budget has to be one wider than the
|
|
39
|
+
# intensity, because the core escalates one restart after the last one it
|
|
40
|
+
# tolerates.
|
|
41
|
+
def initialize(strategy: :one_for_one, restarts: DEFAULT_RESTARTS,
|
|
42
|
+
window: DEFAULT_WINDOW_NS / NS_PER_SECOND.to_f, backoff: nil)
|
|
43
|
+
raise ArgumentError, "unknown strategy: #{strategy.inspect}" unless STRATEGIES.include?(strategy)
|
|
44
|
+
raise ArgumentError, "at most #{Decider::MAX_RESTARTS} restarts" if restarts > Decider::MAX_RESTARTS
|
|
45
|
+
|
|
46
|
+
@strategy = strategy
|
|
47
|
+
@restarts = restarts
|
|
48
|
+
@window = window
|
|
49
|
+
@backoff = backoff || ChronoMachines::Executor.new(max_attempts: restarts + 1)
|
|
50
|
+
@slots = []
|
|
51
|
+
@events = Thread::Queue.new
|
|
52
|
+
@mutex = Thread::Mutex.new
|
|
53
|
+
@halt = Thread::ConditionVariable.new
|
|
54
|
+
@state = :registering
|
|
55
|
+
@failed = nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Registers a child under +name+. The block is its starter: it returns the
|
|
59
|
+
# +Thread+ that runs the child, and is called again for every restart.
|
|
60
|
+
def supervise(name, policy: :permanent, &starter)
|
|
61
|
+
raise ArgumentError, "unknown policy: #{policy.inspect}" unless POLICIES.include?(policy)
|
|
62
|
+
|
|
63
|
+
@mutex.synchronize do
|
|
64
|
+
raise Error, "a #{@state} supervisor cannot take on a child" unless @state == :registering
|
|
65
|
+
|
|
66
|
+
@slots << Child.new(name, policy, starter, nil, 0, false)
|
|
67
|
+
end
|
|
68
|
+
self
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Fixes the shape of the tree, starts every child in registration order and
|
|
72
|
+
# begins watching them. A starter that will not run takes the children
|
|
73
|
+
# already up back down, leaves the tree failed, and raises what the starter
|
|
74
|
+
# raised.
|
|
75
|
+
#
|
|
76
|
+
# Raises {Error} for a tree that is past registering.
|
|
77
|
+
def start
|
|
78
|
+
@mutex.synchronize do
|
|
79
|
+
raise Error, "a #{@state} supervisor cannot be started" unless @state == :registering
|
|
80
|
+
|
|
81
|
+
@decider = Decider.new(children: @slots.size, strategy: @strategy,
|
|
82
|
+
restarts: @restarts, window_ns: to_ns(@window))
|
|
83
|
+
@state = :running
|
|
84
|
+
@monitor = Thread.new { monitor }
|
|
85
|
+
end
|
|
86
|
+
failure = launch
|
|
87
|
+
return self unless failure
|
|
88
|
+
|
|
89
|
+
rollback
|
|
90
|
+
raise failure
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Child names, in start order.
|
|
94
|
+
def children
|
|
95
|
+
@mutex.synchronize { @slots.map(&:name) }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Restarts still inside the intensity window.
|
|
99
|
+
def restarts
|
|
100
|
+
now = monotonic_ns
|
|
101
|
+
@mutex.synchronize { @decider ? @decider.recent_restarts(now) : 0 }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# True once the tree has escalated, and true from then on.
|
|
105
|
+
def terminated?
|
|
106
|
+
@mutex.synchronize { @state == :failed }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Takes every running child down in reverse start order, giving the whole
|
|
110
|
+
# shutdown +grace+ seconds, or all the time it needs when +grace+ is nil.
|
|
111
|
+
# A tree still registering has nothing to take down and stays registering.
|
|
112
|
+
#
|
|
113
|
+
# Raises {Escalated} for a tree that gave up, every time it is called.
|
|
114
|
+
def stop(grace: nil)
|
|
115
|
+
entered = @mutex.synchronize do
|
|
116
|
+
state = @state
|
|
117
|
+
@state = :stopped if state == :running
|
|
118
|
+
@halt.broadcast
|
|
119
|
+
state
|
|
120
|
+
end
|
|
121
|
+
return self if entered == :registering
|
|
122
|
+
|
|
123
|
+
@events.push(STOP) if entered == :running
|
|
124
|
+
failures = nil
|
|
125
|
+
DeadlineMachines.with_deadline(grace) do |deadline|
|
|
126
|
+
failures = shutdown
|
|
127
|
+
@monitor.kill unless @monitor.join(deadline.remaining)
|
|
128
|
+
end
|
|
129
|
+
raise Escalated, escalation if terminated?
|
|
130
|
+
raise failures.first unless failures.empty?
|
|
131
|
+
|
|
132
|
+
self
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Seconds a supervisor taking itself down waits for a killed child, since
|
|
136
|
+
# neither the monitor thread nor a failing {#start} has a caller budget to
|
|
137
|
+
# read.
|
|
138
|
+
ESCALATION_GRACE = 5.0
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
# Brings every child up in registration order, returning what a starter
|
|
143
|
+
# raised, or nil once the whole tree is up.
|
|
144
|
+
def launch
|
|
145
|
+
@slots.each_index { |index| spawn(index) }
|
|
146
|
+
nil
|
|
147
|
+
rescue Exception => e
|
|
148
|
+
@mutex.synchronize do
|
|
149
|
+
@state = :failed
|
|
150
|
+
@failed = unstarted(e)
|
|
151
|
+
end
|
|
152
|
+
e
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Takes down whatever a start brought up before it failed, the children and
|
|
156
|
+
# the monitor watching them, inside one budget.
|
|
157
|
+
def rollback
|
|
158
|
+
DeadlineMachines.with_deadline(ESCALATION_GRACE) do |deadline|
|
|
159
|
+
shutdown
|
|
160
|
+
reap(@monitor, deadline.remaining)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def running?
|
|
165
|
+
@state == :running
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def monitor
|
|
169
|
+
Thread.current.report_on_exception = false
|
|
170
|
+
loop do
|
|
171
|
+
event = @events.pop
|
|
172
|
+
break if event.equal?(STOP)
|
|
173
|
+
|
|
174
|
+
handle(*event)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# One child stopped. The core says what that means; this carries it out.
|
|
179
|
+
def handle(index, epoch, ending)
|
|
180
|
+
now = monotonic_ns
|
|
181
|
+
decision = @mutex.synchronize do
|
|
182
|
+
child = @slots[index]
|
|
183
|
+
next unless running? && child.epoch == epoch
|
|
184
|
+
|
|
185
|
+
child.thread = nil
|
|
186
|
+
child.done = true if child.policy == :temporary
|
|
187
|
+
@decider.stopped(index: index, policy: child.policy, exit: ending, now_ns: now)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
case decision
|
|
191
|
+
when Range then restart(decision)
|
|
192
|
+
when :escalate then escalate
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Retires the whole span in reverse start order and waits for it to go,
|
|
197
|
+
# waits out the backoff without holding the tree, then brings the span back
|
|
198
|
+
# in index order.
|
|
199
|
+
def restart(span)
|
|
200
|
+
retiring = @mutex.synchronize { span.reverse_each.filter_map { |index| retire(@slots[index]) } }
|
|
201
|
+
DeadlineMachines.with_deadline(ESCALATION_GRACE) do |deadline|
|
|
202
|
+
retiring.each { |thread| reap(thread, deadline.remaining) }
|
|
203
|
+
end
|
|
204
|
+
return escalate(abandoned) if retiring.any?(&:alive?)
|
|
205
|
+
|
|
206
|
+
failure = respawn(span)
|
|
207
|
+
escalate(failure) if failure
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# The reason a respawn could not happen, or nil once the span is running.
|
|
211
|
+
def respawn(span)
|
|
212
|
+
delay = @backoff.next_delay(attempt: restarts)
|
|
213
|
+
@mutex.synchronize { @halt.wait(@mutex, delay) if delay && running? }
|
|
214
|
+
span.each { |index| spawn(index) }
|
|
215
|
+
nil
|
|
216
|
+
rescue Exception => e
|
|
217
|
+
unstarted(e)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def escalate(reason = escalation)
|
|
221
|
+
@mutex.synchronize do
|
|
222
|
+
@state = :failed
|
|
223
|
+
@failed = reason
|
|
224
|
+
end
|
|
225
|
+
shutdown(ESCALATION_GRACE)
|
|
226
|
+
raise Escalated, reason
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Reverse start order, each child killed and then waited for inside one
|
|
230
|
+
# budget shared by the whole tree. Touches no tree state, so a shutdown
|
|
231
|
+
# under way in the monitor never holds up the one in {#stop}. Returns what
|
|
232
|
+
# the children raised on their way out, having stopped all of them.
|
|
233
|
+
def shutdown(grace = DeadlineMachines.remaining)
|
|
234
|
+
failures = []
|
|
235
|
+
DeadlineMachines.with_deadline(grace) do |deadline|
|
|
236
|
+
@slots.reverse_each do |child|
|
|
237
|
+
thread = child.thread
|
|
238
|
+
next unless thread
|
|
239
|
+
|
|
240
|
+
failures << reap(thread, deadline.remaining)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
failures.compact
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Kills the thread and waits out the rest of the budget for it, reporting
|
|
247
|
+
# whatever it raised on the way out. A thread that outlives the budget is
|
|
248
|
+
# still alive afterwards.
|
|
249
|
+
def reap(thread, remaining)
|
|
250
|
+
thread.kill
|
|
251
|
+
thread.join(remaining)
|
|
252
|
+
nil
|
|
253
|
+
rescue Exception => e
|
|
254
|
+
e
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Calls the child's starter outside the lock, under the generation the tree
|
|
258
|
+
# is in when the call goes out. A child the tree has dropped, and every
|
|
259
|
+
# child of a tree that is no longer running, is left alone.
|
|
260
|
+
def spawn(index)
|
|
261
|
+
child = @slots[index]
|
|
262
|
+
generation = @mutex.synchronize { child.epoch if running? && !child.done }
|
|
263
|
+
install(index, generation, child.starter.call) if generation
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Hands the started thread to the child, unless the tree has moved past the
|
|
267
|
+
# generation that asked for it, in which case the thread is killed instead
|
|
268
|
+
# of left running with nothing watching it.
|
|
269
|
+
def install(index, generation, thread)
|
|
270
|
+
installed = @mutex.synchronize do
|
|
271
|
+
child = @slots[index]
|
|
272
|
+
next false unless running? && child.epoch == generation
|
|
273
|
+
|
|
274
|
+
child.thread = thread
|
|
275
|
+
watch(index, generation, thread)
|
|
276
|
+
true
|
|
277
|
+
end
|
|
278
|
+
reap(thread, ESCALATION_GRACE) unless installed
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Invalidates the watcher following the child and hands its thread back to
|
|
282
|
+
# be reaped outside the lock.
|
|
283
|
+
def retire(child)
|
|
284
|
+
child.epoch += 1
|
|
285
|
+
thread = child.thread
|
|
286
|
+
child.thread = nil
|
|
287
|
+
thread
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# One thread per running child, blocked on it, reporting how it ended.
|
|
291
|
+
def watch(index, epoch, thread)
|
|
292
|
+
Thread.new do
|
|
293
|
+
thread.join
|
|
294
|
+
@events.push([index, epoch, :normal])
|
|
295
|
+
rescue Exception
|
|
296
|
+
@events.push([index, epoch, :abnormal])
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def escalation
|
|
301
|
+
@failed || "#{@strategy} supervisor escalated: more than #{@restarts} restarts in #{@window} seconds"
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def abandoned
|
|
305
|
+
"#{@strategy} supervisor escalated: a retired child would not stop within #{ESCALATION_GRACE} seconds"
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def unstarted(error)
|
|
309
|
+
"#{@strategy} supervisor escalated: #{error.message}"
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def monotonic_ns
|
|
313
|
+
DeadlineMachines.clock.now_ns
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def to_ns(seconds)
|
|
317
|
+
DeadlineMachines::Deadline.to_ns(seconds)
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'chrono_machines'
|
|
4
|
+
require 'deadline_machines'
|
|
5
|
+
|
|
6
|
+
require_relative 'supervisor_machines/version'
|
|
7
|
+
|
|
8
|
+
# Decider is defined by the native extension, which binds the
|
|
9
|
+
# `supervisor-machines` crate; there is no pure-Ruby implementation to fall
|
|
10
|
+
# back to.
|
|
11
|
+
require 'supervisor_machines_native/supervisor_machines_native'
|
|
12
|
+
|
|
13
|
+
require_relative 'supervisor_machines/errors'
|
|
14
|
+
require_relative 'supervisor_machines/supervisor'
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# OTP-style supervision over Ruby threads.
|
|
2
|
+
#
|
|
3
|
+
# `Decider` is defined by the native extension, which binds the
|
|
4
|
+
# `supervisor-machines` crate: it decides which children a failure takes down
|
|
5
|
+
# and when restarting has stopped being worth it. `Supervisor` owns everything
|
|
6
|
+
# the core refuses to touch, which is every thread.
|
|
7
|
+
module SupervisorMachines
|
|
8
|
+
VERSION: String
|
|
9
|
+
|
|
10
|
+
# Nanoseconds in a second. Seconds are what the Ruby surface takes; the core
|
|
11
|
+
# is fed nanosecond integers.
|
|
12
|
+
NS_PER_SECOND: Integer
|
|
13
|
+
|
|
14
|
+
# Restarts OTP tolerates by default before escalating.
|
|
15
|
+
DEFAULT_RESTARTS: Integer
|
|
16
|
+
|
|
17
|
+
# The window OTP measures those restarts over, five seconds.
|
|
18
|
+
DEFAULT_WINDOW_NS: Integer
|
|
19
|
+
|
|
20
|
+
# Which siblings a failure takes down with it.
|
|
21
|
+
type strategy = :one_for_one | :one_for_all | :rest_for_one
|
|
22
|
+
|
|
23
|
+
# When a child is worth restarting.
|
|
24
|
+
type policy = :permanent | :transient | :temporary
|
|
25
|
+
|
|
26
|
+
# How a child's run ended.
|
|
27
|
+
type exit_kind = :normal | :abnormal
|
|
28
|
+
|
|
29
|
+
# What the core decided: leave it stopped, restart this inclusive range of
|
|
30
|
+
# child indices, or give up.
|
|
31
|
+
type decision = :ignore | :escalate | Range[Integer]
|
|
32
|
+
|
|
33
|
+
# Every strategy, in the order the core declares them. Anything else raises
|
|
34
|
+
# `ArgumentError`.
|
|
35
|
+
STRATEGIES: Array[strategy]
|
|
36
|
+
|
|
37
|
+
# Every policy, in the order the core declares them. Anything else raises
|
|
38
|
+
# `ArgumentError`.
|
|
39
|
+
POLICIES: Array[policy]
|
|
40
|
+
|
|
41
|
+
class Error < StandardError
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Raised once a supervisor has spent its restart intensity.
|
|
45
|
+
class Escalated < Error
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# The core's decision machine. A child count and a restart history are part
|
|
49
|
+
# of its Rust type, so the count given here is dispatched to one of
|
|
50
|
+
# `SUPPORTED_CHILDREN` monomorphised shapes.
|
|
51
|
+
class Decider
|
|
52
|
+
# Restart timestamps every shape keeps.
|
|
53
|
+
HISTORY: Integer
|
|
54
|
+
|
|
55
|
+
# The child counts a decider can be built for. Anything else raises
|
|
56
|
+
# `ArgumentError`.
|
|
57
|
+
SUPPORTED_CHILDREN: Array[Integer]
|
|
58
|
+
|
|
59
|
+
def self.new: (
|
|
60
|
+
children: Integer,
|
|
61
|
+
strategy: strategy,
|
|
62
|
+
restarts: Integer,
|
|
63
|
+
window_ns: Integer
|
|
64
|
+
) -> instance
|
|
65
|
+
|
|
66
|
+
# Feeds one stopped child to the core and returns what it decided.
|
|
67
|
+
def stopped: (
|
|
68
|
+
index: Integer,
|
|
69
|
+
policy: policy,
|
|
70
|
+
exit: exit_kind,
|
|
71
|
+
now_ns: Integer
|
|
72
|
+
) -> decision
|
|
73
|
+
|
|
74
|
+
def children: () -> Integer
|
|
75
|
+
|
|
76
|
+
def strategy: () -> strategy
|
|
77
|
+
|
|
78
|
+
# True once the core has escalated, and true from then on.
|
|
79
|
+
def terminated?: () -> bool
|
|
80
|
+
|
|
81
|
+
# Restarts still inside the intensity window as of `now_ns`.
|
|
82
|
+
def recent_restarts: (Integer) -> Integer
|
|
83
|
+
|
|
84
|
+
def inspect: () -> String
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# A supervision tree over threads.
|
|
88
|
+
class Supervisor
|
|
89
|
+
# Where a tree is in its lifecycle: registering children, running them, or
|
|
90
|
+
# ended — `stopped` by its caller, `failed` by giving up.
|
|
91
|
+
type state = :registering | :running | :stopped | :failed
|
|
92
|
+
|
|
93
|
+
# Seconds a supervisor taking itself down waits for a killed child.
|
|
94
|
+
ESCALATION_GRACE: Float
|
|
95
|
+
|
|
96
|
+
# Anything that can price the wait between a restart decision and the
|
|
97
|
+
# respawn, which is `ChronoMachines::Executor` unless told otherwise.
|
|
98
|
+
interface _Backoff
|
|
99
|
+
def next_delay: (attempt: Integer) -> Numeric?
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# A registered child and whichever thread currently runs it.
|
|
103
|
+
class Child < ::Struct[untyped]
|
|
104
|
+
attr_accessor name: untyped
|
|
105
|
+
attr_accessor policy: policy
|
|
106
|
+
attr_accessor starter: ^() -> Thread
|
|
107
|
+
attr_accessor thread: Thread?
|
|
108
|
+
attr_accessor epoch: Integer
|
|
109
|
+
attr_accessor done: bool
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
attr_reader strategy: strategy
|
|
113
|
+
|
|
114
|
+
# `restarts` within `window` seconds is the restart intensity.
|
|
115
|
+
def initialize: (
|
|
116
|
+
?strategy: strategy,
|
|
117
|
+
?restarts: Integer,
|
|
118
|
+
?window: Float,
|
|
119
|
+
?backoff: _Backoff?
|
|
120
|
+
) -> void
|
|
121
|
+
|
|
122
|
+
# Registers a child under `name`. The block returns the `Thread` that runs
|
|
123
|
+
# it, and is called again for every restart.
|
|
124
|
+
def supervise: (untyped name, ?policy: policy) { () -> Thread } -> self
|
|
125
|
+
|
|
126
|
+
# Fixes the shape of the tree and starts every child in registration
|
|
127
|
+
# order. Raises `ArgumentError` for a child count the core has no shape
|
|
128
|
+
# for, `Error` for a tree that is past registering, and whatever a starter
|
|
129
|
+
# raised after taking the children already up back down.
|
|
130
|
+
def start: () -> self
|
|
131
|
+
|
|
132
|
+
# Child names, in start order.
|
|
133
|
+
def children: () -> Array[untyped]
|
|
134
|
+
|
|
135
|
+
# Restarts still inside the intensity window.
|
|
136
|
+
def restarts: () -> Integer
|
|
137
|
+
|
|
138
|
+
def terminated?: () -> bool
|
|
139
|
+
|
|
140
|
+
# Takes every running child down in reverse start order within `grace`
|
|
141
|
+
# seconds, or all the time it needs when `grace` is nil. A tree still
|
|
142
|
+
# registering has nothing to take down. Raises `Escalated` for a tree that
|
|
143
|
+
# gave up, every time it is called.
|
|
144
|
+
def stop: (?grace: Float?) -> self
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
# What a starter raised, after the children already up have been taken
|
|
149
|
+
# back down, or nil once the whole tree is up.
|
|
150
|
+
def launch: () -> Exception?
|
|
151
|
+
def rollback: () -> void
|
|
152
|
+
def running?: () -> bool
|
|
153
|
+
def monitor: () -> void
|
|
154
|
+
def handle: (Integer index, Integer epoch, exit_kind ending) -> void
|
|
155
|
+
def restart: (Range[Integer] span) -> void
|
|
156
|
+
def respawn: (Range[Integer] span) -> String?
|
|
157
|
+
def escalate: (?String reason) -> void
|
|
158
|
+
def shutdown: (?Float grace) -> Array[Exception]
|
|
159
|
+
def reap: (Thread thread, Float? remaining) -> Exception?
|
|
160
|
+
def spawn: (Integer index) -> void
|
|
161
|
+
|
|
162
|
+
# Installs a started thread under the generation that asked for it, killing
|
|
163
|
+
# it when the tree has moved past that generation.
|
|
164
|
+
def install: (Integer index, Integer generation, Thread thread) -> void
|
|
165
|
+
def retire: (Child child) -> Thread?
|
|
166
|
+
def watch: (Integer index, Integer epoch, Thread thread) -> void
|
|
167
|
+
def escalation: () -> String
|
|
168
|
+
def abandoned: () -> String
|
|
169
|
+
def unstarted: (Exception error) -> String
|
|
170
|
+
def monotonic_ns: () -> Integer
|
|
171
|
+
def to_ns: (Float seconds) -> Integer
|
|
172
|
+
end
|
|
173
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: supervisor_machines
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Abdelkader Boudih
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: chrono_machines
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.11'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.11'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: deadline_machines
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.1'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rb_sys
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.9'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.9'
|
|
54
|
+
description: 'A supervision tree that restarts what died the way OTP would: one_for_one,
|
|
55
|
+
one_for_all or rest_for_one, bounded by a restart intensity that escalates instead
|
|
56
|
+
of thrashing. The decisions come from the no_std supervisor-machines crate; Ruby
|
|
57
|
+
owns the threads, the restart backoff comes from chrono_machines and the shutdown
|
|
58
|
+
budget from deadline_machines.'
|
|
59
|
+
email:
|
|
60
|
+
- oss@seuros.com
|
|
61
|
+
executables: []
|
|
62
|
+
extensions:
|
|
63
|
+
- ext/supervisor_machines_native/extconf.rb
|
|
64
|
+
extra_rdoc_files: []
|
|
65
|
+
files:
|
|
66
|
+
- CHANGELOG.md
|
|
67
|
+
- Cargo.lock
|
|
68
|
+
- Cargo.toml
|
|
69
|
+
- LICENSE.txt
|
|
70
|
+
- README.md
|
|
71
|
+
- ext/supervisor_machines_native/core/Cargo.toml
|
|
72
|
+
- ext/supervisor_machines_native/core/src/lib.rs
|
|
73
|
+
- ext/supervisor_machines_native/core/src/supervisor.rs
|
|
74
|
+
- ext/supervisor_machines_native/extconf.rb
|
|
75
|
+
- ext/supervisor_machines_native/ffi/Cargo.toml
|
|
76
|
+
- ext/supervisor_machines_native/ffi/extconf.rb
|
|
77
|
+
- ext/supervisor_machines_native/ffi/src/decider.rs
|
|
78
|
+
- ext/supervisor_machines_native/ffi/src/lib.rs
|
|
79
|
+
- lib/supervisor_machines.rb
|
|
80
|
+
- lib/supervisor_machines/errors.rb
|
|
81
|
+
- lib/supervisor_machines/supervisor.rb
|
|
82
|
+
- lib/supervisor_machines/version.rb
|
|
83
|
+
- sig/supervisor_machines.rbs
|
|
84
|
+
homepage: https://github.com/seuros/supervisor_machines
|
|
85
|
+
licenses:
|
|
86
|
+
- MIT
|
|
87
|
+
metadata:
|
|
88
|
+
source_code_uri: https://github.com/seuros/supervisor_machines
|
|
89
|
+
changelog_uri: https://github.com/seuros/supervisor_machines/blob/main/CHANGELOG.md
|
|
90
|
+
bug_tracker_uri: https://github.com/seuros/supervisor_machines/issues
|
|
91
|
+
rubygems_mfa_required: 'true'
|
|
92
|
+
cargo_crate_name: supervisor_machines_native
|
|
93
|
+
cargo_manifest_path: ext/supervisor_machines_native/ffi/Cargo.toml
|
|
94
|
+
rdoc_options: []
|
|
95
|
+
require_paths:
|
|
96
|
+
- lib
|
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - ">="
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: 4.0.0
|
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
103
|
+
requirements:
|
|
104
|
+
- - ">="
|
|
105
|
+
- !ruby/object:Gem::Version
|
|
106
|
+
version: '0'
|
|
107
|
+
requirements: []
|
|
108
|
+
rubygems_version: 4.0.20
|
|
109
|
+
specification_version: 4
|
|
110
|
+
summary: OTP-style supervision trees over Ruby threads
|
|
111
|
+
test_files: []
|