omq 0.12.0 → 0.14.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +84 -1
  3. data/README.md +27 -0
  4. data/lib/omq/drop_queue.rb +3 -0
  5. data/lib/omq/engine/connection_setup.rb +70 -0
  6. data/lib/omq/engine/heartbeat.rb +40 -0
  7. data/lib/omq/engine/maintenance.rb +35 -0
  8. data/lib/omq/engine/reconnect.rb +82 -0
  9. data/lib/omq/engine/recv_pump.rb +119 -0
  10. data/lib/omq/engine.rb +139 -304
  11. data/lib/omq/options.rb +44 -0
  12. data/lib/omq/pair.rb +6 -0
  13. data/lib/omq/pub_sub.rb +25 -0
  14. data/lib/omq/push_pull.rb +17 -0
  15. data/lib/omq/queue_interface.rb +1 -0
  16. data/lib/omq/readable.rb +2 -0
  17. data/lib/omq/req_rep.rb +13 -0
  18. data/lib/omq/router_dealer.rb +12 -0
  19. data/lib/omq/routing/conn_send_pump.rb +36 -0
  20. data/lib/omq/routing/dealer.rb +15 -10
  21. data/lib/omq/routing/fair_queue.rb +172 -0
  22. data/lib/omq/routing/fair_recv.rb +27 -0
  23. data/lib/omq/routing/fan_out.rb +127 -74
  24. data/lib/omq/routing/pair.rb +47 -20
  25. data/lib/omq/routing/pub.rb +12 -6
  26. data/lib/omq/routing/pull.rb +12 -4
  27. data/lib/omq/routing/push.rb +3 -12
  28. data/lib/omq/routing/rep.rb +41 -51
  29. data/lib/omq/routing/req.rb +15 -10
  30. data/lib/omq/routing/round_robin.rb +82 -63
  31. data/lib/omq/routing/router.rb +32 -48
  32. data/lib/omq/routing/sub.rb +18 -5
  33. data/lib/omq/routing/xpub.rb +15 -3
  34. data/lib/omq/routing/xsub.rb +53 -27
  35. data/lib/omq/routing.rb +29 -11
  36. data/lib/omq/socket.rb +25 -7
  37. data/lib/omq/transport/inproc/direct_pipe.rb +173 -0
  38. data/lib/omq/transport/inproc.rb +41 -217
  39. data/lib/omq/transport/ipc.rb +7 -1
  40. data/lib/omq/transport/tcp.rb +12 -7
  41. data/lib/omq/version.rb +1 -1
  42. data/lib/omq/writable.rb +2 -0
  43. data/lib/omq.rb +4 -1
  44. metadata +14 -5
data/lib/omq/pub_sub.rb CHANGED
@@ -1,15 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OMQ
4
+ # PUB socket — publish messages to all matching subscribers.
5
+ #
4
6
  class PUB < Socket
5
7
  include Writable
6
8
 
9
+ # @param endpoints [String, nil] endpoint to bind/connect
10
+ # @param linger [Integer] linger period in seconds
11
+ # @param on_mute [Symbol] mute strategy for slow subscribers
12
+ # @param conflate [Boolean] keep only latest message per topic
13
+ # @param backend [Symbol, nil] :ruby (default) or :ffi
14
+ #
7
15
  def initialize(endpoints = nil, linger: 0, on_mute: :drop_newest, conflate: false, backend: nil)
8
16
  _init_engine(:PUB, linger: linger, on_mute: on_mute, conflate: conflate, backend: backend)
9
17
  _attach(endpoints, default: :bind)
10
18
  end
11
19
  end
12
20
 
21
+
13
22
  # SUB socket.
14
23
  #
15
24
  class SUB < Socket
@@ -19,6 +28,7 @@ module OMQ
19
28
  #
20
29
  EVERYTHING = ''
21
30
 
31
+
22
32
  # @param endpoints [String, nil]
23
33
  # @param linger [Integer]
24
34
  # @param subscribe [String, nil] subscription prefix; +nil+ (default)
@@ -31,6 +41,7 @@ module OMQ
31
41
  self.subscribe(subscribe) unless subscribe.nil?
32
42
  end
33
43
 
44
+
34
45
  # Subscribes to a topic prefix.
35
46
  #
36
47
  # @param prefix [String]
@@ -40,6 +51,7 @@ module OMQ
40
51
  @engine.routing.subscribe(prefix)
41
52
  end
42
53
 
54
+
43
55
  # Unsubscribes from a topic prefix.
44
56
  #
45
57
  # @param prefix [String]
@@ -50,16 +62,27 @@ module OMQ
50
62
  end
51
63
  end
52
64
 
65
+
66
+ # XPUB socket — like PUB but exposes subscription events to the application.
67
+ #
53
68
  class XPUB < Socket
54
69
  include Readable
55
70
  include Writable
56
71
 
72
+ # @param endpoints [String, nil] endpoint to bind/connect
73
+ # @param linger [Integer] linger period in seconds
74
+ # @param on_mute [Symbol] mute strategy for slow subscribers
75
+ # @param backend [Symbol, nil] :ruby (default) or :ffi
76
+ #
57
77
  def initialize(endpoints = nil, linger: 0, on_mute: :drop_newest, backend: nil)
58
78
  _init_engine(:XPUB, linger: linger, on_mute: on_mute, backend: backend)
59
79
  _attach(endpoints, default: :bind)
60
80
  end
61
81
  end
62
82
 
83
+
84
+ # XSUB socket — like SUB but subscriptions are sent as data frames.
85
+ #
63
86
  class XSUB < Socket
64
87
  include Readable
65
88
  include Writable
@@ -68,6 +91,8 @@ module OMQ
68
91
  # @param linger [Integer]
69
92
  # @param subscribe [String, nil] subscription prefix; +nil+ (default)
70
93
  # means no subscription — send a subscribe frame explicitly.
94
+ # @param on_mute [Symbol] mute strategy (:block, :drop_newest, :drop_oldest)
95
+ # @param backend [Symbol, nil] :ruby (default) or :ffi
71
96
  #
72
97
  def initialize(endpoints = nil, linger: 0, subscribe: nil, on_mute: :block, backend: nil)
73
98
  _init_engine(:XSUB, linger: linger, on_mute: on_mute, backend: backend)
data/lib/omq/push_pull.rb CHANGED
@@ -1,18 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OMQ
4
+ # PUSH socket — push messages to connected PULL peers via round-robin.
5
+ #
4
6
  class PUSH < Socket
5
7
  include Writable
6
8
 
9
+ # @param endpoints [String, nil] endpoint to bind/connect
10
+ # @param linger [Integer] linger period in seconds
11
+ # @param send_hwm [Integer, nil] send high water mark (nil uses default)
12
+ # @param send_timeout [Numeric, nil] send timeout in seconds
13
+ # @param backend [Symbol, nil] :ruby (default) or :ffi
14
+ #
7
15
  def initialize(endpoints = nil, linger: 0, send_hwm: nil, send_timeout: nil, backend: nil)
8
16
  _init_engine(:PUSH, linger: linger, send_hwm: send_hwm, send_timeout: send_timeout, backend: backend)
9
17
  _attach(endpoints, default: :connect)
10
18
  end
11
19
  end
12
20
 
21
+
22
+ # PULL socket — receive messages from PUSH peers via fair-queue.
23
+ #
13
24
  class PULL < Socket
14
25
  include Readable
15
26
 
27
+ # @param endpoints [String, nil] endpoint to bind/connect
28
+ # @param linger [Integer] linger period in seconds
29
+ # @param recv_hwm [Integer, nil] receive high water mark (nil uses default)
30
+ # @param recv_timeout [Numeric, nil] receive timeout in seconds
31
+ # @param backend [Symbol, nil] :ruby (default) or :ffi
32
+ #
16
33
  def initialize(endpoints = nil, linger: 0, recv_hwm: nil, recv_timeout: nil, backend: nil)
17
34
  _init_engine(:PULL, linger: linger, recv_hwm: recv_hwm, recv_timeout: recv_timeout, backend: backend)
18
35
  _attach(endpoints, default: :bind)
@@ -35,6 +35,7 @@ module OMQ
35
35
  dequeue(timeout: nil)
36
36
  end
37
37
 
38
+
38
39
  # Yields each received message until the socket is closed or
39
40
  # a receive timeout expires.
40
41
  #
data/lib/omq/readable.rb CHANGED
@@ -11,6 +11,7 @@ module OMQ
11
11
  # Maximum messages to prefetch from the recv queue per drain.
12
12
  RECV_BATCH_SIZE = 64
13
13
 
14
+
14
15
  # Receives the next message. Returns from a local prefetch
15
16
  # buffer when available, otherwise drains up to
16
17
  # {RECV_BATCH_SIZE} messages from the recv queue in one
@@ -23,6 +24,7 @@ module OMQ
23
24
  @recv_mutex.synchronize { @recv_buffer.shift } || fill_recv_buffer
24
25
  end
25
26
 
27
+
26
28
  # Waits until the socket is readable.
27
29
  #
28
30
  # @param timeout [Numeric, nil] timeout in seconds
data/lib/omq/req_rep.rb CHANGED
@@ -1,20 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OMQ
4
+ # REQ socket — send a request, then receive one reply (strict alternation).
5
+ #
4
6
  class REQ < Socket
5
7
  include Readable
6
8
  include Writable
7
9
 
10
+ # @param endpoints [String, nil] endpoint to bind/connect
11
+ # @param linger [Integer] linger period in seconds
12
+ # @param backend [Symbol, nil] :ruby (default) or :ffi
13
+ #
8
14
  def initialize(endpoints = nil, linger: 0, backend: nil)
9
15
  _init_engine(:REQ, linger: linger, backend: backend)
10
16
  _attach(endpoints, default: :connect)
11
17
  end
12
18
  end
13
19
 
20
+
21
+ # REP socket — receive a request, then send one reply (strict alternation).
22
+ #
14
23
  class REP < Socket
15
24
  include Readable
16
25
  include Writable
17
26
 
27
+ # @param endpoints [String, nil] endpoint to bind/connect
28
+ # @param linger [Integer] linger period in seconds
29
+ # @param backend [Symbol, nil] :ruby (default) or :ffi
30
+ #
18
31
  def initialize(endpoints = nil, linger: 0, backend: nil)
19
32
  _init_engine(:REP, linger: linger, backend: backend)
20
33
  _attach(endpoints, default: :bind)
@@ -1,27 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OMQ
4
+ # DEALER socket — asynchronous round-robin send, fair-queue receive.
5
+ #
4
6
  class DEALER < Socket
5
7
  include Readable
6
8
  include Writable
7
9
 
10
+ # @param endpoints [String, nil] endpoint to bind/connect
11
+ # @param linger [Integer] linger period in seconds
12
+ # @param backend [Symbol, nil] :ruby (default) or :ffi
13
+ #
8
14
  def initialize(endpoints = nil, linger: 0, backend: nil)
9
15
  _init_engine(:DEALER, linger: linger, backend: backend)
10
16
  _attach(endpoints, default: :connect)
11
17
  end
12
18
  end
13
19
 
20
+
14
21
  # ROUTER socket.
15
22
  #
16
23
  class ROUTER < Socket
17
24
  include Readable
18
25
  include Writable
19
26
 
27
+ # @param endpoints [String, nil] endpoint to bind/connect
28
+ # @param linger [Integer] linger period in seconds
29
+ # @param backend [Symbol, nil] :ruby (default) or :ffi
30
+ #
20
31
  def initialize(endpoints = nil, linger: 0, backend: nil)
21
32
  _init_engine(:ROUTER, linger: linger, backend: backend)
22
33
  _attach(endpoints, default: :bind)
23
34
  end
24
35
 
36
+
25
37
  # Sends a message to a specific peer by identity.
26
38
  #
27
39
  # @param receiver [String] peer identity
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Routing
5
+ # Starts a dedicated send pump for one per-connection send queue.
6
+ #
7
+ # Used by Router and Rep, which have per-connection queues but do not
8
+ # include the RoundRobin mixin.
9
+ #
10
+ module ConnSendPump
11
+ # Spawns the pump task and registers it in +tasks+.
12
+ #
13
+ # @param engine [Engine]
14
+ # @param conn [Connection]
15
+ # @param q [Async::LimitedQueue]
16
+ # @param tasks [Array]
17
+ # @return [Async::Task]
18
+ #
19
+ def self.start(engine, conn, q, tasks)
20
+ task = engine.spawn_pump_task(annotation: "send pump") do
21
+ loop do
22
+ batch = [q.dequeue]
23
+ Routing.drain_send_queue(q, batch)
24
+ batch.each { |parts| conn.write_message(parts) }
25
+ conn.flush
26
+ rescue Protocol::ZMTP::Error, *CONNECTION_LOST
27
+ engine.connection_lost(conn)
28
+ break
29
+ end
30
+ end
31
+ tasks << task
32
+ task
33
+ end
34
+ end
35
+ end
36
+ end
@@ -8,50 +8,55 @@ module OMQ
8
8
  #
9
9
  class Dealer
10
10
  include RoundRobin
11
+ include FairRecv
11
12
 
12
13
  # @param engine [Engine]
13
14
  #
14
15
  def initialize(engine)
15
16
  @engine = engine
16
- @recv_queue = Routing.build_queue(engine.options.recv_hwm, :block)
17
+ @recv_queue = FairQueue.new
17
18
  @tasks = []
18
19
  init_round_robin(engine)
19
20
  end
20
21
 
21
- # @return [Async::LimitedQueue]
22
+
23
+ # @return [FairQueue]
22
24
  #
23
- attr_reader :recv_queue, :send_queue
25
+ attr_reader :recv_queue
24
26
 
25
27
  # @param connection [Connection]
26
28
  #
27
29
  def connection_added(connection)
28
30
  @connections << connection
29
- signal_connection_available
30
- update_direct_pipe
31
- task = @engine.start_recv_pump(connection, @recv_queue)
32
- @tasks << task if task
33
- start_send_pump unless @send_pump_started
31
+ add_fair_recv_connection(connection)
32
+ add_round_robin_send_connection(connection)
34
33
  end
35
34
 
35
+
36
36
  # @param connection [Connection]
37
37
  #
38
38
  def connection_removed(connection)
39
39
  @connections.delete(connection)
40
- update_direct_pipe
40
+ @recv_queue.remove_queue(connection)
41
+ remove_round_robin_send_connection(connection)
41
42
  end
42
43
 
44
+
43
45
  # @param parts [Array<String>]
44
46
  #
45
47
  def enqueue(parts)
46
48
  enqueue_round_robin(parts)
47
49
  end
48
50
 
51
+
52
+ # Stops all background tasks.
53
+ #
54
+ # @return [void]
49
55
  #
50
56
  def stop
51
57
  @tasks.each(&:stop)
52
58
  @tasks.clear
53
59
  end
54
-
55
60
  end
56
61
  end
57
62
  end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Routing
5
+ # Per-connection recv queue aggregator.
6
+ #
7
+ # Maintains one bounded queue per connected peer. #dequeue
8
+ # returns the next available message from any peer in fair
9
+ # round-robin order, blocking until one arrives.
10
+ #
11
+ # Recv pumps do not enqueue directly — they write through a
12
+ # SignalingQueue wrapper, which also wakes a blocked #dequeue.
13
+ #
14
+ class FairQueue
15
+ # Creates an empty fair queue with no per-connection queues.
16
+ #
17
+ def initialize
18
+ @queues = [] # ordered list of per-connection inner queues
19
+ @mapping = {} # connection => inner queue
20
+ @cycle = @queues.cycle # live reference — sees adds/removes
21
+ @condition = Async::Condition.new
22
+ @pending = 0 # signals received before #dequeue waits
23
+ @closed = false
24
+ end
25
+
26
+
27
+ # Registers a per-connection queue. Called when a connection is added.
28
+ #
29
+ # @param conn [Connection]
30
+ # @param q [Async::LimitedQueue]
31
+ #
32
+ def add_queue(conn, q)
33
+ @mapping[conn] = q
34
+ @queues << q
35
+ end
36
+
37
+
38
+ # Removes the per-connection queue for a disconnected peer.
39
+ #
40
+ # If the queue is empty it is removed immediately. If it still has
41
+ # pending messages it is kept in @queues so the application can drain
42
+ # them via #dequeue; it will be cleaned up lazily by try_dequeue once
43
+ # it is empty.
44
+ #
45
+ # @param conn [Connection]
46
+ #
47
+ def remove_queue(conn)
48
+ q = @mapping.delete(conn)
49
+ return unless q
50
+ @queues.delete(q) if q.empty?
51
+ # Non-empty orphaned queues stay in @queues until drained
52
+ end
53
+
54
+
55
+ # Wakes a blocked #dequeue. Called by SignalingQueue after each enqueue.
56
+ #
57
+ def signal
58
+ @pending += 1
59
+ @condition.signal
60
+ end
61
+
62
+
63
+ # Returns the next message from any per-connection queue, in fair
64
+ # round-robin order. Blocks until a message is available.
65
+ #
66
+ # Pass +timeout: 0+ for a non-blocking poll (returns nil immediately
67
+ # if no messages are available).
68
+ #
69
+ # @param timeout [Numeric, nil] 0 = non-blocking, nil = block forever
70
+ # @return [Array<String>, nil]
71
+ #
72
+ def dequeue(timeout: nil)
73
+ return try_dequeue if timeout == 0
74
+
75
+ loop do
76
+ return nil if @closed && @queues.all?(&:empty?)
77
+
78
+ msg = try_dequeue
79
+ return msg if msg
80
+
81
+ if @pending > 0
82
+ @pending -= 1
83
+ next
84
+ end
85
+
86
+ @condition.wait
87
+ end
88
+ end
89
+
90
+
91
+ # Injects a nil sentinel to unblock a waiting #dequeue.
92
+ # Called by Engine on close or fatal error.
93
+ #
94
+ def push(nil_sentinel)
95
+ @closed = true
96
+ @condition.signal
97
+ end
98
+
99
+
100
+ # @return [Boolean]
101
+ #
102
+ def empty?
103
+ @queues.all?(&:empty?)
104
+ end
105
+
106
+ private
107
+
108
+ # Tries each per-connection queue once in round-robin order.
109
+ # Returns the first message found, or nil if all are empty.
110
+ # Lazily removes empty orphaned queues (disconnected peers that have
111
+ # been fully drained).
112
+ #
113
+ def try_dequeue
114
+ @queues.size.times do
115
+ q = begin
116
+ @cycle.next
117
+ rescue StopIteration
118
+ @cycle = @queues.cycle
119
+ break
120
+ end
121
+ msg = q.dequeue(timeout: 0)
122
+ return msg if msg
123
+ if q.empty? && !@mapping.value?(q)
124
+ @queues.delete(q)
125
+ break
126
+ end
127
+ end
128
+ nil
129
+ end
130
+ end
131
+
132
+
133
+ # Wraps a per-connection bounded queue so that each #enqueue also
134
+ # signals the FairQueue to wake a blocked #dequeue.
135
+ #
136
+ class SignalingQueue
137
+ # @param inner [Async::LimitedQueue] the per-connection bounded queue
138
+ # @param fair_queue [FairQueue] the parent fair queue to signal on enqueue
139
+ #
140
+ def initialize(inner, fair_queue)
141
+ @inner = inner
142
+ @fair = fair_queue
143
+ end
144
+
145
+
146
+ # Enqueues a message and signals the fair queue.
147
+ #
148
+ # @param msg [Array<String>]
149
+ # @return [void]
150
+ #
151
+ def enqueue(msg)
152
+ @inner.enqueue(msg)
153
+ @fair.signal
154
+ end
155
+
156
+
157
+ # @param timeout [Numeric, nil] dequeue timeout
158
+ # @return [Array<String>, nil]
159
+ #
160
+ def dequeue(timeout: nil) = @inner.dequeue(timeout: timeout)
161
+
162
+ # @return [Boolean]
163
+ #
164
+ def empty? = @inner.empty?
165
+
166
+ # @param item [Object, nil]
167
+ # @return [void]
168
+ #
169
+ def push(item) = @inner.push(item)
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OMQ
4
+ module Routing
5
+ # Mixin that adds per-connection recv queue setup for fair-queued sockets.
6
+ #
7
+ # Including classes must have @engine, @recv_queue (FairQueue), and @tasks.
8
+ #
9
+ module FairRecv
10
+ private
11
+
12
+ # Creates a per-connection recv queue, registers it with @recv_queue,
13
+ # and starts a recv pump for the connection. Called from #connection_added.
14
+ #
15
+ # @param conn [Connection]
16
+ # @yield [msg] optional per-message transform
17
+ #
18
+ def add_fair_recv_connection(conn, &transform)
19
+ conn_q = Routing.build_queue(@engine.options.recv_hwm, :block)
20
+ signaling = SignalingQueue.new(conn_q, @recv_queue)
21
+ @recv_queue.add_queue(conn, conn_q)
22
+ task = @engine.start_recv_pump(conn, signaling, &transform)
23
+ @tasks << task if task
24
+ end
25
+ end
26
+ end
27
+ end