omq-rs 0.1.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 +7 -0
- data/CHANGELOG.md +12 -0
- data/LICENSE +15 -0
- data/README.md +161 -0
- data/ext/omq_rs_native/Cargo.toml +50 -0
- data/ext/omq_rs_native/build.rs +24 -0
- data/ext/omq_rs_native/extconf.rb +8 -0
- data/ext/omq_rs_native/src/auth.rs +182 -0
- data/ext/omq_rs_native/src/error.rs +15 -0
- data/ext/omq_rs_native/src/lib.rs +126 -0
- data/ext/omq_rs_native/src/notify.rs +114 -0
- data/ext/omq_rs_native/src/options.rs +434 -0
- data/ext/omq_rs_native/src/rb.rs +494 -0
- data/ext/omq_rs_native/src/runtime.rs +459 -0
- data/ext/omq_rs_native/src/socket.rs +972 -0
- data/lib/omq/rs/socket.rb +662 -0
- data/lib/omq/rs/version.rb +10 -0
- data/lib/omq/rs.rb +30 -0
- data/lib/omq-rs.rb +3 -0
- metadata +77 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/wait"
|
|
4
|
+
|
|
5
|
+
module OMQ
|
|
6
|
+
# OMQ.rs-backed Ruby socket API.
|
|
7
|
+
module Rust
|
|
8
|
+
# Supported socket type names.
|
|
9
|
+
# @return [Array<Symbol>]
|
|
10
|
+
SOCKET_TYPES = %i[
|
|
11
|
+
req rep pub sub xpub xsub push pull dealer router pair stream
|
|
12
|
+
client server radio dish scatter gather channel peer
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
# @api private
|
|
16
|
+
ROUTED_TYPES = %i[server].freeze
|
|
17
|
+
|
|
18
|
+
# @api private
|
|
19
|
+
SINGLE_FRAME_TYPES = %i[client server scatter gather channel].freeze
|
|
20
|
+
|
|
21
|
+
# CURVE peer metadata passed to callable authenticators.
|
|
22
|
+
#
|
|
23
|
+
# @!attribute [r] public_key
|
|
24
|
+
# @return [String] peer's 40-byte Z85 public key
|
|
25
|
+
# @!attribute [r] identity
|
|
26
|
+
# @return [String, nil] peer's ZMTP identity
|
|
27
|
+
MechanismPeerInfo = Data.define(:public_key, :identity)
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
# Returns number of OMQ.rs IO threads.
|
|
31
|
+
#
|
|
32
|
+
# @return [Integer]
|
|
33
|
+
def io_threads
|
|
34
|
+
Native.io_threads
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Sets number of OMQ.rs IO threads used by subsequently created sockets.
|
|
38
|
+
#
|
|
39
|
+
# @param count [Integer] positive IO thread count
|
|
40
|
+
# @return [Integer] assigned count
|
|
41
|
+
# @raise [ArgumentError] if +count+ is not positive
|
|
42
|
+
def io_threads=(count)
|
|
43
|
+
count = Integer(count)
|
|
44
|
+
raise ArgumentError, "io_threads must be positive" unless count.positive?
|
|
45
|
+
|
|
46
|
+
Native.send(:io_threads=, count)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Creates a socket for a named OMQ pattern.
|
|
50
|
+
#
|
|
51
|
+
# @param socket_type [Symbol, String] socket pattern, such as +:pull+
|
|
52
|
+
# @param options [Hash] native socket options
|
|
53
|
+
# @return [Socket] concrete socket instance
|
|
54
|
+
# @raise [ArgumentError] if +socket_type+ is unknown or an option is invalid
|
|
55
|
+
def socket(socket_type, **options)
|
|
56
|
+
type = socket_type.to_s.downcase.to_sym
|
|
57
|
+
unless SOCKET_TYPES.include?(type)
|
|
58
|
+
raise ArgumentError, "unknown socket type: #{socket_type}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
const_get(type.to_s.upcase).new(**options)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Reports whether binding was compiled with a feature.
|
|
65
|
+
#
|
|
66
|
+
# @param feature [Symbol, String] feature name, such as +:curve+ or +:zstd+
|
|
67
|
+
# @return [Boolean]
|
|
68
|
+
def has(feature)
|
|
69
|
+
Native.has(feature.to_s)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Generates a CurveZMQ keypair.
|
|
73
|
+
#
|
|
74
|
+
# @return [Array<String>] public and secret 40-byte Z85 keys
|
|
75
|
+
def curve_keypair
|
|
76
|
+
Native.curve_keypair
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Derives CurveZMQ public key from a secret key.
|
|
80
|
+
#
|
|
81
|
+
# @param secret_key [String] 40-byte Z85 secret key
|
|
82
|
+
# @return [String] 40-byte Z85 public key
|
|
83
|
+
# @raise [ArgumentError] if +secret_key+ is invalid
|
|
84
|
+
def curve_public(secret_key)
|
|
85
|
+
Native.curve_public(secret_key)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Adapts a public CURVE authenticator to native peer metadata.
|
|
89
|
+
#
|
|
90
|
+
# @param authenticator [#call] callable receiving {MechanismPeerInfo}
|
|
91
|
+
# @return [Proc]
|
|
92
|
+
# @api private
|
|
93
|
+
def wrap_curve_authenticator(authenticator)
|
|
94
|
+
proc do |peer|
|
|
95
|
+
authenticator.call(
|
|
96
|
+
MechanismPeerInfo.new(
|
|
97
|
+
public_key: peer.fetch(:public_key),
|
|
98
|
+
identity: peer[:identity],
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Enumerable stream of socket lifecycle events.
|
|
106
|
+
class Monitor
|
|
107
|
+
include Enumerable
|
|
108
|
+
|
|
109
|
+
# Creates a monitor for a socket.
|
|
110
|
+
#
|
|
111
|
+
# Normally obtained through {Socket#monitor}.
|
|
112
|
+
#
|
|
113
|
+
# @param socket [Socket]
|
|
114
|
+
# @return [Monitor]
|
|
115
|
+
def initialize(socket)
|
|
116
|
+
@socket = socket
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Receives next monitor event.
|
|
120
|
+
#
|
|
121
|
+
# @param timeout [Numeric, nil] maximum wait in seconds
|
|
122
|
+
# @return [Hash, nil] event fields, or +nil+ when socket closes
|
|
123
|
+
# @raise [IO::TimeoutError] if timeout expires
|
|
124
|
+
def recv(timeout: nil)
|
|
125
|
+
@socket.monitor_event(timeout: timeout)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Receives next available monitor event without blocking.
|
|
129
|
+
#
|
|
130
|
+
# @return [Hash, nil]
|
|
131
|
+
def recv_nowait
|
|
132
|
+
@socket.try_monitor_event
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Yields monitor events until socket closes.
|
|
136
|
+
#
|
|
137
|
+
# @yieldparam event [Hash]
|
|
138
|
+
# @return [Enumerator, nil] enumerator without a block
|
|
139
|
+
def each
|
|
140
|
+
return enum_for(__method__) unless block_given?
|
|
141
|
+
|
|
142
|
+
loop { yield recv }
|
|
143
|
+
rescue IOError
|
|
144
|
+
raise unless @socket.closed?
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Base class for OMQ.rs-backed sockets.
|
|
149
|
+
class Socket
|
|
150
|
+
# @return [Symbol] lowercase socket pattern
|
|
151
|
+
attr_reader :socket_type
|
|
152
|
+
|
|
153
|
+
# Creates a socket without binding or connecting it.
|
|
154
|
+
#
|
|
155
|
+
# @param recv_timeout [Numeric, nil] receive timeout in seconds
|
|
156
|
+
# @param send_timeout [Numeric, nil] send timeout in seconds
|
|
157
|
+
# @param curve_auth [Array<String>, #call, nil] CURVE allowlist or authenticator
|
|
158
|
+
# @param options [Hash] native OMQ.rs socket options
|
|
159
|
+
# @return [Socket]
|
|
160
|
+
# @raise [ArgumentError] if an option is invalid
|
|
161
|
+
def initialize(recv_timeout: nil, send_timeout: nil, curve_auth: nil, **options)
|
|
162
|
+
socket_type = self.class.const_get(:SOCKET_TYPE, false)
|
|
163
|
+
@socket_type = socket_type.to_s.downcase.to_sym
|
|
164
|
+
unless SOCKET_TYPES.include?(@socket_type)
|
|
165
|
+
raise ArgumentError, "unknown socket type: #{socket_type}"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
@recv_timeout = recv_timeout
|
|
169
|
+
@send_timeout = send_timeout
|
|
170
|
+
@recv_batch = []
|
|
171
|
+
@request_waiting = false
|
|
172
|
+
@reply_ready = false
|
|
173
|
+
@native = Native::Socket.new(@socket_type.to_s.upcase)
|
|
174
|
+
@native.set_options(normalize_options(options))
|
|
175
|
+
@materialize_lock = Mutex.new
|
|
176
|
+
@materialized = false
|
|
177
|
+
@recv_io = nil
|
|
178
|
+
@send_io = nil
|
|
179
|
+
set_curve_auth(curve_auth) unless curve_auth.nil?
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Binds socket to an endpoint.
|
|
183
|
+
#
|
|
184
|
+
# @param endpoint [String, #to_str]
|
|
185
|
+
# @return [String] resolved endpoint, including assigned ephemeral port
|
|
186
|
+
def bind(endpoint)
|
|
187
|
+
ensure_materialized
|
|
188
|
+
@native.bind(String(endpoint))
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Connects socket to an endpoint.
|
|
192
|
+
#
|
|
193
|
+
# @param endpoint [String, #to_str]
|
|
194
|
+
# @return [Socket] self
|
|
195
|
+
def connect(endpoint)
|
|
196
|
+
ensure_materialized
|
|
197
|
+
@native.connect(String(endpoint))
|
|
198
|
+
self
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Disconnects socket from an endpoint.
|
|
202
|
+
#
|
|
203
|
+
# @param endpoint [String, #to_str]
|
|
204
|
+
# @return [Socket] self
|
|
205
|
+
def disconnect(endpoint)
|
|
206
|
+
ensure_materialized
|
|
207
|
+
@native.disconnect(String(endpoint))
|
|
208
|
+
self
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Stops listening on an endpoint.
|
|
212
|
+
#
|
|
213
|
+
# @param endpoint [String, #to_str]
|
|
214
|
+
# @return [Socket] self
|
|
215
|
+
def unbind(endpoint)
|
|
216
|
+
ensure_materialized
|
|
217
|
+
@native.unbind(String(endpoint))
|
|
218
|
+
self
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Returns metadata for live SERVER route.
|
|
222
|
+
#
|
|
223
|
+
# @param routing_id [Integer] SERVER routing ID
|
|
224
|
+
# @return [Hash, nil] peer metadata, or +nil+ for stale route
|
|
225
|
+
# @raise [RuntimeError] unless called on SERVER socket
|
|
226
|
+
def peer_info(routing_id)
|
|
227
|
+
ensure_materialized
|
|
228
|
+
@native.peer_info(routing_id)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Configures CURVE client authentication before socket materialization.
|
|
232
|
+
#
|
|
233
|
+
# @param authenticator [Array<String>, #call, nil] public-key allowlist,
|
|
234
|
+
# callable receiving {MechanismPeerInfo}, or +nil+ to allow valid clients
|
|
235
|
+
# @yieldparam peer [MechanismPeerInfo]
|
|
236
|
+
# @return [Socket] self
|
|
237
|
+
# @raise [RuntimeError] if socket is already materialized
|
|
238
|
+
# @raise [TypeError] if authenticator is unsupported
|
|
239
|
+
def set_curve_auth(authenticator = nil, &block)
|
|
240
|
+
raise RuntimeError, "CURVE authentication must be configured before bind or connect" if @materialized
|
|
241
|
+
authenticator = block if block
|
|
242
|
+
|
|
243
|
+
case authenticator
|
|
244
|
+
when nil
|
|
245
|
+
@native.clear_curve_auth
|
|
246
|
+
when Array
|
|
247
|
+
@native.set_curve_auth_keys(authenticator)
|
|
248
|
+
else
|
|
249
|
+
unless authenticator.respond_to?(:call)
|
|
250
|
+
raise TypeError, "CURVE authenticator must be an Array, callable, or nil"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
@native.set_curve_auth_callback(Rust.wrap_curve_authenticator(authenticator))
|
|
254
|
+
end
|
|
255
|
+
self
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Sends message, blocking while send queue is full.
|
|
259
|
+
#
|
|
260
|
+
# @param message [String, Integer, Array] first frame or complete message
|
|
261
|
+
# @param more [Array<String, Integer>] additional frames
|
|
262
|
+
# @return [Socket] self
|
|
263
|
+
# @raise [IO::TimeoutError] if send timeout expires
|
|
264
|
+
# @raise [ArgumentError, RuntimeError] if message violates socket pattern
|
|
265
|
+
def send(message, *more)
|
|
266
|
+
ensure_materialized
|
|
267
|
+
parts = normalize_parts(message, more)
|
|
268
|
+
validate_send_parts!(parts)
|
|
269
|
+
validate_pattern_state_before_send!
|
|
270
|
+
|
|
271
|
+
loop do
|
|
272
|
+
result = enqueue(parts)
|
|
273
|
+
if result == :ok
|
|
274
|
+
sent!
|
|
275
|
+
return self
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
wait_for(@send_io, @send_timeout, "send timed out")
|
|
279
|
+
raise IOError, "socket closed" if closed?
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
# Sends message using {#send}.
|
|
283
|
+
# @see #send
|
|
284
|
+
alias << send
|
|
285
|
+
|
|
286
|
+
# Attempts to send without blocking.
|
|
287
|
+
#
|
|
288
|
+
# @param message [String, Integer, Array] first frame or complete message
|
|
289
|
+
# @param more [Array<String, Integer>] additional frames
|
|
290
|
+
# @return [Boolean] whether message was queued
|
|
291
|
+
# @raise [ArgumentError, RuntimeError] if message violates socket pattern
|
|
292
|
+
def try_send(message, *more)
|
|
293
|
+
ensure_materialized
|
|
294
|
+
parts = normalize_parts(message, more)
|
|
295
|
+
validate_send_parts!(parts)
|
|
296
|
+
validate_pattern_state_before_send!
|
|
297
|
+
return false unless enqueue(parts) == :ok
|
|
298
|
+
|
|
299
|
+
sent!
|
|
300
|
+
true
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Receives next message.
|
|
304
|
+
#
|
|
305
|
+
# @return [Array<String, Integer>] message frames; SERVER prepends routing ID
|
|
306
|
+
# @raise [IO::TimeoutError] if receive timeout expires
|
|
307
|
+
# @raise [IOError] if socket closes
|
|
308
|
+
def recv
|
|
309
|
+
ensure_materialized
|
|
310
|
+
message = try_recv
|
|
311
|
+
return message if message
|
|
312
|
+
|
|
313
|
+
loop do
|
|
314
|
+
wait_for(@recv_io, @recv_timeout, "receive timed out")
|
|
315
|
+
message = try_recv
|
|
316
|
+
return message if message
|
|
317
|
+
raise IOError, "socket closed" if closed?
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
# Receives next message using {#recv}.
|
|
321
|
+
# @see #recv
|
|
322
|
+
alias receive recv
|
|
323
|
+
|
|
324
|
+
# Attempts to receive without blocking.
|
|
325
|
+
#
|
|
326
|
+
# @return [Array<String, Integer>, nil] next message, or +nil+ if none is ready
|
|
327
|
+
def try_recv
|
|
328
|
+
ensure_materialized
|
|
329
|
+
unless @recv_batch.empty?
|
|
330
|
+
message = @recv_batch.shift
|
|
331
|
+
received!
|
|
332
|
+
return message
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
message = if ROUTED_TYPES.include?(@socket_type)
|
|
336
|
+
@native.try_recv_routed
|
|
337
|
+
elsif (batch = @native.try_recv_batch)
|
|
338
|
+
first = batch.shift
|
|
339
|
+
@recv_batch = batch
|
|
340
|
+
first
|
|
341
|
+
end
|
|
342
|
+
received! if message
|
|
343
|
+
message
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Waits for receive notification.
|
|
347
|
+
#
|
|
348
|
+
# Notification may represent a message, close, or explicit {#wake_recv}.
|
|
349
|
+
#
|
|
350
|
+
# @param timeout [Numeric, nil] maximum wait in seconds
|
|
351
|
+
# @return [true]
|
|
352
|
+
# @raise [IO::TimeoutError] if timeout expires
|
|
353
|
+
def wait_readable(timeout: @recv_timeout)
|
|
354
|
+
ensure_materialized
|
|
355
|
+
wait_for(@recv_io, timeout, "receive timed out")
|
|
356
|
+
true
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Wakes a thread or fiber blocked in {#wait_readable}.
|
|
360
|
+
#
|
|
361
|
+
# @return [Socket] self
|
|
362
|
+
def wake_recv
|
|
363
|
+
@native.wake_recv if @materialized
|
|
364
|
+
self
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Yields received messages until socket closes.
|
|
368
|
+
#
|
|
369
|
+
# @yieldparam message [Array<String, Integer>]
|
|
370
|
+
# @return [Enumerator, nil] enumerator without a block
|
|
371
|
+
def each
|
|
372
|
+
return enum_for(__method__) unless block_given?
|
|
373
|
+
|
|
374
|
+
loop { yield recv }
|
|
375
|
+
rescue IOError
|
|
376
|
+
raise unless closed?
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Adds SUB or XSUB subscription prefix.
|
|
380
|
+
#
|
|
381
|
+
# @param prefix [String, #to_str]
|
|
382
|
+
# @return [Socket] self
|
|
383
|
+
def subscribe(prefix = "")
|
|
384
|
+
ensure_materialized
|
|
385
|
+
@native.subscribe(String(prefix).b)
|
|
386
|
+
self
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Removes SUB or XSUB subscription prefix.
|
|
390
|
+
#
|
|
391
|
+
# @param prefix [String, #to_str]
|
|
392
|
+
# @return [Socket] self
|
|
393
|
+
def unsubscribe(prefix = "")
|
|
394
|
+
ensure_materialized
|
|
395
|
+
@native.unsubscribe(String(prefix).b)
|
|
396
|
+
self
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Joins DISH group.
|
|
400
|
+
#
|
|
401
|
+
# @param group [String, #to_str]
|
|
402
|
+
# @return [Socket] self
|
|
403
|
+
def join(group)
|
|
404
|
+
ensure_materialized
|
|
405
|
+
@native.join(String(group).b)
|
|
406
|
+
self
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Leaves DISH group.
|
|
410
|
+
#
|
|
411
|
+
# @param group [String, #to_str]
|
|
412
|
+
# @return [Socket] self
|
|
413
|
+
def leave(group)
|
|
414
|
+
ensure_materialized
|
|
415
|
+
@native.leave(String(group).b)
|
|
416
|
+
self
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# Publishes RADIO message to group.
|
|
420
|
+
#
|
|
421
|
+
# @param group [String, #to_str]
|
|
422
|
+
# @param message [String, #to_str]
|
|
423
|
+
# @return [Socket] self
|
|
424
|
+
def publish(group, message)
|
|
425
|
+
send(group, message)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# Waits until first peer completes handshake.
|
|
429
|
+
#
|
|
430
|
+
# @param timeout [Numeric, nil] maximum wait in seconds
|
|
431
|
+
# @return [Socket] self
|
|
432
|
+
# @raise [IO::TimeoutError] if timeout expires
|
|
433
|
+
def wait_for_peer(timeout: nil)
|
|
434
|
+
ensure_materialized
|
|
435
|
+
wait_for_native_fd(@native.peer_connected_fd, timeout, "peer connection timed out")
|
|
436
|
+
self
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# Waits until PUB, XPUB, or RADIO receives first subscription.
|
|
440
|
+
#
|
|
441
|
+
# @param timeout [Numeric, nil] maximum wait in seconds
|
|
442
|
+
# @return [Socket] self
|
|
443
|
+
# @raise [IO::TimeoutError] if timeout expires
|
|
444
|
+
def wait_for_subscriber(timeout: nil)
|
|
445
|
+
ensure_materialized
|
|
446
|
+
wait_for_native_fd(@native.subscriber_joined_fd, timeout, "subscriber timed out")
|
|
447
|
+
self
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# Returns socket lifecycle monitor.
|
|
451
|
+
#
|
|
452
|
+
# @return [Monitor]
|
|
453
|
+
def monitor
|
|
454
|
+
ensure_materialized
|
|
455
|
+
@monitor ||= Monitor.new(self)
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Returns monitor notification file descriptor.
|
|
459
|
+
#
|
|
460
|
+
# Intended for event-loop adapters; use {#monitor} otherwise.
|
|
461
|
+
#
|
|
462
|
+
# @return [Integer]
|
|
463
|
+
def monitor_fd
|
|
464
|
+
ensure_materialized
|
|
465
|
+
@native.monitor_fd
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# Receives next monitor event.
|
|
469
|
+
#
|
|
470
|
+
# @param timeout [Numeric, nil] maximum wait in seconds
|
|
471
|
+
# @return [Hash, nil]
|
|
472
|
+
# @raise [IO::TimeoutError] if timeout expires
|
|
473
|
+
def monitor_event(timeout: @recv_timeout)
|
|
474
|
+
ensure_materialized
|
|
475
|
+
event = @native.try_recv_monitor
|
|
476
|
+
return event if event
|
|
477
|
+
|
|
478
|
+
wait_for_native_fd(@native.monitor_fd, timeout, "monitor receive timed out")
|
|
479
|
+
@native.try_recv_monitor
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# Attempts to receive monitor event without blocking.
|
|
483
|
+
#
|
|
484
|
+
# @return [Hash, nil]
|
|
485
|
+
def try_monitor_event
|
|
486
|
+
ensure_materialized
|
|
487
|
+
@native.try_recv_monitor
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# Closes socket and releases native resources.
|
|
491
|
+
#
|
|
492
|
+
# @return [nil]
|
|
493
|
+
def close
|
|
494
|
+
return if closed?
|
|
495
|
+
|
|
496
|
+
@native.close
|
|
497
|
+
close_wrapper(@recv_io)
|
|
498
|
+
close_wrapper(@send_io)
|
|
499
|
+
nil
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
# Reports whether socket is closed.
|
|
503
|
+
#
|
|
504
|
+
# @return [Boolean]
|
|
505
|
+
def closed?
|
|
506
|
+
@native.closed?
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
private
|
|
510
|
+
|
|
511
|
+
def ensure_materialized
|
|
512
|
+
return if @materialized
|
|
513
|
+
|
|
514
|
+
@materialize_lock.synchronize do
|
|
515
|
+
return if @materialized
|
|
516
|
+
raise IOError, "socket closed" if closed?
|
|
517
|
+
|
|
518
|
+
@native.materialize
|
|
519
|
+
@recv_io = IO.for_fd(@native.recv_fd, autoclose: false)
|
|
520
|
+
@send_io = IO.for_fd(@native.send_fd, autoclose: false)
|
|
521
|
+
@materialized = true
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def normalize_parts(message, more)
|
|
526
|
+
parts = if more.empty? && message.is_a?(Array)
|
|
527
|
+
message
|
|
528
|
+
else
|
|
529
|
+
[message, *more]
|
|
530
|
+
end
|
|
531
|
+
parts.map { |part| part.is_a?(Integer) ? part : String(part).b }
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def normalize_options(options)
|
|
535
|
+
options.to_h do |key, value|
|
|
536
|
+
value = value.to_s if value.is_a?(Symbol)
|
|
537
|
+
value = value.transform_keys(&:to_s) if value.is_a?(Hash)
|
|
538
|
+
[key.to_s, value]
|
|
539
|
+
end
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def validate_send_parts!(parts)
|
|
543
|
+
if ROUTED_TYPES.include?(@socket_type)
|
|
544
|
+
unless parts.length == 2 && parts[0].is_a?(Integer)
|
|
545
|
+
raise ArgumentError, "#{@socket_type.upcase} send requires [routing_id, body]"
|
|
546
|
+
end
|
|
547
|
+
elsif SINGLE_FRAME_TYPES.include?(@socket_type) && parts.length != 1
|
|
548
|
+
raise ArgumentError, "#{@socket_type.upcase} sockets require one message frame"
|
|
549
|
+
elsif @socket_type == :radio && parts.length != 2
|
|
550
|
+
raise ArgumentError, "RADIO send requires [group, body]"
|
|
551
|
+
elsif @socket_type == :stream && parts.length != 2
|
|
552
|
+
raise ArgumentError, "STREAM send requires [routing_id, body]"
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
def validate_pattern_state_before_send!
|
|
557
|
+
if @socket_type == :req && @request_waiting
|
|
558
|
+
raise RuntimeError, "REQ must receive before sending again"
|
|
559
|
+
end
|
|
560
|
+
if @socket_type == :rep && !@reply_ready
|
|
561
|
+
raise RuntimeError, "REP must receive before sending"
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def sent!
|
|
566
|
+
@request_waiting = true if @socket_type == :req
|
|
567
|
+
@reply_ready = false if @socket_type == :rep
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def received!
|
|
571
|
+
@request_waiting = false if @socket_type == :req
|
|
572
|
+
@reply_ready = true if @socket_type == :rep
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def enqueue(parts)
|
|
576
|
+
if ROUTED_TYPES.include?(@socket_type)
|
|
577
|
+
routing_id, body = parts
|
|
578
|
+
@native.enqueue_send_routed([body], routing_id)
|
|
579
|
+
else
|
|
580
|
+
@native.enqueue_send(parts)
|
|
581
|
+
end
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
def wait_for(io, timeout, message)
|
|
585
|
+
ready = io.wait_readable(timeout)
|
|
586
|
+
raise IO::TimeoutError, message unless ready
|
|
587
|
+
|
|
588
|
+
drain(io)
|
|
589
|
+
rescue Errno::EBADF
|
|
590
|
+
raise IOError, "socket closed" if closed?
|
|
591
|
+
|
|
592
|
+
raise
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def wait_for_native_fd(fd, timeout, message)
|
|
596
|
+
io = IO.for_fd(fd, autoclose: false)
|
|
597
|
+
wait_for(io, timeout, message)
|
|
598
|
+
ensure
|
|
599
|
+
close_wrapper(io)
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
def drain(io)
|
|
603
|
+
loop do
|
|
604
|
+
result = io.read_nonblock(256, exception: false)
|
|
605
|
+
break if result == :wait_readable || result.nil? || result.empty?
|
|
606
|
+
end
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
def close_wrapper(io)
|
|
610
|
+
io.close if io && !io.closed?
|
|
611
|
+
rescue IOError, SystemCallError
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
# @!parse
|
|
616
|
+
# # REQ socket.
|
|
617
|
+
# class REQ < Socket; end
|
|
618
|
+
# # REP socket.
|
|
619
|
+
# class REP < Socket; end
|
|
620
|
+
# # PUB socket.
|
|
621
|
+
# class PUB < Socket; end
|
|
622
|
+
# # SUB socket.
|
|
623
|
+
# class SUB < Socket; end
|
|
624
|
+
# # XPUB socket.
|
|
625
|
+
# class XPUB < Socket; end
|
|
626
|
+
# # XSUB socket.
|
|
627
|
+
# class XSUB < Socket; end
|
|
628
|
+
# # PUSH socket.
|
|
629
|
+
# class PUSH < Socket; end
|
|
630
|
+
# # PULL socket.
|
|
631
|
+
# class PULL < Socket; end
|
|
632
|
+
# # DEALER socket.
|
|
633
|
+
# class DEALER < Socket; end
|
|
634
|
+
# # ROUTER socket.
|
|
635
|
+
# class ROUTER < Socket; end
|
|
636
|
+
# # PAIR socket.
|
|
637
|
+
# class PAIR < Socket; end
|
|
638
|
+
# # STREAM socket.
|
|
639
|
+
# class STREAM < Socket; end
|
|
640
|
+
# # CLIENT socket.
|
|
641
|
+
# class CLIENT < Socket; end
|
|
642
|
+
# # SERVER socket.
|
|
643
|
+
# class SERVER < Socket; end
|
|
644
|
+
# # RADIO socket.
|
|
645
|
+
# class RADIO < Socket; end
|
|
646
|
+
# # DISH socket.
|
|
647
|
+
# class DISH < Socket; end
|
|
648
|
+
# # SCATTER socket.
|
|
649
|
+
# class SCATTER < Socket; end
|
|
650
|
+
# # GATHER socket.
|
|
651
|
+
# class GATHER < Socket; end
|
|
652
|
+
# # CHANNEL socket.
|
|
653
|
+
# class CHANNEL < Socket; end
|
|
654
|
+
# # PEER socket.
|
|
655
|
+
# class PEER < Socket; end
|
|
656
|
+
SOCKET_TYPES.each do |type|
|
|
657
|
+
klass = Class.new(Socket)
|
|
658
|
+
klass.const_set(:SOCKET_TYPE, type)
|
|
659
|
+
const_set(type.to_s.upcase, klass)
|
|
660
|
+
end
|
|
661
|
+
end
|
|
662
|
+
end
|
data/lib/omq/rs.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "rs/version"
|
|
4
|
+
require_relative "rs/omq_rs_native"
|
|
5
|
+
require_relative "rs/socket"
|
|
6
|
+
|
|
7
|
+
module OMQ
|
|
8
|
+
class << self
|
|
9
|
+
# Returns the OMQ.rs namespace or creates an OMQ.rs-backed socket.
|
|
10
|
+
#
|
|
11
|
+
# @param socket_type [Symbol, String, nil] socket pattern, such as +:push+
|
|
12
|
+
# @param options [Hash] socket options passed to {Rust.socket}
|
|
13
|
+
# @yield [socket] yields a newly created socket and closes it afterward
|
|
14
|
+
# @yieldparam socket [Rust::Socket]
|
|
15
|
+
# @return [Module, Rust::Socket, Object] the namespace, socket, or block result
|
|
16
|
+
# @raise [ArgumentError] if +socket_type+ is unknown or an option is invalid
|
|
17
|
+
def rs(socket_type = nil, **options)
|
|
18
|
+
return Rust if socket_type.nil?
|
|
19
|
+
|
|
20
|
+
socket = Rust.socket(socket_type, **options)
|
|
21
|
+
return socket unless block_given?
|
|
22
|
+
|
|
23
|
+
begin
|
|
24
|
+
yield socket
|
|
25
|
+
ensure
|
|
26
|
+
socket.close
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/omq-rs.rb
ADDED