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.
@@ -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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ # OMQ.rs-backed Ruby socket API.
5
+ module Rust
6
+ # Ruby binding version.
7
+ # @return [String]
8
+ VERSION = "0.1.0"
9
+ end
10
+ 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
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "omq/rs"