ractor-pool 0.2.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +6 -0
- data/lib/ractor-pool/version.rb +1 -1
- data/lib/ractor-pool.rb +58 -25
- data/sig/generated/ractor-pool.rbs +22 -4
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b97336e3bda02791d66cbef83761b58ff599afca2e58698bd79e7aa5bdedc497
|
|
4
|
+
data.tar.gz: 77b0ff65e53978436edc7701c204415709092f96a71f1c834e7ce719c75fc3d4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: acdd99e1c5c20f0d628353abe9682a9283c1d60d5b5f1247d366c08e91db3dc19e62797a8d3347fd49c8498e1896e57711637a0cea3c05359f4aa300f3f73eee
|
|
7
|
+
data.tar.gz: 5cf2b4e221ec273c5ed038c256d36e4b29f58619eee6d7412e68d498288e593373a874118b6b4f58e83c8a20009c800e3000d5aa31dc88cc7b6fc5ed277c33ed
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-05-08
|
|
4
|
+
|
|
5
|
+
- Replace state atom with separate `@in_flight` and `@shutdown` atoms
|
|
6
|
+
- Add `on_error:` worker error callback
|
|
7
|
+
- Update `Ractor` warning suppression regex
|
|
8
|
+
|
|
3
9
|
## [0.2.0] - 2026-01-07
|
|
4
10
|
|
|
5
11
|
- Require Ruby >= 4.0.0
|
data/lib/ractor-pool/version.rb
CHANGED
data/lib/ractor-pool.rb
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
require "warning"
|
|
5
5
|
require "atomic-ruby/atom"
|
|
6
6
|
|
|
7
|
-
Warning.ignore(/Ractor is experimental/, __FILE__)
|
|
7
|
+
Warning.ignore(/Ractor API is experimental/, __FILE__)
|
|
8
8
|
|
|
9
9
|
# A thread-safe, lock-free pool of Ractor workers with a coordinator pattern for distributing work.
|
|
10
10
|
#
|
|
@@ -54,11 +54,15 @@ class RactorPool
|
|
|
54
54
|
# @rbs @size: Integer
|
|
55
55
|
# @rbs @worker: ^(untyped) -> untyped
|
|
56
56
|
# @rbs @name: String?
|
|
57
|
+
# @rbs @on_error: (^(Exception) -> void | nil)
|
|
57
58
|
# @rbs @result_handler: (^(untyped) -> void | nil)
|
|
58
|
-
# @rbs @
|
|
59
|
+
# @rbs @in_flight: Atom[Integer]
|
|
60
|
+
# @rbs @shutdown: Atom[bool]
|
|
59
61
|
# @rbs @result_port: Ractor::Port?
|
|
62
|
+
# @rbs @error_port: Ractor::Port?
|
|
60
63
|
# @rbs @coordinator: Ractor?
|
|
61
64
|
# @rbs @workers: Array[Ractor]
|
|
65
|
+
# @rbs @error_collector: Thread?
|
|
62
66
|
# @rbs @collector: Thread?
|
|
63
67
|
|
|
64
68
|
# Creates a new RactorPool with the specified number of workers.
|
|
@@ -66,10 +70,12 @@ class RactorPool
|
|
|
66
70
|
# @param size [Integer] number of worker ractors to create
|
|
67
71
|
# @param worker [Proc] a shareable proc that processes each work item
|
|
68
72
|
# @param name [String, nil] optional name for the pool, used in thread/ractor names
|
|
73
|
+
# @param on_error [Proc, nil] optional shareable proc called with the raised exception when a worker raises
|
|
69
74
|
# @yieldparam result [Object] the result returned by the worker proc
|
|
70
75
|
# @return [void]
|
|
71
76
|
# @raise [ArgumentError] if size is not a positive integer
|
|
72
77
|
# @raise [ArgumentError] if worker is not a proc
|
|
78
|
+
# @raise [ArgumentError] if on_error is given but is not a proc
|
|
73
79
|
#
|
|
74
80
|
# @example With result handler
|
|
75
81
|
# pool = RactorPool.new(size: 4, worker: proc { it }) { |result| puts result }
|
|
@@ -77,21 +83,31 @@ class RactorPool
|
|
|
77
83
|
# @example Without result handler
|
|
78
84
|
# pool = RactorPool.new(size: 4, worker: proc { it })
|
|
79
85
|
#
|
|
80
|
-
# @
|
|
81
|
-
|
|
86
|
+
# @example With error handler
|
|
87
|
+
# error_count = Atom.new(0)
|
|
88
|
+
# on_error = proc { error_count.swap { |count| count + 1 } }
|
|
89
|
+
# pool = RactorPool.new(size: 4, worker: proc { raise }, on_error: on_error)
|
|
90
|
+
#
|
|
91
|
+
# @rbs (?size: Integer, worker: ^(untyped) -> untyped, ?name: String?, ?on_error: (^(Exception) -> void | nil)) ?{ (untyped) -> void } -> void
|
|
92
|
+
def initialize(size: Etc.nprocessors, worker:, name: nil, on_error: nil, &result_handler)
|
|
82
93
|
raise ArgumentError, "size must be a positive Integer" unless size.is_a?(Integer) && size > 0
|
|
83
94
|
raise ArgumentError, "worker must be a Proc" unless worker.is_a?(Proc)
|
|
95
|
+
raise ArgumentError, "on_error must be a Proc" if on_error && !on_error.is_a?(Proc)
|
|
84
96
|
|
|
85
97
|
@size = size
|
|
86
98
|
@worker = Ractor.shareable_proc(&worker)
|
|
87
99
|
@name = name
|
|
100
|
+
@on_error = Ractor.shareable_proc(&on_error) if on_error
|
|
88
101
|
@result_handler = result_handler
|
|
89
102
|
|
|
90
|
-
@
|
|
103
|
+
@in_flight = Atom.new(0)
|
|
104
|
+
@shutdown = Atom.new(false)
|
|
91
105
|
|
|
92
106
|
@result_port = Ractor::Port.new if result_handler
|
|
107
|
+
@error_port = Ractor::Port.new unless on_error
|
|
93
108
|
@coordinator = start_coordinator if size > 1
|
|
94
109
|
@workers = start_workers
|
|
110
|
+
@error_collector = start_error_collector
|
|
95
111
|
@collector = start_collector
|
|
96
112
|
end
|
|
97
113
|
|
|
@@ -107,21 +123,19 @@ class RactorPool
|
|
|
107
123
|
#
|
|
108
124
|
# @rbs (untyped work) -> void
|
|
109
125
|
def <<(work)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
126
|
+
raise EnqueuedWorkAfterShutdownError if @shutdown.value
|
|
127
|
+
|
|
128
|
+
@in_flight.swap { |count| count + 1 }
|
|
129
|
+
|
|
130
|
+
if @shutdown.value
|
|
131
|
+
@in_flight.swap { |count| count - 1 }
|
|
132
|
+
raise EnqueuedWorkAfterShutdownError
|
|
116
133
|
end
|
|
117
|
-
raise EnqueuedWorkAfterShutdownError if state[:shutdown]
|
|
118
134
|
|
|
119
135
|
begin
|
|
120
136
|
(@coordinator || @workers.first).send(work, move: true)
|
|
121
137
|
ensure
|
|
122
|
-
@
|
|
123
|
-
current_state.merge(in_flight: current_state[:in_flight] - 1)
|
|
124
|
-
end
|
|
138
|
+
@in_flight.swap { |count| count - 1 }
|
|
125
139
|
end
|
|
126
140
|
end
|
|
127
141
|
|
|
@@ -132,7 +146,7 @@ class RactorPool
|
|
|
132
146
|
# 2. Waits for all in-flight work submissions to complete
|
|
133
147
|
# 3. Allows all queued work to complete
|
|
134
148
|
# 4. Waits for all workers to finish
|
|
135
|
-
# 5. Waits for all results to be processed
|
|
149
|
+
# 5. Waits for all results and errors to be processed
|
|
136
150
|
#
|
|
137
151
|
# This method is idempotent and can be called multiple times safely.
|
|
138
152
|
#
|
|
@@ -144,22 +158,24 @@ class RactorPool
|
|
|
144
158
|
# @rbs () -> void
|
|
145
159
|
def shutdown
|
|
146
160
|
already_shutdown = false
|
|
147
|
-
@
|
|
148
|
-
if
|
|
161
|
+
@shutdown.swap do |current|
|
|
162
|
+
if current
|
|
149
163
|
already_shutdown = true
|
|
150
|
-
|
|
164
|
+
current
|
|
151
165
|
else
|
|
152
|
-
|
|
166
|
+
true
|
|
153
167
|
end
|
|
154
168
|
end
|
|
155
169
|
return if already_shutdown
|
|
156
170
|
|
|
157
|
-
Thread.pass until @
|
|
171
|
+
Thread.pass until @in_flight.value.zero?
|
|
158
172
|
|
|
159
173
|
@coordinator&.send(SHUTDOWN, move: true) ||
|
|
160
174
|
(@workers.first.send(SHUTDOWN, move: true) && @result_port&.send(SHUTDOWN, move: true))
|
|
161
175
|
@workers.each(&:join)
|
|
162
176
|
@coordinator&.join
|
|
177
|
+
@error_port&.send(SHUTDOWN, move: true)
|
|
178
|
+
@error_collector&.join
|
|
163
179
|
@collector&.join
|
|
164
180
|
end
|
|
165
181
|
|
|
@@ -225,7 +241,7 @@ class RactorPool
|
|
|
225
241
|
ractor_name = String.new("#{self.class.name} ractor #{index}")
|
|
226
242
|
ractor_name << " for #{@name}" if @name
|
|
227
243
|
|
|
228
|
-
Ractor.new(@worker, @coordinator, @result_port, name: ractor_name) do |worker, coordinator, result_port|
|
|
244
|
+
Ractor.new(@worker, @on_error, @error_port, @coordinator, @result_port, name: ractor_name) do |worker, on_error, error_port, coordinator, result_port|
|
|
229
245
|
loop do
|
|
230
246
|
coordinator&.send(Ractor.current, move: true)
|
|
231
247
|
|
|
@@ -237,15 +253,32 @@ class RactorPool
|
|
|
237
253
|
|
|
238
254
|
result_port&.send(result, move: true)
|
|
239
255
|
rescue => error
|
|
240
|
-
|
|
241
|
-
puts "#{error.class}: #{error.message}"
|
|
242
|
-
puts error.backtrace.join("\n")
|
|
256
|
+
on_error ? on_error.call(error) : error_port.send(error.full_message, move: true)
|
|
243
257
|
end
|
|
244
258
|
end
|
|
245
259
|
end
|
|
246
260
|
end
|
|
247
261
|
end
|
|
248
262
|
|
|
263
|
+
# @rbs () -> Thread?
|
|
264
|
+
def start_error_collector
|
|
265
|
+
return if @on_error
|
|
266
|
+
|
|
267
|
+
thread_name = String.new("#{self.class.name} error collector thread")
|
|
268
|
+
thread_name << " for #{@name}" if @name
|
|
269
|
+
|
|
270
|
+
Thread.new(@error_port, thread_name) do |error_port, name|
|
|
271
|
+
Thread.current.name = name
|
|
272
|
+
|
|
273
|
+
loop do
|
|
274
|
+
message = error_port.receive
|
|
275
|
+
break if message == SHUTDOWN
|
|
276
|
+
|
|
277
|
+
warn message
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
249
282
|
# @rbs () -> Thread?
|
|
250
283
|
def start_collector
|
|
251
284
|
return unless @result_handler
|
|
@@ -46,16 +46,24 @@ class RactorPool
|
|
|
46
46
|
|
|
47
47
|
@collector: Thread?
|
|
48
48
|
|
|
49
|
+
@error_collector: Thread?
|
|
50
|
+
|
|
49
51
|
@workers: Array[Ractor]
|
|
50
52
|
|
|
51
53
|
@coordinator: Ractor?
|
|
52
54
|
|
|
55
|
+
@error_port: Ractor::Port?
|
|
56
|
+
|
|
53
57
|
@result_port: Ractor::Port?
|
|
54
58
|
|
|
55
|
-
@
|
|
59
|
+
@shutdown: Atom[bool]
|
|
60
|
+
|
|
61
|
+
@in_flight: Atom[Integer]
|
|
56
62
|
|
|
57
63
|
@result_handler: ^(untyped) -> void | nil
|
|
58
64
|
|
|
65
|
+
@on_error: ^(Exception) -> void | nil
|
|
66
|
+
|
|
59
67
|
@name: String?
|
|
60
68
|
|
|
61
69
|
@worker: ^(untyped) -> untyped
|
|
@@ -67,10 +75,12 @@ class RactorPool
|
|
|
67
75
|
# @param size [Integer] number of worker ractors to create
|
|
68
76
|
# @param worker [Proc] a shareable proc that processes each work item
|
|
69
77
|
# @param name [String, nil] optional name for the pool, used in thread/ractor names
|
|
78
|
+
# @param on_error [Proc, nil] optional shareable proc called with the raised exception when a worker raises
|
|
70
79
|
# @yieldparam result [Object] the result returned by the worker proc
|
|
71
80
|
# @return [void]
|
|
72
81
|
# @raise [ArgumentError] if size is not a positive integer
|
|
73
82
|
# @raise [ArgumentError] if worker is not a proc
|
|
83
|
+
# @raise [ArgumentError] if on_error is given but is not a proc
|
|
74
84
|
#
|
|
75
85
|
# @example With result handler
|
|
76
86
|
# pool = RactorPool.new(size: 4, worker: proc { it }) { |result| puts result }
|
|
@@ -78,8 +88,13 @@ class RactorPool
|
|
|
78
88
|
# @example Without result handler
|
|
79
89
|
# pool = RactorPool.new(size: 4, worker: proc { it })
|
|
80
90
|
#
|
|
81
|
-
# @
|
|
82
|
-
|
|
91
|
+
# @example With error handler
|
|
92
|
+
# error_count = Atom.new(0)
|
|
93
|
+
# on_error = proc { error_count.swap { |count| count + 1 } }
|
|
94
|
+
# pool = RactorPool.new(size: 4, worker: proc { raise }, on_error: on_error)
|
|
95
|
+
#
|
|
96
|
+
# @rbs (?size: Integer, worker: ^(untyped) -> untyped, ?name: String?, ?on_error: (^(Exception) -> void | nil)) ?{ (untyped) -> void } -> void
|
|
97
|
+
def initialize: (worker: ^(untyped) -> untyped, ?size: Integer, ?name: String?, ?on_error: ^(Exception) -> void | nil) ?{ (untyped) -> void } -> void
|
|
83
98
|
|
|
84
99
|
# Queues a work item to be processed by an available worker.
|
|
85
100
|
#
|
|
@@ -101,7 +116,7 @@ class RactorPool
|
|
|
101
116
|
# 2. Waits for all in-flight work submissions to complete
|
|
102
117
|
# 3. Allows all queued work to complete
|
|
103
118
|
# 4. Waits for all workers to finish
|
|
104
|
-
# 5. Waits for all results to be processed
|
|
119
|
+
# 5. Waits for all results and errors to be processed
|
|
105
120
|
#
|
|
106
121
|
# This method is idempotent and can be called multiple times safely.
|
|
107
122
|
#
|
|
@@ -121,6 +136,9 @@ class RactorPool
|
|
|
121
136
|
# @rbs () -> Array[Ractor]
|
|
122
137
|
def start_workers: () -> Array[Ractor]
|
|
123
138
|
|
|
139
|
+
# @rbs () -> Thread?
|
|
140
|
+
def start_error_collector: () -> Thread?
|
|
141
|
+
|
|
124
142
|
# @rbs () -> Thread?
|
|
125
143
|
def start_collector: () -> Thread?
|
|
126
144
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ractor-pool
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joshua Young
|
|
@@ -73,7 +73,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
74
|
version: '0'
|
|
75
75
|
requirements: []
|
|
76
|
-
rubygems_version: 4.0.
|
|
76
|
+
rubygems_version: 4.0.6
|
|
77
77
|
specification_version: 4
|
|
78
78
|
summary: A thread-safe, lock-free pool of Ractor workers with a coordinator pattern
|
|
79
79
|
for distributing work
|