omq-ractor 0.1.2 → 0.1.3

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.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/omq/ractor.rb +85 -5
  3. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 15931abc881035b0cac7d80667168cd3dbdd120226178b76725d2fa40260ac5f
4
- data.tar.gz: 82c4e9531bb7afc5e471d435fadb068b0e7cbf92b9a8db70a063c7623a9cb5bc
3
+ metadata.gz: 4672a215899078075ce99ebd8fdff3b985803dee92e0e17577f9748f2e2a89d0
4
+ data.tar.gz: 7ca1e9d1cc2470901b2bd17c8a1ef8f4db19e08cb118cde74415c350c06e5f04
5
5
  SHA512:
6
- metadata.gz: 41ebd53843bb6e69859a6546bc1d6f7926f8e03894c10de1bffdbd2a6d294e282bc1b8c04d735920178ea4b25bdd509d5822fee1979121dc6b307224150785fb
7
- data.tar.gz: 7f490382cb90a176d28a1d67e75f4edd5f53b2e10740366d81d49c0f5e73b21e63284ae084b1eb7616a9f54d609d64cc42b2e3ff39738f57e035293f6e8d6963
6
+ metadata.gz: f9702ec46b247fadeecedec98a4786992bfc4c47dffb01af09643514ffa835ca2747ae013b3a5e7bab5062d07997c4362a267ae4f8ab3c396f5fc8fa8a8cc63e
7
+ data.tar.gz: 90e3ed42c28560345f9171612029ed85d0f54b67b4ea9e2890da94b28797c4c2855f8b044b7f2c84968bddc974f870e545069a0803ac0c663f50aa884c78ea88
data/lib/omq/ractor.rb CHANGED
@@ -33,6 +33,7 @@ module OMQ
33
33
 
34
34
  HANDSHAKE_TIMEOUT = 0.1
35
35
 
36
+
36
37
  # Socket types that use topic/group-based routing.
37
38
  # These get topic-aware connection wrappers that preserve
38
39
  # the first frame (topic/group) as a plain string for matching.
@@ -45,6 +46,8 @@ module OMQ
45
46
  # the wrapped class (e.g. DirectPipe) still work.
46
47
  #
47
48
  module TransparentDelegator
49
+ # @param klass [Class] class to check against
50
+ # @return [Boolean] true if self or the wrapped object is_a? klass
48
51
  def is_a?(klass)
49
52
  super || __getobj__.is_a?(klass)
50
53
  end
@@ -56,11 +59,18 @@ module OMQ
56
59
  # The send pump is single-threaded, so identity check suffices.
57
60
  #
58
61
  class SerializeCache
62
+ # @return [SerializeCache]
59
63
  def initialize
60
64
  @last = nil
61
65
  @bytes = nil
62
66
  end
63
67
 
68
+
69
+ # Returns the Marshal-dumped bytes for +obj+, reusing the cached result
70
+ # if +obj+ is the same object as the last call.
71
+ #
72
+ # @param obj [Object] object to serialize
73
+ # @return [String] frozen Marshal bytes
64
74
  def marshal(obj)
65
75
  return @bytes if obj.equal?(@last)
66
76
  @last = obj
@@ -75,19 +85,29 @@ module OMQ
75
85
  class MarshalConnection < SimpleDelegator
76
86
  include TransparentDelegator
77
87
 
88
+ # @param conn [Object] underlying connection to wrap
89
+ # @param cache [SerializeCache] shared serialization cache
78
90
  def initialize(conn, cache)
79
91
  super(conn)
80
92
  @cache = cache
81
93
  end
82
94
 
95
+
96
+ # @param parts [Array<String>] message frames to serialize and send
97
+ # @return [void]
83
98
  def send_message(parts)
84
99
  super([@cache.marshal(parts)])
85
100
  end
86
101
 
102
+
103
+ # @param parts [Array<String>] message frames to serialize and write
104
+ # @return [void]
87
105
  def write_message(parts)
88
106
  super([@cache.marshal(parts)])
89
107
  end
90
108
 
109
+
110
+ # @return [Object] deserialized message
91
111
  def receive_message
92
112
  Marshal.load(super.first)
93
113
  end
@@ -99,6 +119,8 @@ module OMQ
99
119
  class ShareableConnection < SimpleDelegator
100
120
  include TransparentDelegator
101
121
 
122
+ # @param obj [Object] message to freeze and send via Ractor.make_shareable
123
+ # @return [void]
102
124
  def send_message(obj)
103
125
  super(::Ractor.make_shareable(obj))
104
126
  end
@@ -112,19 +134,29 @@ module OMQ
112
134
  class TopicMarshalConnection < SimpleDelegator
113
135
  include TransparentDelegator
114
136
 
137
+ # @param conn [Object] underlying connection to wrap
138
+ # @param cache [SerializeCache] shared serialization cache
115
139
  def initialize(conn, cache)
116
140
  super(conn)
117
141
  @cache = cache
118
142
  end
119
143
 
144
+
145
+ # @param parts [Array<String>] message frames; first frame is topic
146
+ # @return [void]
120
147
  def send_message(parts)
121
148
  super([parts[0], @cache.marshal(parts[1..])])
122
149
  end
123
150
 
151
+
152
+ # @param parts [Array<String>] message frames; first frame is topic
153
+ # @return [void]
124
154
  def write_message(parts)
125
155
  super([parts[0], @cache.marshal(parts[1..])])
126
156
  end
127
157
 
158
+
159
+ # @return [Array<String>] deserialized message with topic as first element
128
160
  def receive_message
129
161
  parts = super
130
162
  [parts[0], *Marshal.load(parts[1])]
@@ -137,6 +169,8 @@ module OMQ
137
169
  class TopicShareableConnection < SimpleDelegator
138
170
  include TransparentDelegator
139
171
 
172
+ # @param parts [Array<String>] message frames to freeze and send
173
+ # @return [void]
140
174
  def send_message(parts)
141
175
  super(::Ractor.make_shareable(parts))
142
176
  end
@@ -148,10 +182,16 @@ module OMQ
148
182
  # Raised by SocketProxy#receive after the socket has been closed.
149
183
  # The first receive after closure returns nil; subsequent calls raise.
150
184
  #
151
- class SocketClosedError < IOError; end
185
+ class SocketClosedError < IOError
186
+ end
152
187
 
153
188
 
189
+ # Ractor-side proxy for an OMQ socket. Provides #receive, #<<, and #publish
190
+ # to communicate with the main-thread socket through Ractor ports.
154
191
  class SocketProxy
192
+ # @param input_port [Ractor::Port, nil] port for receiving messages (nil if write-only)
193
+ # @param output_port [Ractor::Port, nil] port for sending messages (nil if read-only)
194
+ # @param topic_type [Boolean] whether this is a topic-based socket (PUB/SUB, RADIO/DISH)
155
195
  def initialize(input_port, output_port, topic_type)
156
196
  @in = input_port
157
197
  @out = output_port
@@ -159,6 +199,7 @@ module OMQ
159
199
  @closed = false
160
200
  end
161
201
 
202
+
162
203
  # Receives the next message from this socket.
163
204
  # Returns nil once when the socket closes, then raises
164
205
  # SocketClosedError on subsequent calls.
@@ -176,6 +217,7 @@ module OMQ
176
217
  @topic_type ? msg.last : msg
177
218
  end
178
219
 
220
+
179
221
  # Receives the next message with its topic (PUB/SUB, RADIO/DISH).
180
222
  #
181
223
  # @return [Array(String, Object), nil] [topic, payload], or nil on close
@@ -191,6 +233,7 @@ module OMQ
191
233
  [msg.first, msg.last]
192
234
  end
193
235
 
236
+
194
237
  # Sends a message through this socket.
195
238
  # For topic-based sockets, wraps as ["", obj] (all subscribers).
196
239
  #
@@ -207,6 +250,7 @@ module OMQ
207
250
  self
208
251
  end
209
252
 
253
+
210
254
  # Publishes a message with an explicit topic (PUB/SUB, RADIO/DISH).
211
255
  #
212
256
  # @param msg [Object] payload
@@ -219,6 +263,7 @@ module OMQ
219
263
  self
220
264
  end
221
265
 
266
+
222
267
  # Returns the input port for use with Ractor.select.
223
268
  #
224
269
  # @return [Ractor::Port]
@@ -234,14 +279,20 @@ module OMQ
234
279
  # Ractor.select results.
235
280
  #
236
281
  class SocketSet < Array
282
+ # @param proxies [Array<SocketProxy>] socket proxies to include
237
283
  def initialize(proxies)
238
284
  super(proxies)
239
285
  @by_port = {}
240
286
  proxies.each do |proxy|
241
- @by_port[proxy.to_port] = proxy if proxy.to_port rescue nil
287
+ begin
288
+ @by_port[proxy.to_port] = proxy if proxy.to_port
289
+ rescue ::Ractor::ClosedError
290
+ # Skip non-readable proxies.
291
+ end
242
292
  end
243
293
  end
244
294
 
295
+
245
296
  # Returns the SocketProxy whose input port matches +port+.
246
297
  # Use after Ractor.select to map back to the proxy:
247
298
  #
@@ -262,14 +313,32 @@ module OMQ
262
313
  # Frozen, shareable context passed to the worker Ractor.
263
314
  # The user calls #sockets to trigger the setup handshake.
264
315
  #
316
+ # An optional +data+ object (any Ractor.make_shareable-able value)
317
+ # can be passed via OMQ::Ractor.new(data: …) and retrieved inside
318
+ # the worker block with +omq.data+. This is the only way to pass
319
+ # extra information into a worker block under Ruby 4.0's strict
320
+ # Ractor isolation, which forbids capturing any outer local variable.
321
+ #
265
322
  class Context
266
- def initialize(setup_port, output_ports, socket_configs)
323
+ # @param setup_port [Ractor::Port] port for the setup handshake
324
+ # @param output_ports [Array<Ractor::Port, nil>] output ports for writable sockets
325
+ # @param socket_configs [Array<Hash>] per-socket configuration hashes
326
+ # @param data [Object, nil] optional shareable data for the worker
327
+ def initialize(setup_port, output_ports, socket_configs, data: nil)
267
328
  @setup_port = setup_port
268
329
  @output_ports = output_ports
269
330
  @socket_configs = socket_configs
331
+ @data = data
270
332
  ::Ractor.make_shareable(self)
271
333
  end
272
334
 
335
+
336
+ # User-supplied shareable data (passed as +data:+ to OMQ::Ractor.new).
337
+ #
338
+ # @return [Object, nil]
339
+ #
340
+ attr_reader :data
341
+
273
342
  # Performs the setup handshake and returns SocketProxy objects.
274
343
  #
275
344
  # @return [Array<SocketProxy>]
@@ -293,10 +362,14 @@ module OMQ
293
362
  #
294
363
  # @param sockets [Array<Socket>] sockets to bridge
295
364
  # @param serialize [Boolean] whether to auto-serialize per connection (default: true)
365
+ # @param data [Object, nil] optional shareable data accessible as +omq.data+
366
+ # inside the worker block. Under Ruby 4.0's strict Ractor isolation,
367
+ # worker blocks cannot close over outer local variables; use +data:+ to
368
+ # pass configuration into the block.
296
369
  # @yield [Context] block executes inside the worker Ractor;
297
370
  # must call omq.sockets immediately
298
371
  #
299
- def initialize(*sockets, serialize: true, &block)
372
+ def initialize(*sockets, serialize: true, data: nil, &block)
300
373
  raise ArgumentError, "no sockets given" if sockets.empty?
301
374
  raise ArgumentError, "no block given" unless block
302
375
 
@@ -311,6 +384,7 @@ module OMQ
311
384
  serialize: serialize, topic_type: topic_type }
312
385
  end
313
386
 
387
+
314
388
  # Main Ractor creates output ports (one per writable socket)
315
389
  @output_ports = socket_configs.map { |cfg| cfg[:writable] ? ::Ractor::Port.new : nil }
316
390
  output_ports = @output_ports
@@ -321,7 +395,8 @@ module OMQ
321
395
  # Build frozen context for the worker
322
396
  frozen_configs = ::Ractor.make_shareable(socket_configs)
323
397
  frozen_outputs = ::Ractor.make_shareable(output_ports)
324
- ctx = Context.new(setup_port, frozen_outputs, frozen_configs)
398
+ frozen_data = data ? ::Ractor.make_shareable(data) : nil
399
+ ctx = Context.new(setup_port, frozen_outputs, frozen_configs, data: frozen_data)
325
400
 
326
401
  # Install connection wrappers for per-connection serialization
327
402
  install_connection_wrappers(socket_configs) if serialize
@@ -356,6 +431,7 @@ module OMQ
356
431
  # Waits for the worker Ractor to finish naturally.
357
432
  # The worker must return from its block on its own.
358
433
  #
434
+ # @return [void]
359
435
  def join
360
436
  await_ractor { @ractor.join }
361
437
  ensure
@@ -366,6 +442,7 @@ module OMQ
366
442
  # Returns the worker Ractor's return value.
367
443
  # The worker must return from its block on its own.
368
444
  #
445
+ # @return [Object] the worker block's return value
369
446
  def value
370
447
  await_ractor { @ractor.value }
371
448
  ensure
@@ -377,6 +454,7 @@ module OMQ
377
454
  # Sends nil through all input ports, causing proxy.receive
378
455
  # to return nil (first time) or raise SocketClosedError.
379
456
  #
457
+ # @return [void]
380
458
  def close
381
459
  @input_ports.each { |p| p&.send(nil) rescue nil }
382
460
  await_ractor { @ractor.join } rescue nil
@@ -507,6 +585,7 @@ module OMQ
507
585
  wr.close rescue nil
508
586
  end
509
587
 
588
+
510
589
  # Async task: wait on pipe, drain queue, enqueue to engine
511
590
  @input_tasks << @parent_task.async(transient: true, annotation: "ractor output bridge") do
512
591
  loop do
@@ -532,6 +611,7 @@ module OMQ
532
611
 
533
612
  SHUTDOWN = :__omq_ractor_shutdown__
534
613
 
614
+
535
615
  def cleanup_bridges
536
616
  @input_tasks.each { |t| t.stop rescue nil }
537
617
  # Unblock output bridge Threads waiting on port.receive
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omq-ractor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrik Wenger