omq 0.4.2 → 0.5.1
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 +4 -4
- data/CHANGELOG.md +46 -0
- data/README.md +6 -1
- data/lib/omq/channel.rb +14 -0
- data/lib/omq/client_server.rb +37 -0
- data/lib/omq/peer.rb +26 -0
- data/lib/omq/radio_dish.rb +74 -0
- data/lib/omq/scatter_gather.rb +23 -0
- data/lib/omq/version.rb +1 -1
- data/lib/omq/zmtp/codec/command.rb +18 -0
- data/lib/omq/zmtp/connection.rb +35 -9
- data/lib/omq/zmtp/reactor.rb +17 -33
- data/lib/omq/zmtp/routing/channel.rb +75 -0
- data/lib/omq/zmtp/routing/client.rb +56 -0
- data/lib/omq/zmtp/routing/dish.rb +80 -0
- data/lib/omq/zmtp/routing/fan_out.rb +20 -8
- data/lib/omq/zmtp/routing/gather.rb +48 -0
- data/lib/omq/zmtp/routing/pair.rb +13 -3
- data/lib/omq/zmtp/routing/peer.rb +93 -0
- data/lib/omq/zmtp/routing/push.rb +16 -0
- data/lib/omq/zmtp/routing/radio.rb +118 -0
- data/lib/omq/zmtp/routing/rep.rb +21 -6
- data/lib/omq/zmtp/routing/round_robin.rb +37 -2
- data/lib/omq/zmtp/routing/router.rb +23 -9
- data/lib/omq/zmtp/routing/scatter.rb +68 -0
- data/lib/omq/zmtp/routing/server.rb +93 -0
- data/lib/omq/zmtp/routing.rb +29 -2
- data/lib/omq/zmtp/single_frame.rb +20 -0
- data/lib/omq/zmtp/transport/inproc.rb +9 -1
- data/lib/omq/zmtp/valid_peers.rb +10 -2
- data/lib/omq/zmtp.rb +9 -0
- data/lib/omq.rb +5 -0
- metadata +15 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 76cecd3650f9fb73d699a027570421f589452a40cf6bbf207af02afb2bc6df08
|
|
4
|
+
data.tar.gz: 0252e01bacd5278defe46ed9ab4750b3e33ca71e928beb13f1092c23a7c2ce62
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: de2508cbe4e22c6dc93bdeab4626fed0c5280cf74eaaf4c4f886f566719a3c732f5db9b766c201f7e9d6eb7bb00ee4ed2d2392eec232abe88c7847148b889fc5
|
|
7
|
+
data.tar.gz: 65e96cef70697abe90588e5e35f4563e55ef183bb0be44276c6cb62ec007f6accb4e482a08c14d89b15ec3f542d75c78bacc87c6174ca6f4e69b588ddb540730
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.1 — 2026-03-28
|
|
4
|
+
|
|
5
|
+
### Improved
|
|
6
|
+
|
|
7
|
+
- **3–4x throughput under burst load** — send pumps now batch writes
|
|
8
|
+
before flushing. `Connection#write_message` buffers without flushing;
|
|
9
|
+
`Connection#flush` triggers the syscall. Pumps drain all queued messages
|
|
10
|
+
per cycle, reducing flush count from `N_msgs × N_conns` to `N_conns`
|
|
11
|
+
per batch. PUB/SUB TCP with 10 subscribers: 2.3k → 9.2k msg/s (**4x**).
|
|
12
|
+
PUSH/PULL TCP: 24k → 83k msg/s (**3.4x**). Zero overhead under light
|
|
13
|
+
load (batch of 1 = same path as before).
|
|
14
|
+
|
|
15
|
+
- **Simplified Reactor IO thread** — replaced `Thread::Queue` + `IO.pipe`
|
|
16
|
+
wake signal with a single `Async::Queue`. `Thread::Queue#pop` is
|
|
17
|
+
fiber-scheduler-aware in Ruby 4.0, so the pipe pair was unnecessary.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **`router_mandatory` SocketError raised in send pump** — the error
|
|
22
|
+
killed the pump fiber instead of reaching the caller. Now checked
|
|
23
|
+
synchronously in `enqueue` before queuing.
|
|
24
|
+
|
|
25
|
+
## 0.5.0 — 2026-03-28
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- **Draft socket types** (RFCs 41, 48, 49, 51, 52):
|
|
30
|
+
- `CLIENT`/`SERVER` — thread-safe REQ/REP without envelope, 4-byte routing IDs
|
|
31
|
+
- `RADIO`/`DISH` — group-based pub/sub with exact match, JOIN/LEAVE commands.
|
|
32
|
+
`radio.publish(group, body)`, `radio.send(body, group:)`, `radio << [group, body]`
|
|
33
|
+
- `SCATTER`/`GATHER` — thread-safe PUSH/PULL
|
|
34
|
+
- `PEER` — bidirectional multi-peer with 4-byte routing IDs
|
|
35
|
+
- `CHANNEL` — thread-safe PAIR
|
|
36
|
+
- All draft types enforce single-frame messages (no multipart)
|
|
37
|
+
- Reconnect-after-restart tests for all 10 socket type pairings
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **PUSH/SCATTER silently wrote to dead peers** — write-only sockets had
|
|
42
|
+
no recv pump to detect peer disconnection. Writes succeeded because the
|
|
43
|
+
kernel send buffer absorbed the data, preventing reconnect from
|
|
44
|
+
triggering. Added background monitor task per connection.
|
|
45
|
+
- **PAIR/CHANNEL stale send pump after reconnect** — old send pump kept
|
|
46
|
+
its captured connection reference and raced with the new send pump,
|
|
47
|
+
sending to the dead connection. Now stopped in `connection_removed`.
|
|
48
|
+
|
|
3
49
|
## 0.4.2 — 2026-03-27
|
|
4
50
|
|
|
5
51
|
### Fixed
|
data/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Pure Ruby implementation of the [ZMTP 3.1](https://rfc.zeromq.org/spec/23/) wire
|
|
|
18
18
|
## Highlights
|
|
19
19
|
|
|
20
20
|
- **Pure Ruby** — no C extensions, no FFI, no libzmq/libczmq dependency
|
|
21
|
-
- **All socket types** — req/rep, pub/sub, push/pull, dealer/router, xpub/xsub, pair
|
|
21
|
+
- **All socket types** — req/rep, pub/sub, push/pull, dealer/router, xpub/xsub, pair + draft types (client/server, radio/dish, scatter/gather, peer, channel)
|
|
22
22
|
- **Async-native** — built on [Async](https://github.com/socketry/async) fibers, also works with plain threads
|
|
23
23
|
- **Ruby-idiomatic API** — messages as `Array<String>`, errors as exceptions, timeouts as `IO::TimeoutError`
|
|
24
24
|
- **All transports** — tcp, ipc, inproc
|
|
@@ -112,6 +112,11 @@ end
|
|
|
112
112
|
| Pipeline | `PUSH`, `PULL` | unidirectional |
|
|
113
113
|
| Routing | `DEALER`, `ROUTER` | bidirectional |
|
|
114
114
|
| Exclusive pair | `PAIR` | bidirectional |
|
|
115
|
+
| Client/Server | `CLIENT`, `SERVER` | bidirectional |
|
|
116
|
+
| Group messaging | `RADIO`, `DISH` | unidirectional |
|
|
117
|
+
| Pipeline (draft) | `SCATTER`, `GATHER` | unidirectional |
|
|
118
|
+
| Peer-to-peer | `PEER` | bidirectional |
|
|
119
|
+
| Channel (draft) | `CHANNEL` | bidirectional |
|
|
115
120
|
|
|
116
121
|
All classes live under `OMQ::`. For the purists, `ØMQ` is an alias:
|
|
117
122
|
|
data/lib/omq/channel.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
class CHANNEL < Socket
|
|
5
|
+
include ZMTP::Readable
|
|
6
|
+
include ZMTP::Writable
|
|
7
|
+
include ZMTP::SingleFrame
|
|
8
|
+
|
|
9
|
+
def initialize(endpoints = nil, linger: 0)
|
|
10
|
+
_init_engine(:CHANNEL, linger: linger)
|
|
11
|
+
_attach(endpoints, default: :connect)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
class CLIENT < Socket
|
|
5
|
+
include ZMTP::Readable
|
|
6
|
+
include ZMTP::Writable
|
|
7
|
+
include ZMTP::SingleFrame
|
|
8
|
+
|
|
9
|
+
def initialize(endpoints = nil, linger: 0)
|
|
10
|
+
_init_engine(:CLIENT, linger: linger)
|
|
11
|
+
_attach(endpoints, default: :connect)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class SERVER < Socket
|
|
16
|
+
include ZMTP::Readable
|
|
17
|
+
include ZMTP::Writable
|
|
18
|
+
include ZMTP::SingleFrame
|
|
19
|
+
|
|
20
|
+
def initialize(endpoints = nil, linger: 0)
|
|
21
|
+
_init_engine(:SERVER, linger: linger)
|
|
22
|
+
_attach(endpoints, default: :bind)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Sends a message to a specific peer by routing ID.
|
|
26
|
+
#
|
|
27
|
+
# @param routing_id [String] 4-byte routing ID
|
|
28
|
+
# @param message [String] message body
|
|
29
|
+
# @return [self]
|
|
30
|
+
#
|
|
31
|
+
def send_to(routing_id, message)
|
|
32
|
+
parts = [routing_id.b.freeze, message.b.freeze]
|
|
33
|
+
with_timeout(@options.write_timeout) { @engine.enqueue_send(parts) }
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/omq/peer.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
class PEER < Socket
|
|
5
|
+
include ZMTP::Readable
|
|
6
|
+
include ZMTP::Writable
|
|
7
|
+
include ZMTP::SingleFrame
|
|
8
|
+
|
|
9
|
+
def initialize(endpoints = nil, linger: 0)
|
|
10
|
+
_init_engine(:PEER, linger: linger)
|
|
11
|
+
_attach(endpoints, default: :connect)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Sends a message to a specific peer by routing ID.
|
|
15
|
+
#
|
|
16
|
+
# @param routing_id [String] 4-byte routing ID
|
|
17
|
+
# @param message [String] message body
|
|
18
|
+
# @return [self]
|
|
19
|
+
#
|
|
20
|
+
def send_to(routing_id, message)
|
|
21
|
+
parts = [routing_id.b.freeze, message.b.freeze]
|
|
22
|
+
with_timeout(@options.write_timeout) { @engine.enqueue_send(parts) }
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
class RADIO < Socket
|
|
5
|
+
include ZMTP::Writable
|
|
6
|
+
|
|
7
|
+
def initialize(endpoints = nil, linger: 0)
|
|
8
|
+
_init_engine(:RADIO, linger: linger)
|
|
9
|
+
_attach(endpoints, default: :bind)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Publishes a message to a group.
|
|
13
|
+
#
|
|
14
|
+
# @param group [String] group name
|
|
15
|
+
# @param body [String] message body
|
|
16
|
+
# @return [self]
|
|
17
|
+
#
|
|
18
|
+
def publish(group, body)
|
|
19
|
+
with_timeout(@options.write_timeout) do
|
|
20
|
+
@engine.enqueue_send([group.b.freeze, body.b.freeze])
|
|
21
|
+
end
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Sends a message to a group.
|
|
26
|
+
#
|
|
27
|
+
# @param message [String] message body (requires group: kwarg)
|
|
28
|
+
# @param group [String] group name
|
|
29
|
+
# @return [self]
|
|
30
|
+
#
|
|
31
|
+
def send(message, group: nil)
|
|
32
|
+
raise ArgumentError, "RADIO requires a group (use group: kwarg, publish, or << [group, body])" unless group
|
|
33
|
+
publish(group, message)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Sends a message to a group via [group, body] array.
|
|
37
|
+
#
|
|
38
|
+
# @param message [Array<String>] [group, body]
|
|
39
|
+
# @return [self]
|
|
40
|
+
#
|
|
41
|
+
def <<(message)
|
|
42
|
+
raise ArgumentError, "RADIO requires [group, body] array" unless message.is_a?(Array) && message.size == 2
|
|
43
|
+
publish(message[0], message[1])
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class DISH < Socket
|
|
48
|
+
include ZMTP::Readable
|
|
49
|
+
|
|
50
|
+
def initialize(endpoints = nil, linger: 0, group: nil)
|
|
51
|
+
_init_engine(:DISH, linger: linger)
|
|
52
|
+
_attach(endpoints, default: :connect)
|
|
53
|
+
join(group) if group
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Joins a group.
|
|
57
|
+
#
|
|
58
|
+
# @param group [String]
|
|
59
|
+
# @return [void]
|
|
60
|
+
#
|
|
61
|
+
def join(group)
|
|
62
|
+
@engine.routing.join(group)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Leaves a group.
|
|
66
|
+
#
|
|
67
|
+
# @param group [String]
|
|
68
|
+
# @return [void]
|
|
69
|
+
#
|
|
70
|
+
def leave(group)
|
|
71
|
+
@engine.routing.leave(group)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
class SCATTER < Socket
|
|
5
|
+
include ZMTP::Writable
|
|
6
|
+
include ZMTP::SingleFrame
|
|
7
|
+
|
|
8
|
+
def initialize(endpoints = nil, linger: 0, send_hwm: nil, send_timeout: nil)
|
|
9
|
+
_init_engine(:SCATTER, linger: linger, send_hwm: send_hwm, send_timeout: send_timeout)
|
|
10
|
+
_attach(endpoints, default: :connect)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class GATHER < Socket
|
|
15
|
+
include ZMTP::Readable
|
|
16
|
+
include ZMTP::SingleFrame
|
|
17
|
+
|
|
18
|
+
def initialize(endpoints = nil, linger: 0, recv_hwm: nil, recv_timeout: nil)
|
|
19
|
+
_init_engine(:GATHER, linger: linger, recv_hwm: recv_hwm, recv_timeout: recv_timeout)
|
|
20
|
+
_attach(endpoints, default: :bind)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/omq/version.rb
CHANGED
|
@@ -101,6 +101,24 @@ module OMQ
|
|
|
101
101
|
new("CANCEL", prefix.b)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
+
# Builds a JOIN command (RADIO/DISH group subscription).
|
|
105
|
+
#
|
|
106
|
+
# @param group [String] group name
|
|
107
|
+
# @return [Command]
|
|
108
|
+
#
|
|
109
|
+
def self.join(group)
|
|
110
|
+
new("JOIN", group.b)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Builds a LEAVE command (RADIO/DISH group unsubscription).
|
|
114
|
+
#
|
|
115
|
+
# @param group [String] group name
|
|
116
|
+
# @return [Command]
|
|
117
|
+
#
|
|
118
|
+
def self.leave(group)
|
|
119
|
+
new("LEAVE", group.b)
|
|
120
|
+
end
|
|
121
|
+
|
|
104
122
|
# Builds a PING command.
|
|
105
123
|
#
|
|
106
124
|
# @param ttl [Numeric] time-to-live in seconds (sent as deciseconds)
|
data/lib/omq/zmtp/connection.rb
CHANGED
|
@@ -77,21 +77,36 @@ module OMQ
|
|
|
77
77
|
end
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
-
# Sends a multi-frame message.
|
|
80
|
+
# Sends a multi-frame message (write + flush).
|
|
81
81
|
#
|
|
82
82
|
# @param parts [Array<String>] message frames
|
|
83
83
|
# @return [void]
|
|
84
84
|
#
|
|
85
85
|
def send_message(parts)
|
|
86
86
|
@mutex.synchronize do
|
|
87
|
-
parts
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
87
|
+
write_frames(parts)
|
|
88
|
+
@io.flush
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Writes a multi-frame message to the buffer without flushing.
|
|
93
|
+
# Call {#flush} after batching writes.
|
|
94
|
+
#
|
|
95
|
+
# @param parts [Array<String>] message frames
|
|
96
|
+
# @return [void]
|
|
97
|
+
#
|
|
98
|
+
def write_message(parts)
|
|
99
|
+
@mutex.synchronize do
|
|
100
|
+
write_frames(parts)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Flushes the write buffer to the underlying IO.
|
|
105
|
+
#
|
|
106
|
+
# @return [void]
|
|
107
|
+
#
|
|
108
|
+
def flush
|
|
109
|
+
@mutex.synchronize do
|
|
95
110
|
@io.flush
|
|
96
111
|
end
|
|
97
112
|
end
|
|
@@ -213,6 +228,17 @@ module OMQ
|
|
|
213
228
|
|
|
214
229
|
private
|
|
215
230
|
|
|
231
|
+
def write_frames(parts)
|
|
232
|
+
parts.each_with_index do |part, i|
|
|
233
|
+
more = i < parts.size - 1
|
|
234
|
+
if @mechanism.encrypted?
|
|
235
|
+
@io.write(@mechanism.encrypt(part.b, more: more))
|
|
236
|
+
else
|
|
237
|
+
@io.write(Codec::Frame.new(part, more: more).to_wire)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
216
242
|
def touch_heartbeat
|
|
217
243
|
@last_received_at = monotonic_now if @heartbeat_interval
|
|
218
244
|
end
|
data/lib/omq/zmtp/reactor.rb
CHANGED
|
@@ -12,11 +12,9 @@ module OMQ
|
|
|
12
12
|
# tasks — mirroring libzmq's IO thread architecture.
|
|
13
13
|
#
|
|
14
14
|
module Reactor
|
|
15
|
-
@work_queue =
|
|
15
|
+
@work_queue = Async::Queue.new
|
|
16
16
|
@thread = nil
|
|
17
17
|
@mutex = Mutex.new
|
|
18
|
-
@wake_r = nil
|
|
19
|
-
@wake_w = nil
|
|
20
18
|
|
|
21
19
|
class << self
|
|
22
20
|
# Spawns a pump task (recv loop, send loop, accept loop).
|
|
@@ -33,7 +31,6 @@ module OMQ
|
|
|
33
31
|
handle = PumpHandle.new
|
|
34
32
|
ensure_started
|
|
35
33
|
@work_queue.push([:spawn, block, handle])
|
|
36
|
-
@wake_w.write_nonblock(".") rescue nil
|
|
37
34
|
handle
|
|
38
35
|
end
|
|
39
36
|
end
|
|
@@ -52,7 +49,6 @@ module OMQ
|
|
|
52
49
|
result_queue = Thread::Queue.new
|
|
53
50
|
ensure_started
|
|
54
51
|
@work_queue.push([:run, block, result_queue])
|
|
55
|
-
@wake_w.write_nonblock(".") rescue nil
|
|
56
52
|
status, value = result_queue.pop
|
|
57
53
|
raise value if status == :error
|
|
58
54
|
value
|
|
@@ -66,9 +62,8 @@ module OMQ
|
|
|
66
62
|
def ensure_started
|
|
67
63
|
@mutex.synchronize do
|
|
68
64
|
return if @thread&.alive?
|
|
69
|
-
@wake_r, @wake_w = IO.pipe
|
|
70
65
|
ready = Thread::Queue.new
|
|
71
|
-
@thread = Thread.new { run_reactor(ready
|
|
66
|
+
@thread = Thread.new { run_reactor(ready) }
|
|
72
67
|
@thread.name = "omq-io"
|
|
73
68
|
ready.pop
|
|
74
69
|
end
|
|
@@ -80,42 +75,31 @@ module OMQ
|
|
|
80
75
|
#
|
|
81
76
|
def stop!
|
|
82
77
|
@work_queue.push([:stop])
|
|
83
|
-
@wake_w&.write_nonblock(".") rescue nil
|
|
84
78
|
@thread&.join(2)
|
|
85
79
|
@thread = nil
|
|
86
|
-
@wake_r&.close rescue nil
|
|
87
|
-
@wake_w&.close rescue nil
|
|
88
|
-
@wake_r = nil
|
|
89
|
-
@wake_w = nil
|
|
90
80
|
end
|
|
91
81
|
|
|
92
82
|
private
|
|
93
83
|
|
|
94
|
-
def run_reactor(ready
|
|
84
|
+
def run_reactor(ready)
|
|
95
85
|
Async do |task|
|
|
96
86
|
ready.push(true)
|
|
97
87
|
loop do
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
_, block, result_queue = item
|
|
111
|
-
task.async do
|
|
112
|
-
result_queue.push([:ok, block.call])
|
|
113
|
-
rescue => e
|
|
114
|
-
result_queue.push([:error, e])
|
|
115
|
-
end
|
|
116
|
-
when :stop
|
|
117
|
-
return
|
|
88
|
+
item = @work_queue.dequeue
|
|
89
|
+
case item[0]
|
|
90
|
+
when :spawn
|
|
91
|
+
_, block, handle = item
|
|
92
|
+
async_task = task.async(transient: true, &block)
|
|
93
|
+
handle.task = async_task
|
|
94
|
+
when :run
|
|
95
|
+
_, block, result_queue = item
|
|
96
|
+
task.async do
|
|
97
|
+
result_queue.push([:ok, block.call])
|
|
98
|
+
rescue => e
|
|
99
|
+
result_queue.push([:error, e])
|
|
118
100
|
end
|
|
101
|
+
when :stop
|
|
102
|
+
return
|
|
119
103
|
end
|
|
120
104
|
end
|
|
121
105
|
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module ZMTP
|
|
5
|
+
module Routing
|
|
6
|
+
# CHANNEL socket routing: exclusive 1-to-1 bidirectional.
|
|
7
|
+
#
|
|
8
|
+
class Channel
|
|
9
|
+
|
|
10
|
+
# @param engine [Engine]
|
|
11
|
+
#
|
|
12
|
+
def initialize(engine)
|
|
13
|
+
@engine = engine
|
|
14
|
+
@connection = nil
|
|
15
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
16
|
+
@send_queue = Async::LimitedQueue.new(engine.options.send_hwm)
|
|
17
|
+
@tasks = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @return [Async::LimitedQueue]
|
|
21
|
+
#
|
|
22
|
+
attr_reader :recv_queue, :send_queue
|
|
23
|
+
|
|
24
|
+
# @param connection [Connection]
|
|
25
|
+
# @raise [RuntimeError] if a connection already exists
|
|
26
|
+
#
|
|
27
|
+
def connection_added(connection)
|
|
28
|
+
raise "CHANNEL allows only one peer" if @connection
|
|
29
|
+
@connection = connection
|
|
30
|
+
task = @engine.start_recv_pump(connection, @recv_queue)
|
|
31
|
+
@tasks << task if task
|
|
32
|
+
start_send_pump(connection)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @param connection [Connection]
|
|
36
|
+
#
|
|
37
|
+
def connection_removed(connection)
|
|
38
|
+
if @connection == connection
|
|
39
|
+
@connection = nil
|
|
40
|
+
@send_pump&.stop
|
|
41
|
+
@send_pump = nil
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @param parts [Array<String>]
|
|
46
|
+
#
|
|
47
|
+
def enqueue(parts)
|
|
48
|
+
@send_queue.enqueue(parts)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
#
|
|
52
|
+
def stop
|
|
53
|
+
@tasks.each(&:stop)
|
|
54
|
+
@tasks.clear
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def start_send_pump(conn)
|
|
60
|
+
@send_pump = Reactor.spawn_pump do
|
|
61
|
+
loop do
|
|
62
|
+
batch = [@send_queue.dequeue]
|
|
63
|
+
Routing.drain_send_queue(@send_queue, batch)
|
|
64
|
+
batch.each { |parts| conn.write_message(parts) }
|
|
65
|
+
conn.flush
|
|
66
|
+
end
|
|
67
|
+
rescue *ZMTP::CONNECTION_LOST
|
|
68
|
+
@engine.connection_lost(conn)
|
|
69
|
+
end
|
|
70
|
+
@tasks << @send_pump
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module ZMTP
|
|
5
|
+
module Routing
|
|
6
|
+
# CLIENT socket routing: round-robin send, fair-queue receive.
|
|
7
|
+
#
|
|
8
|
+
# Same as DEALER — no envelope manipulation.
|
|
9
|
+
#
|
|
10
|
+
class Client
|
|
11
|
+
include RoundRobin
|
|
12
|
+
|
|
13
|
+
# @param engine [Engine]
|
|
14
|
+
#
|
|
15
|
+
def initialize(engine)
|
|
16
|
+
@engine = engine
|
|
17
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
18
|
+
@tasks = []
|
|
19
|
+
init_round_robin(engine)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Async::LimitedQueue]
|
|
23
|
+
#
|
|
24
|
+
attr_reader :recv_queue, :send_queue
|
|
25
|
+
|
|
26
|
+
# @param connection [Connection]
|
|
27
|
+
#
|
|
28
|
+
def connection_added(connection)
|
|
29
|
+
@connections << connection
|
|
30
|
+
signal_connection_available
|
|
31
|
+
task = @engine.start_recv_pump(connection, @recv_queue)
|
|
32
|
+
@tasks << task if task
|
|
33
|
+
start_send_pump unless @send_pump_started
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @param connection [Connection]
|
|
37
|
+
#
|
|
38
|
+
def connection_removed(connection)
|
|
39
|
+
@connections.delete(connection)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @param parts [Array<String>]
|
|
43
|
+
#
|
|
44
|
+
def enqueue(parts)
|
|
45
|
+
@send_queue.enqueue(parts)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
#
|
|
49
|
+
def stop
|
|
50
|
+
@tasks.each(&:stop)
|
|
51
|
+
@tasks.clear
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OMQ
|
|
4
|
+
module ZMTP
|
|
5
|
+
module Routing
|
|
6
|
+
# DISH socket routing: group-based receive from RADIO peers.
|
|
7
|
+
#
|
|
8
|
+
# Sends JOIN/LEAVE commands to connected RADIO peers.
|
|
9
|
+
# Receives two-frame messages (group + body) from RADIO.
|
|
10
|
+
#
|
|
11
|
+
class Dish
|
|
12
|
+
|
|
13
|
+
# @param engine [Engine]
|
|
14
|
+
#
|
|
15
|
+
def initialize(engine)
|
|
16
|
+
@engine = engine
|
|
17
|
+
@connections = []
|
|
18
|
+
@recv_queue = Async::LimitedQueue.new(engine.options.recv_hwm)
|
|
19
|
+
@groups = Set.new
|
|
20
|
+
@tasks = []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Async::LimitedQueue]
|
|
24
|
+
#
|
|
25
|
+
attr_reader :recv_queue
|
|
26
|
+
|
|
27
|
+
# @param connection [Connection]
|
|
28
|
+
#
|
|
29
|
+
def connection_added(connection)
|
|
30
|
+
@connections << connection
|
|
31
|
+
# Send existing group memberships to new peer
|
|
32
|
+
@groups.each do |group|
|
|
33
|
+
connection.send_command(Codec::Command.join(group))
|
|
34
|
+
end
|
|
35
|
+
task = @engine.start_recv_pump(connection, @recv_queue)
|
|
36
|
+
@tasks << task if task
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @param connection [Connection]
|
|
40
|
+
#
|
|
41
|
+
def connection_removed(connection)
|
|
42
|
+
@connections.delete(connection)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# DISH is read-only.
|
|
46
|
+
#
|
|
47
|
+
def enqueue(_parts)
|
|
48
|
+
raise "DISH sockets cannot send"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Joins a group.
|
|
52
|
+
#
|
|
53
|
+
# @param group [String]
|
|
54
|
+
#
|
|
55
|
+
def join(group)
|
|
56
|
+
@groups << group
|
|
57
|
+
@connections.each do |conn|
|
|
58
|
+
conn.send_command(Codec::Command.join(group))
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Leaves a group.
|
|
63
|
+
#
|
|
64
|
+
# @param group [String]
|
|
65
|
+
#
|
|
66
|
+
def leave(group)
|
|
67
|
+
@groups.delete(group)
|
|
68
|
+
@connections.each do |conn|
|
|
69
|
+
conn.send_command(Codec::Command.leave(group))
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def stop
|
|
74
|
+
@tasks.each(&:stop)
|
|
75
|
+
@tasks.clear
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|