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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8166a49f66449c7fdd8d93c65e10e39a58c2e6590179112e2d34667477422c1e
4
+ data.tar.gz: 0a646565208fb4808b685bac16822a8df1a48b27440d90a7ad7f618b53b7254f
5
+ SHA512:
6
+ metadata.gz: 8c04a92b8ac6e949373dcc0ab3fa93d9120a8b71bb023edb317c3494ee458c13c55aec93f8ee7fa681b2117e7c785706cd802b8259c4cde861daab8ccbc83742
7
+ data.tar.gz: f9b15124d25db3eb8ac96e28b164853c4d21727f5cb7a4b4fa83866f0d9dd101bf56cc81e19dd1ae916959f6f26696c97d635ae9a7273c43f91f2db1f060d1bb
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-09-17
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Yudai Takada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,215 @@
1
+ # ractor_shepherd
2
+
3
+ Supervise Ruby Ractors the way Erlang/OTP supervisors do. When a child Ractor dies
4
+ with an exception, it is restarted according to a policy you declare; when
5
+ restarting stops helping, the failure is escalated to the supervisor above.
6
+ Supervisors can themselves be children, so a supervision tree is just supervisors
7
+ all the way down.
8
+
9
+ - Ruby 4.0 or later, and ready for the Ractor API changes in 4.1
10
+ - No runtime dependencies: standard library only
11
+ - The decision logic lives in a layer that never touches Ractors, so it is covered by ordinary unit tests
12
+
13
+ ## Installation
14
+
15
+ ```ruby
16
+ gem "ractor_shepherd"
17
+ ```
18
+
19
+ ## A minimal example
20
+
21
+ ```ruby
22
+ require "ractor_shepherd"
23
+
24
+ class Counter
25
+ include RactorShepherd::Server
26
+
27
+ def initialize(start = 0)
28
+ super()
29
+ @count = start
30
+ end
31
+
32
+ def handle_call(message)
33
+ case message
34
+ in :get then @count
35
+ end
36
+ end
37
+
38
+ def handle_cast(message)
39
+ case message
40
+ in [:add, n] then @count += n
41
+ in :crash then raise "boom"
42
+ end
43
+ end
44
+ end
45
+
46
+ events = Ractor::Port.new
47
+ RactorShepherd::EventLogger.start(events) # one line per event on stderr
48
+
49
+ RactorShepherd.run(name: :root, strategy: :one_for_one, max_restarts: 3, max_seconds: 5,
50
+ event_port: events,
51
+ children: [RactorShepherd.worker(:counter, Counter, args: [0])]) do |sup|
52
+ counter = sup.lookup(:counter)
53
+ counter.cast([:add, 2])
54
+ counter.call(:get, timeout: 5) #=> 2
55
+
56
+ counter.cast(:crash) # it dies, and is restarted with its state back at zero
57
+ end
58
+ ```
59
+
60
+ Runnable examples live in [`examples/`](examples).
61
+
62
+ | File | What it shows |
63
+ |---|---|
64
+ | `examples/basic.rb` | one_for_one and automatic restarts |
65
+ | `examples/tree.rb` | a supervision tree |
66
+ | `examples/dynamic.rb` | adding and removing children at runtime |
67
+ | `examples/graceful_shutdown.rb` | shutting down cleanly on a signal |
68
+ | `examples/crash_report.rb` | subscribing to events to report why a child died |
69
+
70
+ ## Two kinds of worker
71
+
72
+ ### `RactorShepherd::Worker`, the low level one
73
+
74
+ You write the loop yourself. `initialize` plays the role of OTP's `init`.
75
+
76
+ ```ruby
77
+ class Poller
78
+ include RactorShepherd::Worker
79
+
80
+ def initialize(url, interval)
81
+ super()
82
+ @url = url
83
+ @interval = interval
84
+ end
85
+
86
+ def run(ctx)
87
+ until ctx.shutdown_requested?
88
+ fetch(@url)
89
+ ctx.sleep(@interval) # returns as soon as a shutdown is requested
90
+ end
91
+ end
92
+
93
+ def terminate(reason) = nil # optional; reason is :normal, :shutdown, or an exception
94
+ end
95
+ ```
96
+
97
+ What `ctx` (a `Context`) gives you:
98
+
99
+ | Method | Description |
100
+ |---|---|
101
+ | `id` / `path` | this child's id and path, e.g. `"root/jobs/poller"` |
102
+ | `supervisor` | the parent `SupervisorRef`, which is how you reach siblings |
103
+ | `receive(timeout: nil)` | receive one message; nil on timeout; raises `ShutdownSignal` after a shutdown request |
104
+ | `shutdown_requested?` | has a shutdown been requested? |
105
+ | `sleep(seconds)` | sleep, but wake on shutdown; true if it slept the whole time, false if cut short |
106
+ | `emit(name, **data)` | publish an application event to the event port |
107
+
108
+ ### `RactorShepherd::Server`, the GenServer style one
109
+
110
+ `run` is already written; you fill in `handle_call` and `handle_cast`. Whatever
111
+ `handle_call` returns becomes the reply.
112
+
113
+ ## Restart strategies
114
+
115
+ With children `[a, b, c, d]`, when `b` dies:
116
+
117
+ | Strategy | Stopped (reverse start order) | Started (start order) |
118
+ |---|---|---|
119
+ | `:one_for_one` | nothing | `[b]` |
120
+ | `:one_for_all` | `[d, c, a]` | `[a, b, c, d]` |
121
+ | `:rest_for_one` | `[d, c]` | `[b, c, d]` |
122
+
123
+ The restart kind (`restart:`) decides whether that child is restarted at all.
124
+
125
+ | | Exited on its own | Crashed |
126
+ |---|---|---|
127
+ | `:permanent` (default) | restart | restart |
128
+ | `:transient` | leave it stopped | restart |
129
+ | `:temporary` | drop the spec | drop the spec |
130
+
131
+ Once there have been more than `max_restarts` restarts within `max_seconds`, the
132
+ supervisor gives up: it crashes itself so that its parent decides what happens
133
+ next. At the root, `SupervisorRef#join` raises `SupervisorCrashed`, which is your
134
+ cue to log and `exit(1)` and let systemd or Kubernetes restart the process.
135
+
136
+ ## Things to watch out for
137
+
138
+ - **Ractors are experimental.** The first `Ractor.new` prints a warning. Silence it
139
+ with `Warning[:experimental] = false`.
140
+ - **Ractors cannot be killed from outside.** Shutdown is cooperative only. A child
141
+ spinning on the CPU without looking at `ctx`, or blocked inside a C call, cannot
142
+ be stopped; after `shutdown_timeout` it is treated as unresponsive.
143
+ - **Symbols starting with `:"$"` are reserved.** Do not start your own messages with one.
144
+ - **Do not call `value` or `join` on a child.** Once another Ractor has taken a
145
+ Ractor's value, the supervisor can no longer read the exit reason and reports
146
+ `:unknown`. Restarting still works.
147
+ - **Do not call the supervisor synchronously from `initialize`.** The supervisor is
148
+ waiting for this child to finish starting, so the call deadlocks until
149
+ `start_timeout` fires. Look siblings up from `run` instead.
150
+ - **Restarting resets a child's state**, exactly as in Erlang. Anything that must
151
+ survive belongs outside the child.
152
+ - **Delivery is at-most-once.** A message that arrived just before the child died is lost.
153
+ - **`call` is not for hot paths.** Each one builds a port and a timer thread:
154
+ roughly 30k req/s against 1.1M msg/s for `cast` (see [bench/RESULTS.md](bench/RESULTS.md)).
155
+ Use `cast`, or your own `Ractor::Port`, when messages are frequent.
156
+ - **Stopping can take as long as the sum of every child's `shutdown_timeout`**,
157
+ because children are stopped one at a time in reverse order.
158
+
159
+ ### Gems that are not Ractor safe
160
+
161
+ Calling a C extension that is not Ractor safe raises `Ractor::UnsafeError`, and
162
+ referencing something unshareable raises `Ractor::IsolationError`. Either way the
163
+ child keeps crashing until the supervisor escalates. This gem attaches a `hint` to
164
+ the event for the exceptions it recognises, so start from the event log.
165
+
166
+ ```text
167
+ ERROR -- [ractor_shepherd] root child_exited child=:a status=:aborted reason=:error
168
+ error_class="Ractor::IsolationError" hint="the child may be referencing something unshareable, such as a Proc or an IO"
169
+ ```
170
+
171
+ `args:` and `kwargs:` accept only values that survive
172
+ `Ractor.make_shareable(copy: true)`. Procs, Threads and Mutexes do not. Pass plain
173
+ values and hand the behaviour over as a class.
174
+
175
+ ### A note on `Ractor#unmonitor` in Ruby 4.0
176
+
177
+ Ruby 4.0.6's `Ractor#unmonitor(port)` looks a monitor registration up by port id
178
+ alone and ignores which Ractor created the port. Port ids are a per Ractor
179
+ sequence, so when another Ractor is monitoring the same target, unmonitoring drops
180
+ its registration too. Reproduce it with `ruby spike/unmonitor_id_collision.rb`.
181
+
182
+ This gem therefore never calls `unmonitor`. One consequence: when a callee dies
183
+ mid-call, the caller finds out after the timeout rather than immediately.
184
+ **Do not call `unmonitor` on a Ractor this gem manages, either.**
185
+
186
+ ## Stopping on a signal
187
+
188
+ Do not touch ports from inside a trap handler; call `stop` from a thread instead.
189
+
190
+ ```ruby
191
+ Signal.trap("TERM") { Thread.new { sup.stop(:shutdown, timeout: 10) } }
192
+ ```
193
+
194
+ When the main Ractor ends, the process ends and takes every Ractor with it, so
195
+ either use the block form of `RactorShepherd.run` or stop the supervisor from `at_exit`.
196
+
197
+ ## Development
198
+
199
+ ```console
200
+ $ bundle install
201
+ $ bundle exec rake # rubocop + lint:no_loop + spec + rbs validate
202
+ $ bundle exec rake spec:core # the unit tests, which use no Ractors
203
+ $ bundle exec rake spec:stress # the stress tests
204
+ $ bundle exec rake spec:isolated # one file per process, to catch hangs
205
+ ```
206
+
207
+ Ractor compatibility is checked with [audition](https://github.com/ruby/audition).
208
+
209
+ ```console
210
+ $ audition lib
211
+ ```
212
+
213
+ ## License
214
+
215
+ MIT. See [LICENSE.txt](LICENSE.txt).
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RactorShepherd
4
+ # A destination identified by name, returned by `sup.lookup(:jobs, :poller)`.
5
+ #
6
+ # It caches what it resolved, so it is **not shareable**: build one per Ractor
7
+ # that uses it.
8
+ #
9
+ # Delivery is at-most-once. A message that arrived just before the child died
10
+ # is lost.
11
+ class Address
12
+ RETRY_INITIAL_DELAY = 0.01
13
+ RETRY_MAX_DELAY = 0.2
14
+ RESOLVE_POLL_INTERVAL = 0.01
15
+
16
+ attr_reader :path
17
+
18
+ def initialize(supervisor, path, retries: 3, resolve_timeout: 1.0)
19
+ raise InvalidSpec, "lookup needs at least one id" if path.empty?
20
+
21
+ @supervisor = supervisor
22
+ @path = path.freeze
23
+ @retries = retries
24
+ @resolve_timeout = resolve_timeout
25
+ @ractor = nil
26
+ end
27
+
28
+ # The currently cached destination, which may be stale.
29
+ def ractor
30
+ @ractor ||= resolve
31
+ end
32
+
33
+ # Throw the cache away and resolve again.
34
+ def resolve!
35
+ @ractor = nil
36
+ ractor
37
+ end
38
+
39
+ # Send without waiting. If the destination has been replaced, resolve again and retry.
40
+ def cast(message)
41
+ delay = RETRY_INITIAL_DELAY
42
+ attempts = 0
43
+ while true # `loop do` is forbidden here: Ractor::ClosedError < StopIteration
44
+ begin
45
+ return send_to(ractor, message)
46
+ rescue Ractor::ClosedError
47
+ raise ChildUnavailable, "#{label} is gone" if attempts >= @retries
48
+
49
+ attempts += 1
50
+ Kernel.sleep(delay)
51
+ delay = [delay * 2, RETRY_MAX_DELAY].min
52
+ @ractor = nil
53
+ end
54
+ end
55
+ end
56
+
57
+ # A synchronous call, for Server children. It never retries on its own,
58
+ # because the request may not be idempotent.
59
+ def call(message, timeout: 5)
60
+ target = ractor
61
+ raise InvalidOperation, "#{label} is a supervisor; call its SupervisorRef instead" unless target.is_a?(Ractor)
62
+
63
+ Runtime::Call.perform(target, target, message, timeout: timeout, down_error: WorkerDown)
64
+ end
65
+
66
+ private
67
+
68
+ def label = "#{@supervisor.path}/#{@path.join("/")}"
69
+
70
+ def send_to(target, message)
71
+ raise InvalidOperation, "#{label} is a supervisor; send to its SupervisorRef" unless target.is_a?(Ractor)
72
+
73
+ target << message
74
+ :ok
75
+ end
76
+
77
+ # While the child is being restarted there is nothing to resolve to, so keep
78
+ # trying until resolve_timeout runs out.
79
+ def resolve
80
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @resolve_timeout
81
+ while true # `loop do` is forbidden here: Ractor::ClosedError < StopIteration
82
+ found = walk
83
+ return found if found
84
+ raise ChildUnavailable, "#{label} is not running" if
85
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
86
+
87
+ Kernel.sleep(RESOLVE_POLL_INTERVAL)
88
+ end
89
+ end
90
+
91
+ def walk
92
+ current = @supervisor
93
+ @path[0..-2].each do |segment|
94
+ current = current.whereis(segment)
95
+ return nil unless current.is_a?(SupervisorRef)
96
+ end
97
+ current.whereis(@path.last)
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RactorShepherd
4
+ module Core
5
+ # Works out how long to wait before restarting a child.
6
+ #
7
+ # The consecutive failure count resets to 1 when the child had been up for
8
+ # at least `reset_after` seconds before dying.
9
+ #
10
+ # @api private
11
+ class Backoff
12
+ attr_reader :attempt
13
+
14
+ # @param spec [nil, Numeric, BackoffSpec]
15
+ # @param reset_after [Numeric] seconds of uptime after which failures stop counting as consecutive
16
+ def initialize(spec, reset_after:)
17
+ @spec = spec
18
+ @reset_after = reset_after
19
+ @attempt = 0
20
+ @started_at = nil
21
+ end
22
+
23
+ # The child came up.
24
+ def record_start(now)
25
+ @started_at = now
26
+ end
27
+
28
+ # The child went down, or failed to start.
29
+ #
30
+ # @return [Integer] the consecutive failure count
31
+ def record_failure(now)
32
+ @attempt = if @started_at && (now - @started_at) >= @reset_after
33
+ 1
34
+ else
35
+ @attempt + 1
36
+ end
37
+ @started_at = nil
38
+ @attempt
39
+ end
40
+
41
+ # @return [Numeric] seconds to wait before the next start; 0 means right away
42
+ def delay
43
+ return 0 if attempt.zero?
44
+
45
+ case @spec
46
+ when nil then 0
47
+ when Numeric then @spec
48
+ when BackoffSpec then [@spec.max, @spec.initial * (@spec.factor**(attempt - 1))].min
49
+ else raise ArgumentError, "unknown restart_delay: #{@spec.inspect}"
50
+ end
51
+ end
52
+
53
+ def reset
54
+ @attempt = 0
55
+ @started_at = nil
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RactorShepherd
4
+ module Core
5
+ # Builds the event hashes the supervisor and its children publish.
6
+ #
7
+ # Events travel between Ractors, so they are always deeply frozen.
8
+ #
9
+ # @api private
10
+ module Event
11
+ BACKTRACE_LINES = 10
12
+
13
+ HINTS = Ractor.make_shareable({
14
+ "Ractor::UnsafeError" =>
15
+ "the child may be calling a C extension that is not Ractor safe",
16
+ "Ractor::IsolationError" =>
17
+ "the child may be referencing something unshareable, such as a Proc or an IO",
18
+ "Ractor::MovedError" =>
19
+ "the child may be referencing an object that was moved to another Ractor"
20
+ })
21
+
22
+ module_function
23
+
24
+ # @param type [Symbol]
25
+ # @param supervisor [String] the supervisor's path, e.g. "root/jobs"
26
+ # @param at [Float] CLOCK_REALTIME
27
+ # @return [Hash] deeply frozen
28
+ def build(type, supervisor:, at:, **data)
29
+ Ractor.make_shareable({ type: type, supervisor: supervisor, at: at }.merge(data), copy: true)
30
+ end
31
+
32
+ # Event keys describing an exception.
33
+ #
34
+ # @param error [Exception, Symbol, nil]
35
+ # @return [Hash]
36
+ def error_info(error)
37
+ return {} unless error.is_a?(Exception)
38
+
39
+ {
40
+ error_class: error.class.name,
41
+ error_message: error.message.to_s,
42
+ backtrace: (error.backtrace || []).first(BACKTRACE_LINES),
43
+ hint: hint_for(error)
44
+ }.compact
45
+ end
46
+
47
+ # @return [String, nil] advice for exceptions we recognise
48
+ def hint_for(error)
49
+ klass = error.is_a?(Exception) ? error.class : error
50
+ klass.ancestors.each do |ancestor|
51
+ hint = HINTS[ancestor.name]
52
+ return hint if hint
53
+ end
54
+ nil
55
+ end
56
+
57
+ # Normalise an exit reason to :normal, :shutdown, :error or :unknown.
58
+ def reason_kind(reason)
59
+ case reason
60
+ when :normal, :shutdown then reason
61
+ when Exception then :error
62
+ else :unknown
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RactorShepherd
4
+ # Pure logic: no Ractors, no threads, no clock reads. The caller passes the time in.
5
+ module Core
6
+ # Give up once there have been more than `max_restarts` restarts within `max_seconds`.
7
+ #
8
+ # @api private
9
+ class RestartIntensity
10
+ attr_reader :max_restarts, :max_seconds
11
+
12
+ def initialize(max_restarts:, max_seconds:)
13
+ @max_restarts = max_restarts
14
+ @max_seconds = max_seconds
15
+ @times = []
16
+ end
17
+
18
+ # Call this once per failure, even when the strategy restarts several children.
19
+ #
20
+ # @param now [Float] CLOCK_MONOTONIC
21
+ # @return [Symbol] :ok or :exceeded
22
+ def record(now)
23
+ @times.reject! { |t| now - t > max_seconds }
24
+ @times << now
25
+ @times.size > max_restarts ? :exceeded : :ok
26
+ end
27
+
28
+ # @return [Integer] restarts still inside the window
29
+ def count = @times.size
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RactorShepherd
4
+ module Core
5
+ # Decides whether a child should be restarted, from its restart kind and how it exited.
6
+ #
7
+ # Exits the supervisor asked for do not go through this table; the shutdown
8
+ # path handles those.
9
+ #
10
+ # @api private
11
+ module RestartPolicy
12
+ # | restart \ status | :exited | :aborted |
13
+ # |------------------|------------------|----------|
14
+ # | :permanent | :restart | :restart |
15
+ # | :transient | :keep_terminated | :restart |
16
+ # | :temporary | :remove | :remove |
17
+ TABLE = Ractor.make_shareable({
18
+ permanent: { exited: :restart, aborted: :restart },
19
+ transient: { exited: :keep_terminated, aborted: :restart },
20
+ temporary: { exited: :remove, aborted: :remove }
21
+ })
22
+
23
+ # @return [Symbol] :restart, :keep_terminated or :remove
24
+ def self.decide(restart:, status:, dynamic: false)
25
+ by_status = TABLE[restart] or raise ArgumentError, "unknown restart: #{restart.inspect}"
26
+ decision = by_status[status] or raise ArgumentError, "unknown status: #{status.inspect}"
27
+ # A dynamic supervisor keeps no spec around, so "stay terminated" becomes "forget it".
28
+ return :remove if dynamic && decision == :keep_terminated
29
+
30
+ decision
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RactorShepherd
4
+ module Core
5
+ # Turns a restart strategy into the concrete list of children to stop and start.
6
+ #
7
+ # @api private
8
+ module StrategyPlanner
9
+ # The little that the planner needs to know about a child.
10
+ ChildView = Data.define(:id, :restart, :alive)
11
+
12
+ # `terminate` is in stop order (reverse of start order), `start` is in start order.
13
+ Plan = Data.define(:terminate, :start, :remove)
14
+
15
+ # @param children [Array<ChildView>] in start order
16
+ # @param failed_id [Object] the id of the child that went down
17
+ # @param strategy [Symbol] :one_for_one, :one_for_all or :rest_for_one
18
+ # @return [Plan]
19
+ def self.plan(children:, failed_id:, strategy:)
20
+ index = children.index { |c| c.id == failed_id }
21
+ raise ArgumentError, "unknown child: #{failed_id.inspect}" if index.nil?
22
+
23
+ case strategy
24
+ when :one_for_one then Plan.new(terminate: [], start: [failed_id], remove: [])
25
+ when :one_for_all then for_group(children, children, failed_id)
26
+ when :rest_for_one then for_group(children, children[index..], failed_id)
27
+ else raise ArgumentError, "unknown strategy: #{strategy.inspect}"
28
+ end
29
+ end
30
+
31
+ # @param scope [Array<ChildView>] the children the strategy reaches, in start order
32
+ def self.for_group(_children, scope, failed_id)
33
+ affected = scope.reject { |c| c.id == failed_id }
34
+ terminate = affected.select(&:alive)
35
+ Plan.new(
36
+ terminate: terminate.map(&:id).reverse,
37
+ start: scope.reject { |c| c.restart == :temporary }.map(&:id),
38
+ remove: terminate.select { |c| c.restart == :temporary }.map(&:id)
39
+ )
40
+ end
41
+ private_class_method :for_group
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RactorShepherd
4
+ # Base class for every error this gem raises.
5
+ #
6
+ # Exceptions are copied across Ractor boundaries, so keep their instance
7
+ # variables to simple values: symbols, numbers, frozen strings.
8
+ class Error < StandardError; end
9
+
10
+ # A ChildSpec or SupervisorSpec is invalid.
11
+ class InvalidSpec < Error; end
12
+
13
+ # Loaded on a Ruby that does not meet the requirements.
14
+ class UnsupportedRuby < Error; end
15
+
16
+ # The initial boot failed. The child's exception is the cause.
17
+ class StartError < Error; end
18
+
19
+ # Raised by #join when the supervisor had escalated instead of stopping.
20
+ class SupervisorCrashed < Error; end
21
+
22
+ # Restart intensity exceeded. Raised inside a supervisor and propagated to its parent.
23
+ class MaxRestartsExceeded < Error; end
24
+
25
+ # A child did not acknowledge a shutdown request, or did not finish starting, in time.
26
+ class ChildUnresponsive < Error; end
27
+
28
+ # A synchronous call timed out.
29
+ class CallTimeout < Error; end
30
+
31
+ # The supervisor being called is not running.
32
+ class SupervisorDown < Error; end
33
+
34
+ # The worker being called is not running.
35
+ class WorkerDown < Error; end
36
+
37
+ # No child with that id.
38
+ class ChildNotFound < Error; end
39
+
40
+ # The child exists but has no running Ractor right now (it is being restarted).
41
+ class ChildUnavailable < Error; end
42
+
43
+ # The operation does not apply in the current state.
44
+ class InvalidOperation < Error; end
45
+
46
+ # A dynamic supervisor is already at max_children.
47
+ class MaxChildrenReached < Error; end
48
+
49
+ # An unexpected message shape arrived. Signals a bug.
50
+ class ProtocolError < Error; end
51
+
52
+ # Signals that a shutdown was requested.
53
+ #
54
+ # It inherits Exception rather than StandardError so that a plain
55
+ # `rescue => e` in user code cannot swallow it.
56
+ class ShutdownSignal < Exception; end # rubocop:disable Lint/InheritException
57
+ end