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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 73bb38fa65f10e0a6eb11265acef8a298e6d0109a2c5b2c1dba750b38a0d5ed4
4
- data.tar.gz: 4fa41532f78bceea5ddcd33a4c9ce9f9efefe114edb37de5953cd97438f349ad
3
+ metadata.gz: b97336e3bda02791d66cbef83761b58ff599afca2e58698bd79e7aa5bdedc497
4
+ data.tar.gz: 77b0ff65e53978436edc7701c204415709092f96a71f1c834e7ce719c75fc3d4
5
5
  SHA512:
6
- metadata.gz: fee7cb83e33e99e265484986b1208c0742216322cd7bc0f23a3de14f38f9fe71d21adbccec1179f51cd4aa4f0875c8291ae2b621a0599edbb479357bc2dabe2c
7
- data.tar.gz: 8135a1c1fbc84d70e0b6650d87715a94ee2322a67e6471786df29f8c2c0cd927a4de061301982a1802ad4a5b2f408e8aa34f1ae31679b9a5edbc4e1cfb5481cc
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class RactorPool
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
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 @state: Atom[Hash[Symbol, bool | Integer]]
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
- # @rbs (?size: Integer, worker: ^(untyped) -> untyped, ?name: String?) ?{ (untyped) -> void } -> void
81
- def initialize(size: Etc.nprocessors, worker:, name: nil, &result_handler)
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
- @state = Atom.new(in_flight: 0, shutdown: false)
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
- state = @state.swap do |current_state|
111
- if current_state[:shutdown]
112
- current_state
113
- else
114
- current_state.merge(in_flight: current_state[:in_flight] + 1)
115
- end
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
- @state.swap do |current_state|
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
- @state.swap do |current_state|
148
- if current_state[:shutdown]
161
+ @shutdown.swap do |current|
162
+ if current
149
163
  already_shutdown = true
150
- current_state
164
+ current
151
165
  else
152
- current_state.merge(shutdown: true)
166
+ true
153
167
  end
154
168
  end
155
169
  return if already_shutdown
156
170
 
157
- Thread.pass until @state.value[:in_flight] == 0
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
- puts "#{Ractor.current.name} rescued:"
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
- @state: Atom[Hash[Symbol, bool | Integer]]
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
- # @rbs (?size: Integer, worker: ^(untyped) -> untyped, ?name: String?) ?{ (untyped) -> void } -> void
82
- def initialize: (worker: ^(untyped) -> untyped, ?size: Integer, ?name: String?) ?{ (untyped) -> void } -> void
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.2.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.3
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