ractor-pool 0.3.0 → 0.4.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/.buildkite/pipeline.yml +1 -0
- data/CHANGELOG.md +8 -0
- data/README.md +1 -1
- data/lib/ractor-pool/version.rb +1 -1
- data/lib/ractor-pool.rb +59 -28
- data/sig/generated/ractor-pool.rbs +26 -10
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2d8741738f0de76db49cbbbaccc701b687af3df13ce9802205c7661a10519126
|
|
4
|
+
data.tar.gz: 337ce4d26b7735ec2517c2b88e76f2b11bef87124e3581a74772e68fbd3b6e57
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 663d3b28aeab7e0341bbfd5d78540c670fbb74e3cfc89679572093067fc72396028d3b1637c224676d4c52d86aaafd688143e28499217f0b513c7d5035ba42c4
|
|
7
|
+
data.tar.gz: 6a9fc3ec933ece355f9bba48ac1de2980a3c105f0baaf2a13684ab74a54028daec5f2695dee25c4d970d3dc71f7bd066d207977424f8f199c44f8f61e9e6ae85
|
data/.buildkite/pipeline.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-05-20
|
|
4
|
+
|
|
5
|
+
- Add `:round_robin` dispatch strategy
|
|
6
|
+
|
|
7
|
+
## [0.3.1] - 2026-05-12
|
|
8
|
+
|
|
9
|
+
- Fix `result_port` race condition in single-worker shutdown
|
|
10
|
+
|
|
3
11
|
## [0.3.0] - 2026-05-08
|
|
4
12
|
|
|
5
13
|
- Replace state atom with separate `@in_flight` and `@shutdown` atoms
|
data/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|

|
|
4
4
|

|
|
5
5
|
|
|
6
|
-
A thread-safe, lock-free pool of Ractor workers with
|
|
6
|
+
A thread-safe, lock-free pool of Ractor workers with coordinator or round-robin dispatch for distributing work.
|
|
7
7
|
|
|
8
8
|
## Installation
|
|
9
9
|
|
data/lib/ractor-pool/version.rb
CHANGED
data/lib/ractor-pool.rb
CHANGED
|
@@ -6,11 +6,16 @@ require "atomic-ruby/atom"
|
|
|
6
6
|
|
|
7
7
|
Warning.ignore(/Ractor API is experimental/, __FILE__)
|
|
8
8
|
|
|
9
|
-
# A thread-safe, lock-free pool of Ractor workers with
|
|
9
|
+
# A thread-safe, lock-free pool of Ractor workers with coordinator or round-robin dispatch for distributing work.
|
|
10
10
|
#
|
|
11
11
|
# RactorPool manages a fixed number of worker ractors that process work items in parallel.
|
|
12
|
-
#
|
|
13
|
-
#
|
|
12
|
+
# The +:coordinator+ strategy (the default) routes each work item to whichever worker is
|
|
13
|
+
# currently idle via a dedicated coordinator Ractor, so a slow item on one worker does not
|
|
14
|
+
# block faster items from being picked up by other workers. Use it when work items have
|
|
15
|
+
# variable cost. The +:round_robin+ strategy dispatches work to workers in turn, so a slow
|
|
16
|
+
# item on one worker queues the next item destined for that worker behind it, even if other
|
|
17
|
+
# workers are idle. Use it when work items have uniform cost. Results are collected and
|
|
18
|
+
# passed to a result handler running in a separate thread.
|
|
14
19
|
#
|
|
15
20
|
# @example Basic usage
|
|
16
21
|
# results = []
|
|
@@ -35,7 +40,7 @@ Warning.ignore(/Ractor API is experimental/, __FILE__)
|
|
|
35
40
|
#
|
|
36
41
|
# p counter.value #=> 10
|
|
37
42
|
#
|
|
38
|
-
# @see https://docs.ruby-lang.org/en/master/ractor_md.html Ractor Guide
|
|
43
|
+
# @see https://docs.ruby-lang.org/en/master/language/ractor_md.html Ractor Guide
|
|
39
44
|
# @see https://docs.ruby-lang.org/en/master/Ractor.html Ractor API
|
|
40
45
|
# @see https://docs.ruby-lang.org/en/master/Ractor/Port.html Ractor::Port API
|
|
41
46
|
# @see https://github.com/joshuay03/atomic-ruby atomic-ruby gem
|
|
@@ -48,33 +53,40 @@ class RactorPool
|
|
|
48
53
|
def message = "cannot queue work after shutdown"
|
|
49
54
|
end
|
|
50
55
|
|
|
56
|
+
STRATEGIES = %i[coordinator round_robin].freeze
|
|
57
|
+
private_constant :STRATEGIES
|
|
58
|
+
|
|
51
59
|
SHUTDOWN = :shutdown
|
|
52
60
|
private_constant :SHUTDOWN
|
|
53
61
|
|
|
54
62
|
# @rbs @size: Integer
|
|
55
63
|
# @rbs @worker: ^(untyped) -> untyped
|
|
64
|
+
# @rbs @strategy: Symbol
|
|
56
65
|
# @rbs @name: String?
|
|
57
66
|
# @rbs @on_error: (^(Exception) -> void | nil)
|
|
58
67
|
# @rbs @result_handler: (^(untyped) -> void | nil)
|
|
59
68
|
# @rbs @in_flight: Atom[Integer]
|
|
60
69
|
# @rbs @shutdown: Atom[bool]
|
|
70
|
+
# @rbs @next_worker_index: Atom[Integer]?
|
|
61
71
|
# @rbs @result_port: Ractor::Port?
|
|
62
72
|
# @rbs @error_port: Ractor::Port?
|
|
63
73
|
# @rbs @coordinator: Ractor?
|
|
64
74
|
# @rbs @workers: Array[Ractor]
|
|
65
|
-
# @rbs @error_collector: Thread?
|
|
66
75
|
# @rbs @collector: Thread?
|
|
76
|
+
# @rbs @error_collector: Thread?
|
|
67
77
|
|
|
68
78
|
# Creates a new RactorPool with the specified number of workers.
|
|
69
79
|
#
|
|
70
80
|
# @param size [Integer] number of worker ractors to create
|
|
71
81
|
# @param worker [Proc] a shareable proc that processes each work item
|
|
82
|
+
# @param strategy [Symbol] dispatch strategy, either +:coordinator+ (the default) or +:round_robin+
|
|
72
83
|
# @param name [String, nil] optional name for the pool, used in thread/ractor names
|
|
73
84
|
# @param on_error [Proc, nil] optional shareable proc called with the raised exception when a worker raises
|
|
74
85
|
# @yieldparam result [Object] the result returned by the worker proc
|
|
75
86
|
# @return [void]
|
|
76
87
|
# @raise [ArgumentError] if size is not a positive integer
|
|
77
88
|
# @raise [ArgumentError] if worker is not a proc
|
|
89
|
+
# @raise [ArgumentError] if strategy is not +:coordinator+ or +:round_robin+
|
|
78
90
|
# @raise [ArgumentError] if on_error is given but is not a proc
|
|
79
91
|
#
|
|
80
92
|
# @example With result handler
|
|
@@ -83,32 +95,38 @@ class RactorPool
|
|
|
83
95
|
# @example Without result handler
|
|
84
96
|
# pool = RactorPool.new(size: 4, worker: proc { it })
|
|
85
97
|
#
|
|
98
|
+
# @example With round-robin strategy
|
|
99
|
+
# pool = RactorPool.new(size: 4, strategy: :round_robin, worker: proc { it })
|
|
100
|
+
#
|
|
86
101
|
# @example With error handler
|
|
87
102
|
# error_count = Atom.new(0)
|
|
88
103
|
# on_error = proc { error_count.swap { |count| count + 1 } }
|
|
89
104
|
# pool = RactorPool.new(size: 4, worker: proc { raise }, on_error: on_error)
|
|
90
105
|
#
|
|
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)
|
|
106
|
+
# @rbs (?size: Integer, worker: ^(untyped) -> untyped, ?strategy: Symbol, ?name: String?, ?on_error: (^(Exception) -> void | nil)) ?{ (untyped) -> void } -> void
|
|
107
|
+
def initialize(size: Etc.nprocessors, worker:, strategy: :coordinator, name: nil, on_error: nil, &result_handler)
|
|
93
108
|
raise ArgumentError, "size must be a positive Integer" unless size.is_a?(Integer) && size > 0
|
|
94
109
|
raise ArgumentError, "worker must be a Proc" unless worker.is_a?(Proc)
|
|
110
|
+
raise ArgumentError, "strategy must be one of #{STRATEGIES.inspect}" unless STRATEGIES.include?(strategy)
|
|
95
111
|
raise ArgumentError, "on_error must be a Proc" if on_error && !on_error.is_a?(Proc)
|
|
96
112
|
|
|
97
113
|
@size = size
|
|
98
114
|
@worker = Ractor.shareable_proc(&worker)
|
|
115
|
+
@strategy = strategy
|
|
99
116
|
@name = name
|
|
100
117
|
@on_error = Ractor.shareable_proc(&on_error) if on_error
|
|
101
118
|
@result_handler = result_handler
|
|
102
119
|
|
|
103
120
|
@in_flight = Atom.new(0)
|
|
104
121
|
@shutdown = Atom.new(false)
|
|
122
|
+
@next_worker_index = Atom.new(-1) if size > 1 && strategy == :round_robin
|
|
105
123
|
|
|
106
124
|
@result_port = Ractor::Port.new if result_handler
|
|
107
125
|
@error_port = Ractor::Port.new unless on_error
|
|
108
|
-
@coordinator = start_coordinator if size > 1
|
|
126
|
+
@coordinator = start_coordinator if size > 1 && strategy == :coordinator
|
|
109
127
|
@workers = start_workers
|
|
110
|
-
@error_collector = start_error_collector
|
|
111
128
|
@collector = start_collector
|
|
129
|
+
@error_collector = start_error_collector
|
|
112
130
|
end
|
|
113
131
|
|
|
114
132
|
# Queues a work item to be processed by an available worker.
|
|
@@ -132,8 +150,16 @@ class RactorPool
|
|
|
132
150
|
raise EnqueuedWorkAfterShutdownError
|
|
133
151
|
end
|
|
134
152
|
|
|
153
|
+
target = if @coordinator
|
|
154
|
+
@coordinator
|
|
155
|
+
elsif @next_worker_index
|
|
156
|
+
@workers[@next_worker_index.swap { |index| (index + 1) % @size }]
|
|
157
|
+
else
|
|
158
|
+
@workers.first
|
|
159
|
+
end
|
|
160
|
+
|
|
135
161
|
begin
|
|
136
|
-
|
|
162
|
+
target.send(work, move: true)
|
|
137
163
|
ensure
|
|
138
164
|
@in_flight.swap { |count| count - 1 }
|
|
139
165
|
end
|
|
@@ -170,10 +196,15 @@ class RactorPool
|
|
|
170
196
|
|
|
171
197
|
Thread.pass until @in_flight.value.zero?
|
|
172
198
|
|
|
173
|
-
@coordinator
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
199
|
+
if @coordinator
|
|
200
|
+
@coordinator.send(SHUTDOWN, move: true)
|
|
201
|
+
@workers.each(&:join)
|
|
202
|
+
@coordinator.join
|
|
203
|
+
else
|
|
204
|
+
@workers.each { |worker| worker.send(SHUTDOWN, move: true) }
|
|
205
|
+
@workers.each(&:join)
|
|
206
|
+
@result_port&.send(SHUTDOWN, move: true)
|
|
207
|
+
end
|
|
177
208
|
@error_port&.send(SHUTDOWN, move: true)
|
|
178
209
|
@error_collector&.join
|
|
179
210
|
@collector&.join
|
|
@@ -261,39 +292,39 @@ class RactorPool
|
|
|
261
292
|
end
|
|
262
293
|
|
|
263
294
|
# @rbs () -> Thread?
|
|
264
|
-
def
|
|
265
|
-
return
|
|
295
|
+
def start_collector
|
|
296
|
+
return unless @result_handler
|
|
266
297
|
|
|
267
|
-
thread_name = String.new("#{self.class.name}
|
|
298
|
+
thread_name = String.new("#{self.class.name} collector thread")
|
|
268
299
|
thread_name << " for #{@name}" if @name
|
|
269
300
|
|
|
270
|
-
Thread.new(@
|
|
301
|
+
Thread.new(@result_port, @result_handler, thread_name) do |result_port, result_handler, name|
|
|
271
302
|
Thread.current.name = name
|
|
272
303
|
|
|
273
304
|
loop do
|
|
274
|
-
|
|
275
|
-
break if
|
|
305
|
+
result = result_port.receive
|
|
306
|
+
break if result == SHUTDOWN
|
|
276
307
|
|
|
277
|
-
|
|
308
|
+
result_handler.call(result)
|
|
278
309
|
end
|
|
279
310
|
end
|
|
280
311
|
end
|
|
281
312
|
|
|
282
313
|
# @rbs () -> Thread?
|
|
283
|
-
def
|
|
284
|
-
return
|
|
314
|
+
def start_error_collector
|
|
315
|
+
return if @on_error
|
|
285
316
|
|
|
286
|
-
thread_name = String.new("#{self.class.name} collector thread")
|
|
317
|
+
thread_name = String.new("#{self.class.name} error collector thread")
|
|
287
318
|
thread_name << " for #{@name}" if @name
|
|
288
319
|
|
|
289
|
-
Thread.new(@
|
|
320
|
+
Thread.new(@error_port, thread_name) do |error_port, name|
|
|
290
321
|
Thread.current.name = name
|
|
291
322
|
|
|
292
323
|
loop do
|
|
293
|
-
|
|
294
|
-
break if
|
|
324
|
+
message = error_port.receive
|
|
325
|
+
break if message == SHUTDOWN
|
|
295
326
|
|
|
296
|
-
|
|
327
|
+
warn message
|
|
297
328
|
end
|
|
298
329
|
end
|
|
299
330
|
end
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
# Generated from lib/ractor-pool.rb with RBS::Inline
|
|
2
2
|
|
|
3
|
-
# A thread-safe, lock-free pool of Ractor workers with
|
|
3
|
+
# A thread-safe, lock-free pool of Ractor workers with coordinator or round-robin dispatch for distributing work.
|
|
4
4
|
#
|
|
5
5
|
# RactorPool manages a fixed number of worker ractors that process work items in parallel.
|
|
6
|
-
#
|
|
7
|
-
#
|
|
6
|
+
# The +:coordinator+ strategy (the default) routes each work item to whichever worker is
|
|
7
|
+
# currently idle via a dedicated coordinator Ractor, so a slow item on one worker does not
|
|
8
|
+
# block faster items from being picked up by other workers. Use it when work items have
|
|
9
|
+
# variable cost. The +:round_robin+ strategy dispatches work to workers in turn, so a slow
|
|
10
|
+
# item on one worker queues the next item destined for that worker behind it, even if other
|
|
11
|
+
# workers are idle. Use it when work items have uniform cost. Results are collected and
|
|
12
|
+
# passed to a result handler running in a separate thread.
|
|
8
13
|
#
|
|
9
14
|
# @example Basic usage
|
|
10
15
|
# results = []
|
|
@@ -29,7 +34,7 @@
|
|
|
29
34
|
#
|
|
30
35
|
# p counter.value #=> 10
|
|
31
36
|
#
|
|
32
|
-
# @see https://docs.ruby-lang.org/en/master/ractor_md.html Ractor Guide
|
|
37
|
+
# @see https://docs.ruby-lang.org/en/master/language/ractor_md.html Ractor Guide
|
|
33
38
|
# @see https://docs.ruby-lang.org/en/master/Ractor.html Ractor API
|
|
34
39
|
# @see https://docs.ruby-lang.org/en/master/Ractor/Port.html Ractor::Port API
|
|
35
40
|
# @see https://github.com/joshuay03/atomic-ruby atomic-ruby gem
|
|
@@ -42,12 +47,14 @@ class RactorPool
|
|
|
42
47
|
def message: () -> String
|
|
43
48
|
end
|
|
44
49
|
|
|
45
|
-
|
|
50
|
+
STRATEGIES: untyped
|
|
46
51
|
|
|
47
|
-
|
|
52
|
+
SHUTDOWN: ::Symbol
|
|
48
53
|
|
|
49
54
|
@error_collector: Thread?
|
|
50
55
|
|
|
56
|
+
@collector: Thread?
|
|
57
|
+
|
|
51
58
|
@workers: Array[Ractor]
|
|
52
59
|
|
|
53
60
|
@coordinator: Ractor?
|
|
@@ -56,6 +63,8 @@ class RactorPool
|
|
|
56
63
|
|
|
57
64
|
@result_port: Ractor::Port?
|
|
58
65
|
|
|
66
|
+
@next_worker_index: Atom[Integer]?
|
|
67
|
+
|
|
59
68
|
@shutdown: Atom[bool]
|
|
60
69
|
|
|
61
70
|
@in_flight: Atom[Integer]
|
|
@@ -66,6 +75,8 @@ class RactorPool
|
|
|
66
75
|
|
|
67
76
|
@name: String?
|
|
68
77
|
|
|
78
|
+
@strategy: Symbol
|
|
79
|
+
|
|
69
80
|
@worker: ^(untyped) -> untyped
|
|
70
81
|
|
|
71
82
|
@size: Integer
|
|
@@ -74,12 +85,14 @@ class RactorPool
|
|
|
74
85
|
#
|
|
75
86
|
# @param size [Integer] number of worker ractors to create
|
|
76
87
|
# @param worker [Proc] a shareable proc that processes each work item
|
|
88
|
+
# @param strategy [Symbol] dispatch strategy, either +:coordinator+ (the default) or +:round_robin+
|
|
77
89
|
# @param name [String, nil] optional name for the pool, used in thread/ractor names
|
|
78
90
|
# @param on_error [Proc, nil] optional shareable proc called with the raised exception when a worker raises
|
|
79
91
|
# @yieldparam result [Object] the result returned by the worker proc
|
|
80
92
|
# @return [void]
|
|
81
93
|
# @raise [ArgumentError] if size is not a positive integer
|
|
82
94
|
# @raise [ArgumentError] if worker is not a proc
|
|
95
|
+
# @raise [ArgumentError] if strategy is not +:coordinator+ or +:round_robin+
|
|
83
96
|
# @raise [ArgumentError] if on_error is given but is not a proc
|
|
84
97
|
#
|
|
85
98
|
# @example With result handler
|
|
@@ -88,13 +101,16 @@ class RactorPool
|
|
|
88
101
|
# @example Without result handler
|
|
89
102
|
# pool = RactorPool.new(size: 4, worker: proc { it })
|
|
90
103
|
#
|
|
104
|
+
# @example With round-robin strategy
|
|
105
|
+
# pool = RactorPool.new(size: 4, strategy: :round_robin, worker: proc { it })
|
|
106
|
+
#
|
|
91
107
|
# @example With error handler
|
|
92
108
|
# error_count = Atom.new(0)
|
|
93
109
|
# on_error = proc { error_count.swap { |count| count + 1 } }
|
|
94
110
|
# pool = RactorPool.new(size: 4, worker: proc { raise }, on_error: on_error)
|
|
95
111
|
#
|
|
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
|
|
112
|
+
# @rbs (?size: Integer, worker: ^(untyped) -> untyped, ?strategy: Symbol, ?name: String?, ?on_error: (^(Exception) -> void | nil)) ?{ (untyped) -> void } -> void
|
|
113
|
+
def initialize: (worker: ^(untyped) -> untyped, ?size: Integer, ?strategy: Symbol, ?name: String?, ?on_error: ^(Exception) -> void | nil) ?{ (untyped) -> void } -> void
|
|
98
114
|
|
|
99
115
|
# Queues a work item to be processed by an available worker.
|
|
100
116
|
#
|
|
@@ -137,8 +153,8 @@ class RactorPool
|
|
|
137
153
|
def start_workers: () -> Array[Ractor]
|
|
138
154
|
|
|
139
155
|
# @rbs () -> Thread?
|
|
140
|
-
def
|
|
156
|
+
def start_collector: () -> Thread?
|
|
141
157
|
|
|
142
158
|
# @rbs () -> Thread?
|
|
143
|
-
def
|
|
159
|
+
def start_error_collector: () -> Thread?
|
|
144
160
|
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.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joshua Young
|
|
@@ -73,8 +73,8 @@ 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.10
|
|
77
77
|
specification_version: 4
|
|
78
|
-
summary: A thread-safe, lock-free pool of Ractor workers with
|
|
79
|
-
for distributing work
|
|
78
|
+
summary: A thread-safe, lock-free pool of Ractor workers with coordinator or round-robin
|
|
79
|
+
dispatch for distributing work
|
|
80
80
|
test_files: []
|