ractor-pool 0.3.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 13aa15dab296025cc8fa8348a4477bd8afd2db09b24ea4e13c43a17cca976cd2
4
- data.tar.gz: b99432e86a0e93b69aa84867b869a0502469b402a297698599175442987cc23f
3
+ metadata.gz: 2d8741738f0de76db49cbbbaccc701b687af3df13ce9802205c7661a10519126
4
+ data.tar.gz: 337ce4d26b7735ec2517c2b88e76f2b11bef87124e3581a74772e68fbd3b6e57
5
5
  SHA512:
6
- metadata.gz: 8f85b3c281484b9daf22e6e0fe2c195ca4a5a67640e3913d70f93cd109c022e3138cbae924477de09e2400c552b9b4c19eb386cf9b5b3479ec6bf60e382cbb4d
7
- data.tar.gz: 6101a788de91cbe8f681cfa3c3c05556154061f0f96910e2474c1b083b5c283455d2ee1c525d30f8e73785148ad6392dbf1464f578cdb8b07670718b911bfba5
6
+ metadata.gz: 663d3b28aeab7e0341bbfd5d78540c670fbb74e3cfc89679572093067fc72396028d3b1637c224676d4c52d86aaafd688143e28499217f0b513c7d5035ba42c4
7
+ data.tar.gz: 6a9fc3ec933ece355f9bba48ac1de2980a3c105f0baaf2a13684ab74a54028daec5f2695dee25c4d970d3dc71f7bd066d207977424f8f199c44f8f61e9e6ae85
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.0] - 2026-05-20
4
+
5
+ - Add `:round_robin` dispatch strategy
6
+
3
7
  ## [0.3.1] - 2026-05-12
4
8
 
5
9
  - Fix `result_port` race condition in single-worker shutdown
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  ![Version](https://img.shields.io/gem/v/ractor-pool)
4
4
  ![Build](https://badge.buildkite.com/f5f08eba1c869dee6e9e87dd66b241059d8fbefaaed6c56d86.svg)
5
5
 
6
- A thread-safe, lock-free pool of Ractor workers with a coordinator pattern for distributing work.
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class RactorPool
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.0"
5
5
  end
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 a coordinator pattern for distributing work.
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
- # Work is distributed on-demand to idle workers, ensuring efficient utilisation. Results
13
- # are collected and passed to a result handler running in a separate thread.
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 = []
@@ -48,16 +53,21 @@ 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?
@@ -69,12 +79,14 @@ class RactorPool
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,29 +95,35 @@ 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
128
  @collector = start_collector
111
129
  @error_collector = start_error_collector
@@ -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
- (@coordinator || @workers.first).send(work, move: true)
162
+ target.send(work, move: true)
137
163
  ensure
138
164
  @in_flight.swap { |count| count - 1 }
139
165
  end
@@ -175,7 +201,7 @@ class RactorPool
175
201
  @workers.each(&:join)
176
202
  @coordinator.join
177
203
  else
178
- @workers.first.send(SHUTDOWN, move: true)
204
+ @workers.each { |worker| worker.send(SHUTDOWN, move: true) }
179
205
  @workers.each(&:join)
180
206
  @result_port&.send(SHUTDOWN, move: true)
181
207
  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 a coordinator pattern for distributing work.
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
- # Work is distributed on-demand to idle workers, ensuring efficient utilisation. Results
7
- # are collected and passed to a result handler running in a separate thread.
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 = []
@@ -42,6 +47,8 @@ class RactorPool
42
47
  def message: () -> String
43
48
  end
44
49
 
50
+ STRATEGIES: untyped
51
+
45
52
  SHUTDOWN: ::Symbol
46
53
 
47
54
  @error_collector: Thread?
@@ -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
  #
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.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Young
@@ -75,6 +75,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
75
75
  requirements: []
76
76
  rubygems_version: 4.0.10
77
77
  specification_version: 4
78
- summary: A thread-safe, lock-free pool of Ractor workers with a coordinator pattern
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: []