ractor_shepherd 0.1.0
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 +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +215 -0
- data/lib/ractor_shepherd/address.rb +100 -0
- data/lib/ractor_shepherd/core/backoff.rb +59 -0
- data/lib/ractor_shepherd/core/event.rb +67 -0
- data/lib/ractor_shepherd/core/restart_intensity.rb +32 -0
- data/lib/ractor_shepherd/core/restart_policy.rb +34 -0
- data/lib/ractor_shepherd/core/strategy_planner.rb +44 -0
- data/lib/ractor_shepherd/errors.rb +57 -0
- data/lib/ractor_shepherd/event_logger.rb +70 -0
- data/lib/ractor_shepherd/facade.rb +75 -0
- data/lib/ractor_shepherd/runtime/call.rb +89 -0
- data/lib/ractor_shepherd/runtime/child_runner.rb +64 -0
- data/lib/ractor_shepherd/runtime/child_state.rb +50 -0
- data/lib/ractor_shepherd/runtime/compat.rb +41 -0
- data/lib/ractor_shepherd/runtime/context.rb +118 -0
- data/lib/ractor_shepherd/runtime/protocol.rb +55 -0
- data/lib/ractor_shepherd/runtime/supervisor_server.rb +506 -0
- data/lib/ractor_shepherd/runtime/timer.rb +53 -0
- data/lib/ractor_shepherd/server.rb +62 -0
- data/lib/ractor_shepherd/spec.rb +238 -0
- data/lib/ractor_shepherd/supervisor_ref.rb +104 -0
- data/lib/ractor_shepherd/version.rb +5 -0
- data/lib/ractor_shepherd/worker.rb +33 -0
- data/lib/ractor_shepherd.rb +37 -0
- data/sig/ractor_shepherd.rbs +179 -0
- metadata +72 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RactorShepherd
|
|
4
|
+
# What a child is. Immutable and shareable.
|
|
5
|
+
#
|
|
6
|
+
# - `type`: `:worker` or `:supervisor`
|
|
7
|
+
# - `start`: a Class for a worker, a {SupervisorSpec} for a supervisor
|
|
8
|
+
ChildSpec = Data.define(:id, :type, :start, :args, :kwargs, :restart,
|
|
9
|
+
:shutdown_timeout, :start_timeout, :restart_delay)
|
|
10
|
+
|
|
11
|
+
# What a supervisor is. `kind` is `:static` or `:dynamic`.
|
|
12
|
+
SupervisorSpec = Data.define(:kind, :strategy, :children, :max_restarts,
|
|
13
|
+
:max_seconds, :max_children, :on_unresponsive)
|
|
14
|
+
|
|
15
|
+
# Exponential backoff settings.
|
|
16
|
+
BackoffSpec = Data.define(:initial, :max, :factor)
|
|
17
|
+
|
|
18
|
+
# Validates and builds specs. Every violation is listed in a single {InvalidSpec}.
|
|
19
|
+
#
|
|
20
|
+
# This is part of the functional core: no Ractors and no clock reads.
|
|
21
|
+
# (`Ractor.make_shareable` is used here purely as a conversion.)
|
|
22
|
+
module Validator
|
|
23
|
+
RESTARTS = %i[permanent transient temporary].freeze
|
|
24
|
+
STRATEGIES = %i[one_for_one one_for_all rest_for_one].freeze
|
|
25
|
+
ON_UNRESPONSIVE = %i[escalate abandon].freeze
|
|
26
|
+
|
|
27
|
+
SHAREABLE_HINT = "Procs, Threads and Mutexes cannot cross Ractors. " \
|
|
28
|
+
"Pass plain values and hand the behaviour over as a class."
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
# @return [ChildSpec]
|
|
33
|
+
def worker_spec(id, klass, args: [], kwargs: {}, restart: :permanent,
|
|
34
|
+
shutdown_timeout: 5.0, start_timeout: 5.0, restart_delay: nil, allow_nil_id: false)
|
|
35
|
+
errors = []
|
|
36
|
+
cause = nil
|
|
37
|
+
|
|
38
|
+
check_id(errors, id, allow_nil: allow_nil_id)
|
|
39
|
+
check_worker_class(errors, klass)
|
|
40
|
+
shareable_args, cause = share(errors, "args", args, cause)
|
|
41
|
+
shareable_kwargs, cause = share(errors, "kwargs", kwargs, cause)
|
|
42
|
+
check_restart(errors, restart)
|
|
43
|
+
check_timeout(errors, "shutdown_timeout", shutdown_timeout)
|
|
44
|
+
check_timeout(errors, "start_timeout", start_timeout)
|
|
45
|
+
delay = check_restart_delay(errors, restart_delay)
|
|
46
|
+
|
|
47
|
+
fail_spec!(errors, "worker #{id.inspect}", cause)
|
|
48
|
+
|
|
49
|
+
build(ChildSpec.new(id: id, type: :worker, start: klass,
|
|
50
|
+
args: shareable_args, kwargs: shareable_kwargs, restart: restart,
|
|
51
|
+
shutdown_timeout: shutdown_timeout, start_timeout: start_timeout,
|
|
52
|
+
restart_delay: delay))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @return [ChildSpec] type: :supervisor
|
|
56
|
+
def supervisor_spec(id, kind: :static, strategy: :one_for_one, children: [], max_restarts: 3,
|
|
57
|
+
max_seconds: 5.0, max_children: nil, on_unresponsive: :escalate,
|
|
58
|
+
restart: :permanent, shutdown_timeout: :infinity, start_timeout: :infinity,
|
|
59
|
+
restart_delay: nil, allow_nil_id: false)
|
|
60
|
+
errors = []
|
|
61
|
+
check_id(errors, id, allow_nil: allow_nil_id)
|
|
62
|
+
inner = supervisor_body(errors, kind: kind, strategy: strategy, children: children,
|
|
63
|
+
max_restarts: max_restarts, max_seconds: max_seconds,
|
|
64
|
+
max_children: max_children, on_unresponsive: on_unresponsive)
|
|
65
|
+
check_restart(errors, restart)
|
|
66
|
+
check_timeout(errors, "shutdown_timeout", shutdown_timeout)
|
|
67
|
+
check_timeout(errors, "start_timeout", start_timeout)
|
|
68
|
+
delay = check_restart_delay(errors, restart_delay)
|
|
69
|
+
|
|
70
|
+
fail_spec!(errors, "supervisor #{id.inspect}", nil)
|
|
71
|
+
|
|
72
|
+
build(ChildSpec.new(id: id, type: :supervisor, start: inner,
|
|
73
|
+
args: [], kwargs: {}, restart: restart,
|
|
74
|
+
shutdown_timeout: shutdown_timeout, start_timeout: start_timeout,
|
|
75
|
+
restart_delay: delay))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# For a root supervisor: returns a bare {SupervisorSpec}, not wrapped in a ChildSpec.
|
|
79
|
+
def root_spec(kind: :static, strategy: :one_for_one, children: [], max_restarts: 3,
|
|
80
|
+
max_seconds: 5.0, max_children: nil, on_unresponsive: :escalate)
|
|
81
|
+
errors = []
|
|
82
|
+
inner = supervisor_body(errors, kind: kind, strategy: strategy, children: children,
|
|
83
|
+
max_restarts: max_restarts, max_seconds: max_seconds,
|
|
84
|
+
max_children: max_children, on_unresponsive: on_unresponsive)
|
|
85
|
+
fail_spec!(errors, "supervisor", nil)
|
|
86
|
+
build(inner)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Child ids must be unique within one supervisor.
|
|
90
|
+
def check_unique_ids(errors, children)
|
|
91
|
+
ids = children.filter_map { |c| c.id if c.respond_to?(:id) }
|
|
92
|
+
dups = ids.tally.select { |_, n| n > 1 }.keys
|
|
93
|
+
errors << "duplicated child ids: #{dups.map(&:inspect).join(", ")}" unless dups.empty?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def supervisor_body(errors, kind:, strategy:, children:, max_restarts:, max_seconds:,
|
|
97
|
+
max_children:, on_unresponsive:)
|
|
98
|
+
errors << "kind must be :static or :dynamic (got #{kind.inspect})" unless %i[static dynamic].include?(kind)
|
|
99
|
+
strategy = check_strategy(errors, strategy, kind)
|
|
100
|
+
check_children(errors, children, kind)
|
|
101
|
+
check_non_negative_integer(errors, "max_restarts", max_restarts)
|
|
102
|
+
check_positive_number(errors, "max_seconds", max_seconds)
|
|
103
|
+
check_max_children(errors, max_children)
|
|
104
|
+
unless ON_UNRESPONSIVE.include?(on_unresponsive)
|
|
105
|
+
errors << "on_unresponsive must be one of #{ON_UNRESPONSIVE.inspect} (got #{on_unresponsive.inspect})"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
SupervisorSpec.new(kind: kind, strategy: strategy, children: children.dup.freeze,
|
|
109
|
+
max_restarts: max_restarts, max_seconds: max_seconds,
|
|
110
|
+
max_children: max_children, on_unresponsive: on_unresponsive)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def check_id(errors, id, allow_nil: false)
|
|
114
|
+
return if id.nil? && allow_nil
|
|
115
|
+
return if id.is_a?(Symbol) || id.is_a?(Integer)
|
|
116
|
+
return if id.is_a?(String) && id.frozen?
|
|
117
|
+
|
|
118
|
+
errors << "id must be a Symbol, an Integer or a frozen String (got #{id.inspect})"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def check_worker_class(errors, klass)
|
|
122
|
+
unless klass.is_a?(Class)
|
|
123
|
+
errors << "start must be a Class (got #{klass.inspect})"
|
|
124
|
+
return
|
|
125
|
+
end
|
|
126
|
+
return if klass.include?(Worker)
|
|
127
|
+
|
|
128
|
+
errors << "#{klass} must include RactorShepherd::Worker"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def check_restart(errors, restart)
|
|
132
|
+
return if RESTARTS.include?(restart)
|
|
133
|
+
|
|
134
|
+
errors << "restart must be one of #{RESTARTS.inspect} (got #{restart.inspect})"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def check_strategy(errors, strategy, kind)
|
|
138
|
+
if kind == :dynamic
|
|
139
|
+
errors << "dynamic supervisors only support :one_for_one (got #{strategy.inspect})" unless
|
|
140
|
+
strategy == :one_for_one
|
|
141
|
+
return :one_for_one
|
|
142
|
+
end
|
|
143
|
+
errors << "strategy must be one of #{STRATEGIES.inspect} (got #{strategy.inspect})" unless
|
|
144
|
+
STRATEGIES.include?(strategy)
|
|
145
|
+
strategy
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def check_children(errors, children, kind)
|
|
149
|
+
unless children.is_a?(Array)
|
|
150
|
+
errors << "children must be an Array (got #{children.inspect})"
|
|
151
|
+
return
|
|
152
|
+
end
|
|
153
|
+
if kind == :dynamic && !children.empty?
|
|
154
|
+
errors << "dynamic supervisors must start with no children"
|
|
155
|
+
return
|
|
156
|
+
end
|
|
157
|
+
bad = children.grep_v(ChildSpec)
|
|
158
|
+
errors << "children must all be ChildSpec (got #{bad.map(&:class).uniq.inspect})" unless bad.empty?
|
|
159
|
+
check_unique_ids(errors, children)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def check_timeout(errors, name, value)
|
|
163
|
+
return if value == :infinity
|
|
164
|
+
return if value.is_a?(Numeric) && value.positive?
|
|
165
|
+
|
|
166
|
+
errors << "#{name} must be a positive Numeric or :infinity (got #{value.inspect})"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def check_non_negative_integer(errors, name, value)
|
|
170
|
+
return if value.is_a?(Integer) && !value.negative?
|
|
171
|
+
|
|
172
|
+
errors << "#{name} must be an Integer >= 0 (got #{value.inspect})"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def check_positive_number(errors, name, value)
|
|
176
|
+
return if value.is_a?(Numeric) && value.positive?
|
|
177
|
+
|
|
178
|
+
errors << "#{name} must be a positive Numeric (got #{value.inspect})"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def check_max_children(errors, value)
|
|
182
|
+
return if value.nil?
|
|
183
|
+
return if value.is_a?(Integer) && value >= 1
|
|
184
|
+
|
|
185
|
+
errors << "max_children must be nil or an Integer >= 1 (got #{value.inspect})"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# @return [nil, Numeric, BackoffSpec]
|
|
189
|
+
def check_restart_delay(errors, value)
|
|
190
|
+
case value
|
|
191
|
+
when nil then nil
|
|
192
|
+
when Numeric
|
|
193
|
+
errors << "restart_delay must be >= 0 (got #{value.inspect})" if value.negative?
|
|
194
|
+
value
|
|
195
|
+
when Hash then backoff_spec(errors, value)
|
|
196
|
+
when BackoffSpec then value
|
|
197
|
+
else
|
|
198
|
+
errors << "restart_delay must be nil, a Numeric or a Hash (got #{value.inspect})"
|
|
199
|
+
nil
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def backoff_spec(errors, hash)
|
|
204
|
+
unknown = hash.keys - %i[initial max factor]
|
|
205
|
+
errors << "restart_delay has unknown keys: #{unknown.inspect}" unless unknown.empty?
|
|
206
|
+
initial = hash.fetch(:initial, 0.1)
|
|
207
|
+
max = hash.fetch(:max, 5.0)
|
|
208
|
+
factor = hash.fetch(:factor, 2.0)
|
|
209
|
+
errors << "restart_delay[:initial] must be a Numeric >= 0 (got #{initial.inspect})" unless
|
|
210
|
+
initial.is_a?(Numeric) && !initial.negative?
|
|
211
|
+
errors << "restart_delay[:max] must be a Numeric >= initial (got #{max.inspect})" unless
|
|
212
|
+
max.is_a?(Numeric) && initial.is_a?(Numeric) && max >= initial
|
|
213
|
+
errors << "restart_delay[:factor] must be a Numeric >= 1 (got #{factor.inspect})" unless
|
|
214
|
+
factor.is_a?(Numeric) && factor >= 1
|
|
215
|
+
BackoffSpec.new(initial: initial, max: max, factor: factor)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Make a shareable copy without freezing what the caller passed in.
|
|
219
|
+
def share(errors, name, value, cause)
|
|
220
|
+
[Ractor.make_shareable(value, copy: true), cause]
|
|
221
|
+
# Procs and Threads raise TypeError (allocator undefined); Mutexes raise Ractor::Error.
|
|
222
|
+
rescue Ractor::Error, TypeError => e
|
|
223
|
+
errors << "#{name} cannot be shared with a Ractor (#{e.class}: #{e.message}). #{SHAREABLE_HINT}"
|
|
224
|
+
[value, cause || e]
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def fail_spec!(errors, subject, cause)
|
|
228
|
+
return if errors.empty?
|
|
229
|
+
|
|
230
|
+
message = "invalid #{subject}:\n" + errors.map { |e| " - #{e}" }.join("\n")
|
|
231
|
+
raise InvalidSpec, message, cause: cause
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def build(data)
|
|
235
|
+
Ractor.make_shareable(data)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RactorShepherd
|
|
4
|
+
# What `which_children` reports about one child.
|
|
5
|
+
ChildInfo = Data.define(:id, :type, :status, :ref, :restart_count)
|
|
6
|
+
|
|
7
|
+
# Maps an error name reported by a supervisor back to its class.
|
|
8
|
+
REMOTE_ERRORS = Ractor.make_shareable(
|
|
9
|
+
[ChildNotFound, ChildUnavailable, InvalidOperation, InvalidSpec,
|
|
10
|
+
MaxChildrenReached, ProtocolError, StartError].to_h { |klass| [klass.name, klass] }
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
# A handle on a supervisor.
|
|
14
|
+
#
|
|
15
|
+
# It is shareable, so it can be handed to children or sent to other Ractors.
|
|
16
|
+
# Every query is a synchronous call over the control port, with a default
|
|
17
|
+
# timeout of five seconds.
|
|
18
|
+
SupervisorRef = Data.define(:name, :path, :ractor, :control_port) do
|
|
19
|
+
# @return [Array<ChildInfo>]
|
|
20
|
+
def which_children(timeout: 5) = request([:which_children], timeout)
|
|
21
|
+
|
|
22
|
+
# @return [Hash] { specs:, active:, workers:, supervisors: }
|
|
23
|
+
def count_children(timeout: 5) = request([:count_children], timeout)
|
|
24
|
+
|
|
25
|
+
# @return [Ractor, SupervisorRef, nil] nil while the child is being restarted
|
|
26
|
+
def whereis(id, timeout: 5) = request([:whereis, id], timeout)
|
|
27
|
+
|
|
28
|
+
# Build a lazily resolved address.
|
|
29
|
+
#
|
|
30
|
+
# sup.lookup(:jobs, :poller).cast([:refresh])
|
|
31
|
+
#
|
|
32
|
+
# @return [Address]
|
|
33
|
+
def lookup(*path, retries: 3, resolve_timeout: 1.0)
|
|
34
|
+
Address.new(self, path, retries: retries, resolve_timeout: resolve_timeout)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Object] the id of the child that was added
|
|
38
|
+
def start_child(child_spec, timeout: 5) = request([:start_child, child_spec], timeout)
|
|
39
|
+
|
|
40
|
+
def terminate_child(id, timeout: 5) = request([:terminate_child, id], timeout)
|
|
41
|
+
def restart_child(id, timeout: 5) = request([:restart_child, id], timeout)
|
|
42
|
+
def delete_child(id, timeout: 5) = request([:delete_child, id], timeout)
|
|
43
|
+
|
|
44
|
+
# Stop every child in reverse order, then the supervisor, and wait for it to finish.
|
|
45
|
+
#
|
|
46
|
+
# Returns true right away if it had already stopped.
|
|
47
|
+
def stop(reason = :normal, timeout: :infinity)
|
|
48
|
+
port = Ractor::Port.new
|
|
49
|
+
timer = nil
|
|
50
|
+
return true unless ractor.monitor(port) # already finished
|
|
51
|
+
|
|
52
|
+
timer = Runtime::Timer.for_current_ractor.after(timeout, port, Runtime::Protocol.timeout(0)) unless
|
|
53
|
+
timeout == :infinity
|
|
54
|
+
return true unless send_shutdown(reason) # it finished just before we asked
|
|
55
|
+
|
|
56
|
+
case port.receive
|
|
57
|
+
in [Runtime::Protocol::TIMEOUT, _] then raise CallTimeout, "#{path} did not stop within #{timeout}s"
|
|
58
|
+
else true # a monitor notification: it stopped
|
|
59
|
+
end
|
|
60
|
+
ensure
|
|
61
|
+
timer&.cancel
|
|
62
|
+
# unmonitor is unusable (see Runtime::Call). Just close the port.
|
|
63
|
+
port&.close
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Leaves one monitor registration behind when the supervisor is alive,
|
|
67
|
+
# because unmonitor is unusable (see Runtime::Call).
|
|
68
|
+
def alive?
|
|
69
|
+
port = Ractor::Port.new
|
|
70
|
+
ractor.monitor(port)
|
|
71
|
+
ensure
|
|
72
|
+
port&.close
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Wait for the supervisor to finish.
|
|
76
|
+
#
|
|
77
|
+
# Call this only from the Ractor that started it: once another Ractor has
|
|
78
|
+
# taken a Ractor's value, nobody else can.
|
|
79
|
+
#
|
|
80
|
+
# @raise [SupervisorCrashed] if it escalated instead of stopping
|
|
81
|
+
def join
|
|
82
|
+
ractor.value
|
|
83
|
+
self
|
|
84
|
+
rescue Ractor::RemoteError => e
|
|
85
|
+
raise SupervisorCrashed, "#{path} crashed: #{e.cause.class}: #{e.cause.message}", cause: e.cause
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def send_shutdown(reason)
|
|
91
|
+
control_port << Runtime::Protocol.shutdown(reason)
|
|
92
|
+
true
|
|
93
|
+
rescue Ractor::ClosedError
|
|
94
|
+
false
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def request(body, timeout)
|
|
98
|
+
case Runtime::Call.perform(ractor, control_port, body, timeout: timeout, down_error: SupervisorDown)
|
|
99
|
+
in [:ok, value] then value
|
|
100
|
+
in [:error, error_class, message] then raise(REMOTE_ERRORS.fetch(error_class, Error), message)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RactorShepherd
|
|
4
|
+
# The low level worker interface: you write the loop yourself.
|
|
5
|
+
#
|
|
6
|
+
# class Poller
|
|
7
|
+
# include RactorShepherd::Worker
|
|
8
|
+
#
|
|
9
|
+
# def initialize(url) = @url = url
|
|
10
|
+
#
|
|
11
|
+
# def run(ctx)
|
|
12
|
+
# until ctx.shutdown_requested?
|
|
13
|
+
# fetch(@url)
|
|
14
|
+
# ctx.sleep(1.0)
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# `initialize` plays the role of OTP's `init`. Do not make a synchronous call
|
|
20
|
+
# to the supervisor from there: the supervisor is waiting for this child to
|
|
21
|
+
# finish starting, so the call would deadlock until `start_timeout` fires.
|
|
22
|
+
module Worker
|
|
23
|
+
# @param ctx [RactorShepherd::Runtime::Context]
|
|
24
|
+
def run(ctx)
|
|
25
|
+
raise NotImplementedError, "#{self.class} must implement #run(ctx)"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Optional cleanup hook.
|
|
29
|
+
#
|
|
30
|
+
# @param reason [Symbol, Exception] :normal, :shutdown, or the exception that killed the worker
|
|
31
|
+
def terminate(reason) = nil # rubocop:disable Lint/UnusedMethodArgument
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ractor_shepherd/version"
|
|
4
|
+
require_relative "ractor_shepherd/errors"
|
|
5
|
+
require_relative "ractor_shepherd/runtime/compat"
|
|
6
|
+
|
|
7
|
+
RactorShepherd::Runtime::Compat.check!
|
|
8
|
+
|
|
9
|
+
require_relative "ractor_shepherd/worker"
|
|
10
|
+
require_relative "ractor_shepherd/core/backoff"
|
|
11
|
+
require_relative "ractor_shepherd/core/event"
|
|
12
|
+
require_relative "ractor_shepherd/core/restart_intensity"
|
|
13
|
+
require_relative "ractor_shepherd/core/restart_policy"
|
|
14
|
+
require_relative "ractor_shepherd/core/strategy_planner"
|
|
15
|
+
require_relative "ractor_shepherd/spec"
|
|
16
|
+
require_relative "ractor_shepherd/runtime/protocol"
|
|
17
|
+
require_relative "ractor_shepherd/runtime/timer"
|
|
18
|
+
require_relative "ractor_shepherd/runtime/call"
|
|
19
|
+
require_relative "ractor_shepherd/runtime/context"
|
|
20
|
+
require_relative "ractor_shepherd/server"
|
|
21
|
+
require_relative "ractor_shepherd/runtime/child_state"
|
|
22
|
+
require_relative "ractor_shepherd/runtime/child_runner"
|
|
23
|
+
require_relative "ractor_shepherd/runtime/supervisor_server"
|
|
24
|
+
require_relative "ractor_shepherd/supervisor_ref"
|
|
25
|
+
require_relative "ractor_shepherd/address"
|
|
26
|
+
require_relative "ractor_shepherd/event_logger"
|
|
27
|
+
require_relative "ractor_shepherd/facade"
|
|
28
|
+
|
|
29
|
+
# Supervise Ractors the way Erlang/OTP supervisors do.
|
|
30
|
+
#
|
|
31
|
+
# A supervisor starts its children, watches them, restarts them according to a
|
|
32
|
+
# declared policy, and escalates to its own parent when restarting stops helping.
|
|
33
|
+
# Supervisors can themselves be children, so trees are just supervisors all the way down.
|
|
34
|
+
#
|
|
35
|
+
# @see DESIGN.md
|
|
36
|
+
module RactorShepherd
|
|
37
|
+
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# Signatures for the public API.
|
|
2
|
+
# Runtime:: and Core:: are internal, so they are deliberately left out.
|
|
3
|
+
module RactorShepherd
|
|
4
|
+
VERSION: String
|
|
5
|
+
|
|
6
|
+
type child_id = Symbol | Integer | String
|
|
7
|
+
type restart_kind = :permanent | :transient | :temporary
|
|
8
|
+
type strategy = :one_for_one | :one_for_all | :rest_for_one
|
|
9
|
+
type timeout = Numeric | :infinity
|
|
10
|
+
type unresponsive_action = :escalate | :abandon
|
|
11
|
+
type restart_delay = nil | Numeric | Hash[Symbol, Numeric] | BackoffSpec
|
|
12
|
+
type child_status = :starting | :running | :stopping | :exited | :terminated
|
|
13
|
+
| :restart_scheduled | :start_failed | :unresponsive | :removed
|
|
14
|
+
type event = Hash[Symbol, untyped]
|
|
15
|
+
|
|
16
|
+
class Error < StandardError
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class InvalidSpec < Error
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class UnsupportedRuby < Error
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class StartError < Error
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class SupervisorCrashed < Error
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class MaxRestartsExceeded < Error
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class ChildUnresponsive < Error
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class CallTimeout < Error
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class SupervisorDown < Error
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class WorkerDown < Error
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
class ChildNotFound < Error
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class ChildUnavailable < Error
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class InvalidOperation < Error
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class MaxChildrenReached < Error
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class ProtocolError < Error
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class ShutdownSignal < Exception
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class BackoffSpec
|
|
65
|
+
attr_reader initial: Numeric
|
|
66
|
+
attr_reader max: Numeric
|
|
67
|
+
attr_reader factor: Numeric
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class SupervisorSpec
|
|
71
|
+
attr_reader kind: (:static | :dynamic)
|
|
72
|
+
attr_reader strategy: strategy
|
|
73
|
+
attr_reader children: Array[ChildSpec]
|
|
74
|
+
attr_reader max_restarts: Integer
|
|
75
|
+
attr_reader max_seconds: Numeric
|
|
76
|
+
attr_reader max_children: Integer?
|
|
77
|
+
attr_reader on_unresponsive: unresponsive_action
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class ChildSpec
|
|
81
|
+
attr_reader id: child_id?
|
|
82
|
+
attr_reader type: (:worker | :supervisor)
|
|
83
|
+
attr_reader start: (Class | SupervisorSpec)
|
|
84
|
+
attr_reader args: Array[untyped]
|
|
85
|
+
attr_reader kwargs: Hash[Symbol, untyped]
|
|
86
|
+
attr_reader restart: restart_kind
|
|
87
|
+
attr_reader shutdown_timeout: timeout
|
|
88
|
+
attr_reader start_timeout: timeout
|
|
89
|
+
attr_reader restart_delay: restart_delay
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class ChildInfo
|
|
93
|
+
attr_reader id: child_id
|
|
94
|
+
attr_reader type: (:worker | :supervisor)
|
|
95
|
+
attr_reader status: child_status
|
|
96
|
+
attr_reader ref: (Ractor | SupervisorRef)?
|
|
97
|
+
attr_reader restart_count: Integer
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
class SupervisorRef
|
|
101
|
+
attr_reader name: (Symbol | String)
|
|
102
|
+
attr_reader path: String
|
|
103
|
+
attr_reader ractor: Ractor
|
|
104
|
+
attr_reader control_port: untyped
|
|
105
|
+
|
|
106
|
+
def which_children: (?timeout: Numeric) -> Array[ChildInfo]
|
|
107
|
+
def count_children: (?timeout: Numeric) -> Hash[Symbol, Integer]
|
|
108
|
+
def whereis: (child_id, ?timeout: Numeric) -> (Ractor | SupervisorRef)?
|
|
109
|
+
def lookup: (*child_id, ?retries: Integer, ?resolve_timeout: Numeric) -> Address
|
|
110
|
+
def start_child: (ChildSpec, ?timeout: Numeric) -> child_id
|
|
111
|
+
def terminate_child: (child_id, ?timeout: Numeric) -> :ok
|
|
112
|
+
def restart_child: (child_id, ?timeout: Numeric) -> :ok
|
|
113
|
+
def delete_child: (child_id, ?timeout: Numeric) -> :ok
|
|
114
|
+
def stop: (?Symbol, ?timeout: timeout) -> bool
|
|
115
|
+
def alive?: () -> bool
|
|
116
|
+
def join: () -> SupervisorRef
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
class Address
|
|
120
|
+
attr_reader path: Array[child_id]
|
|
121
|
+
|
|
122
|
+
def ractor: () -> (Ractor | SupervisorRef)
|
|
123
|
+
def resolve!: () -> (Ractor | SupervisorRef)
|
|
124
|
+
def cast: (untyped) -> :ok
|
|
125
|
+
def call: (untyped, ?timeout: timeout) -> untyped
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
interface _Logger
|
|
129
|
+
def info: (String) -> void
|
|
130
|
+
def warn: (String) -> void
|
|
131
|
+
def error: (String) -> void
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
module EventLogger
|
|
135
|
+
def self.start: (untyped port, ?logger: _Logger) -> Thread
|
|
136
|
+
def self.level_for: (event) -> (:info | :warn | :error)
|
|
137
|
+
def self.format_event: (event) -> String
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
module Worker
|
|
141
|
+
def run: (untyped ctx) -> untyped
|
|
142
|
+
def terminate: ((Symbol | Exception)) -> untyped
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
module Server
|
|
146
|
+
include Worker
|
|
147
|
+
|
|
148
|
+
def context: () -> untyped
|
|
149
|
+
def handle_call: (untyped) -> untyped
|
|
150
|
+
def handle_cast: (untyped) -> untyped
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def self.worker: (child_id?, Class,
|
|
154
|
+
?args: Array[untyped], ?kwargs: Hash[Symbol, untyped],
|
|
155
|
+
?restart: restart_kind, ?shutdown_timeout: timeout,
|
|
156
|
+
?start_timeout: timeout, ?restart_delay: restart_delay) -> ChildSpec
|
|
157
|
+
|
|
158
|
+
def self.supervisor: (child_id, ?strategy: strategy, ?children: Array[ChildSpec],
|
|
159
|
+
?max_restarts: Integer, ?max_seconds: Numeric,
|
|
160
|
+
?on_unresponsive: unresponsive_action, ?restart: restart_kind,
|
|
161
|
+
?shutdown_timeout: timeout, ?start_timeout: timeout,
|
|
162
|
+
?restart_delay: restart_delay) -> ChildSpec
|
|
163
|
+
|
|
164
|
+
def self.dynamic_supervisor: (child_id, ?max_children: Integer?, ?max_restarts: Integer,
|
|
165
|
+
?max_seconds: Numeric, ?on_unresponsive: unresponsive_action,
|
|
166
|
+
?restart: restart_kind, ?shutdown_timeout: timeout,
|
|
167
|
+
?start_timeout: timeout, ?restart_delay: restart_delay) -> ChildSpec
|
|
168
|
+
|
|
169
|
+
def self.start: (name: (Symbol | String), ?strategy: strategy, ?children: Array[ChildSpec],
|
|
170
|
+
?max_restarts: Integer, ?max_seconds: Numeric,
|
|
171
|
+
?on_unresponsive: unresponsive_action, ?event_port: untyped,
|
|
172
|
+
?boot_timeout: timeout) -> SupervisorRef
|
|
173
|
+
|
|
174
|
+
def self.start_dynamic: (name: (Symbol | String), ?max_children: Integer?, ?max_restarts: Integer,
|
|
175
|
+
?max_seconds: Numeric, ?on_unresponsive: unresponsive_action,
|
|
176
|
+
?event_port: untyped, ?boot_timeout: timeout) -> SupervisorRef
|
|
177
|
+
|
|
178
|
+
def self.run: (**untyped) { (SupervisorRef) -> untyped } -> untyped
|
|
179
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ractor_shepherd
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yudai Takada
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: 'ractor_shepherd supervises Ractors the way Erlang/OTP supervisors do:
|
|
13
|
+
declarative restart strategies, restart intensity, supervision trees and ordered
|
|
14
|
+
graceful shutdown. No runtime dependencies.'
|
|
15
|
+
email:
|
|
16
|
+
- t.yudai92@gmail.com
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- CHANGELOG.md
|
|
22
|
+
- LICENSE.txt
|
|
23
|
+
- README.md
|
|
24
|
+
- lib/ractor_shepherd.rb
|
|
25
|
+
- lib/ractor_shepherd/address.rb
|
|
26
|
+
- lib/ractor_shepherd/core/backoff.rb
|
|
27
|
+
- lib/ractor_shepherd/core/event.rb
|
|
28
|
+
- lib/ractor_shepherd/core/restart_intensity.rb
|
|
29
|
+
- lib/ractor_shepherd/core/restart_policy.rb
|
|
30
|
+
- lib/ractor_shepherd/core/strategy_planner.rb
|
|
31
|
+
- lib/ractor_shepherd/errors.rb
|
|
32
|
+
- lib/ractor_shepherd/event_logger.rb
|
|
33
|
+
- lib/ractor_shepherd/facade.rb
|
|
34
|
+
- lib/ractor_shepherd/runtime/call.rb
|
|
35
|
+
- lib/ractor_shepherd/runtime/child_runner.rb
|
|
36
|
+
- lib/ractor_shepherd/runtime/child_state.rb
|
|
37
|
+
- lib/ractor_shepherd/runtime/compat.rb
|
|
38
|
+
- lib/ractor_shepherd/runtime/context.rb
|
|
39
|
+
- lib/ractor_shepherd/runtime/protocol.rb
|
|
40
|
+
- lib/ractor_shepherd/runtime/supervisor_server.rb
|
|
41
|
+
- lib/ractor_shepherd/runtime/timer.rb
|
|
42
|
+
- lib/ractor_shepherd/server.rb
|
|
43
|
+
- lib/ractor_shepherd/spec.rb
|
|
44
|
+
- lib/ractor_shepherd/supervisor_ref.rb
|
|
45
|
+
- lib/ractor_shepherd/version.rb
|
|
46
|
+
- lib/ractor_shepherd/worker.rb
|
|
47
|
+
- sig/ractor_shepherd.rbs
|
|
48
|
+
homepage: https://github.com/ydah/ractor_shepherd
|
|
49
|
+
licenses:
|
|
50
|
+
- MIT
|
|
51
|
+
metadata:
|
|
52
|
+
homepage_uri: https://github.com/ydah/ractor_shepherd
|
|
53
|
+
changelog_uri: https://github.com/ydah/ractor_shepherd/blob/main/CHANGELOG.md
|
|
54
|
+
rubygems_mfa_required: 'true'
|
|
55
|
+
rdoc_options: []
|
|
56
|
+
require_paths:
|
|
57
|
+
- lib
|
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '4.0'
|
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
requirements: []
|
|
69
|
+
rubygems_version: 4.0.16
|
|
70
|
+
specification_version: 4
|
|
71
|
+
summary: OTP-style supervisor for Ruby Ractors.
|
|
72
|
+
test_files: []
|