zzq 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +35 -0
- data/LICENSE-APACHE +202 -0
- data/LICENSE-ISC +15 -0
- data/README.md +67 -122
- data/ext/zzq_native/Cargo.toml +35 -0
- data/ext/zzq_native/extconf.rb +8 -0
- data/ext/zzq_native/src/broker.rs +163 -0
- data/ext/zzq_native/src/client.rs +294 -0
- data/ext/zzq_native/src/context.rs +51 -0
- data/ext/zzq_native/src/error.rs +117 -0
- data/ext/zzq_native/src/lib.rs +52 -0
- data/ext/zzq_native/src/op.rs +140 -0
- data/ext/zzq_native/src/options.rs +168 -0
- data/ext/zzq_native/src/rb.rs +424 -0
- data/ext/zzq_native/src/wake.rs +155 -0
- data/lib/zzq/broker.rb +90 -21
- data/lib/zzq/client.rb +202 -35
- data/lib/zzq/errors.rb +37 -0
- data/lib/zzq/message.rb +16 -107
- data/lib/zzq/options.rb +27 -86
- data/lib/zzq/version.rb +2 -1
- data/lib/zzq/waiting.rb +65 -0
- data/lib/zzq.rb +22 -19
- metadata +33 -77
- data/LICENSE +0 -13
- data/lib/zzq/engine/connection_lifecycle.rb +0 -139
- data/lib/zzq/engine/keepalive.rb +0 -75
- data/lib/zzq/engine/socket_lifecycle.rb +0 -79
- data/lib/zzq/engine.rb +0 -163
- data/lib/zzq/error.rb +0 -42
- data/lib/zzq/monitor_event.rb +0 -14
- data/lib/zzq/persistence/interface.rb +0 -43
- data/lib/zzq/persistence/memory.rb +0 -37
- data/lib/zzq/persistence/pstore.rb +0 -66
- data/lib/zzq/routing/broker.rb +0 -380
- data/lib/zzq/routing/client.rb +0 -327
- data/lib/zzq/routing/packet_id_allocator.rb +0 -54
- data/lib/zzq/routing/qos_tracker.rb +0 -108
- data/lib/zzq/routing/retained_store.rb +0 -67
- data/lib/zzq/routing/topic_match.rb +0 -49
- data/lib/zzq/routing/topic_trie.rb +0 -232
- data/lib/zzq/socket.rb +0 -146
- data/lib/zzq/subscription.rb +0 -70
- data/lib/zzq/transport/ipc.rb +0 -92
- data/lib/zzq/transport/mqtt.rb +0 -83
- data/lib/zzq/transport/mqtts.rb +0 -173
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "topic_match"
|
|
4
|
-
|
|
5
|
-
module ZZQ
|
|
6
|
-
module Routing
|
|
7
|
-
# Per-level topic-filter trie. Each node holds literal children,
|
|
8
|
-
# one +wildcard ("+") child, and one hash-wildcard ("#") child.
|
|
9
|
-
# Subscriptions are stored at the terminal node of their filter.
|
|
10
|
-
#
|
|
11
|
-
# Matches are collected by walking a topic down the trie, traversing
|
|
12
|
-
# literal + '+' branches in parallel and collecting '#' subtree
|
|
13
|
-
# entries at every step. Honours the $SYS-vs-bare-wildcard rule
|
|
14
|
-
# (MQTT-4.7.2-1): filters starting with `+` or `#` at the root do
|
|
15
|
-
# not match topics starting with `$SYS`.
|
|
16
|
-
#
|
|
17
|
-
# Shared-subscription filters ("$share/<group>/<filter>") are
|
|
18
|
-
# stored with their group stripped off but tagged with the group
|
|
19
|
-
# name on each SubscriberEntry; fan-out chooses exactly one entry
|
|
20
|
-
# per group (MVP: round-robin).
|
|
21
|
-
#
|
|
22
|
-
# Not thread-safe — a broker-owned trie is mutated from the broker
|
|
23
|
-
# fibers only.
|
|
24
|
-
class TopicTrie
|
|
25
|
-
SubscriberEntry = Struct.new(:session, :entry, :share_group, keyword_init: true) do
|
|
26
|
-
# Forward-compat shorthand for fan-out.
|
|
27
|
-
def filter = entry[:filter]
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class Node
|
|
32
|
-
attr_accessor :plus_child, :hash_child
|
|
33
|
-
attr_reader :children, :entries
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def initialize
|
|
37
|
-
@children = {}
|
|
38
|
-
@plus_child = nil
|
|
39
|
-
@hash_child = nil
|
|
40
|
-
@entries = []
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def empty?
|
|
45
|
-
@children.empty? && @plus_child.nil? && @hash_child.nil? && @entries.empty?
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def initialize
|
|
51
|
-
@root = Node.new
|
|
52
|
-
# Round-robin cursor for shared groups: group_key => index.
|
|
53
|
-
@share_cursor = Hash.new(0)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
# Insert a subscription. +filter+ may be a plain MQTT filter
|
|
58
|
-
# ("a/+/c", "#", "$SYS/b/+") or a shared-subscription filter
|
|
59
|
-
# ("$share/group/a/+/c").
|
|
60
|
-
def add(filter, session, entry)
|
|
61
|
-
real_filter, group = strip_share(filter)
|
|
62
|
-
node, _segs = walk_or_create(real_filter)
|
|
63
|
-
node.entries << SubscriberEntry.new(session: session, entry: entry, share_group: group)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# Remove the entry for (session, filter). Returns true if found.
|
|
68
|
-
def remove(filter, session)
|
|
69
|
-
real_filter, _group = strip_share(filter)
|
|
70
|
-
segs = real_filter.split("/", -1)
|
|
71
|
-
path = [@root]
|
|
72
|
-
segs.each do |seg|
|
|
73
|
-
node = path.last
|
|
74
|
-
nxt =
|
|
75
|
-
case seg
|
|
76
|
-
when "+" then node.plus_child
|
|
77
|
-
when "#" then node.hash_child
|
|
78
|
-
else node.children[seg]
|
|
79
|
-
end
|
|
80
|
-
return false unless nxt
|
|
81
|
-
path << nxt
|
|
82
|
-
end
|
|
83
|
-
leaf = path.last
|
|
84
|
-
before = leaf.entries.size
|
|
85
|
-
leaf.entries.reject! { |e| e.session.equal?(session) && e.entry[:filter] == filter }
|
|
86
|
-
return false if leaf.entries.size == before
|
|
87
|
-
prune(path, segs)
|
|
88
|
-
true
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
# Drop every entry belonging to +session+, pruning empty nodes.
|
|
93
|
-
def remove_session(session)
|
|
94
|
-
walk_prune(@root, []) { |e| e.session.equal?(session) }
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
# Return the matching subscriber entries for +topic+. One entry
|
|
99
|
-
# per (session, filter); shared-group collapse is performed here
|
|
100
|
-
# so fan-out sees exactly one delivery per $share group.
|
|
101
|
-
def match(topic)
|
|
102
|
-
segs = topic.split("/", -1)
|
|
103
|
-
collected = []
|
|
104
|
-
traverse(@root, segs, 0, topic.start_with?("$"), collected)
|
|
105
|
-
collapse_shared(collected)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def empty? = @root.empty?
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
private
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
SHARE_PREFIX = "$share/"
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def strip_share(filter)
|
|
119
|
-
return [filter, nil] unless filter.start_with?(SHARE_PREFIX)
|
|
120
|
-
rest = filter[SHARE_PREFIX.length..]
|
|
121
|
-
slash = rest.index("/") or raise ArgumentError, "malformed $share filter: #{filter}"
|
|
122
|
-
group = rest[0, slash]
|
|
123
|
-
real = rest[(slash + 1)..]
|
|
124
|
-
raise ArgumentError, "$share filter has empty group: #{filter}" if group.empty?
|
|
125
|
-
raise ArgumentError, "$share filter has empty inner filter: #{filter}" if real.empty?
|
|
126
|
-
[real, group]
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def walk_or_create(filter)
|
|
131
|
-
segs = filter.split("/", -1)
|
|
132
|
-
node = @root
|
|
133
|
-
segs.each do |seg|
|
|
134
|
-
node =
|
|
135
|
-
case seg
|
|
136
|
-
when "+"
|
|
137
|
-
node.plus_child ||= Node.new
|
|
138
|
-
when "#"
|
|
139
|
-
node.hash_child ||= Node.new
|
|
140
|
-
else
|
|
141
|
-
node.children[seg] ||= Node.new
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
[node, segs]
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def prune(path, segs)
|
|
149
|
-
# path[i] is the node reached after segs[0..i-1]; path[0] = root.
|
|
150
|
-
(segs.length - 1).downto(0) do |i|
|
|
151
|
-
parent = path[i]
|
|
152
|
-
child = path[i + 1]
|
|
153
|
-
next unless child.empty?
|
|
154
|
-
seg = segs[i]
|
|
155
|
-
case seg
|
|
156
|
-
when "+" then parent.plus_child = nil
|
|
157
|
-
when "#" then parent.hash_child = nil
|
|
158
|
-
else parent.children.delete(seg)
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def walk_prune(node, path_segs, &block)
|
|
165
|
-
node.entries.reject!(&block)
|
|
166
|
-
node.children.each do |seg, child|
|
|
167
|
-
walk_prune(child, path_segs + [seg], &block)
|
|
168
|
-
node.children.delete(seg) if child.empty?
|
|
169
|
-
end
|
|
170
|
-
if node.plus_child
|
|
171
|
-
walk_prune(node.plus_child, path_segs + ["+"], &block)
|
|
172
|
-
node.plus_child = nil if node.plus_child.empty?
|
|
173
|
-
end
|
|
174
|
-
if node.hash_child
|
|
175
|
-
walk_prune(node.hash_child, path_segs + ["#"], &block)
|
|
176
|
-
node.hash_child = nil if node.hash_child.empty?
|
|
177
|
-
end
|
|
178
|
-
end
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
# Walk all trie branches compatible with segs[i..]. Collect
|
|
182
|
-
# entries at every node that marks a terminal filter. Rules:
|
|
183
|
-
# * +#+ at a node matches the rest of the topic (including zero
|
|
184
|
-
# more segments).
|
|
185
|
-
# * MQTT-4.7.2-1: filters starting with +/# at the root don't
|
|
186
|
-
# match topics starting with $.
|
|
187
|
-
def traverse(node, segs, i, sys_topic, out)
|
|
188
|
-
at_root = i.zero?
|
|
189
|
-
|
|
190
|
-
if node.hash_child && !(at_root && sys_topic)
|
|
191
|
-
out.concat(node.hash_child.entries)
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
if i == segs.length
|
|
195
|
-
out.concat(node.entries)
|
|
196
|
-
return
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
seg = segs[i]
|
|
200
|
-
|
|
201
|
-
if (lit = node.children[seg])
|
|
202
|
-
traverse(lit, segs, i + 1, sys_topic, out)
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
if (plus = node.plus_child) && !(at_root && sys_topic)
|
|
206
|
-
traverse(plus, segs, i + 1, sys_topic, out)
|
|
207
|
-
end
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
# For shared subscriptions: group matches by (group, filter),
|
|
212
|
-
# pick one member round-robin; unshared entries pass through.
|
|
213
|
-
def collapse_shared(entries)
|
|
214
|
-
return entries if entries.all? { |e| e.share_group.nil? }
|
|
215
|
-
out = []
|
|
216
|
-
grouped = Hash.new { |h, k| h[k] = [] }
|
|
217
|
-
entries.each do |e|
|
|
218
|
-
if e.share_group
|
|
219
|
-
grouped[[e.share_group, e.entry[:filter]]] << e
|
|
220
|
-
else
|
|
221
|
-
out << e
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
grouped.each do |key, members|
|
|
225
|
-
idx = (@share_cursor[key] += 1) % members.size
|
|
226
|
-
out << members[idx]
|
|
227
|
-
end
|
|
228
|
-
out
|
|
229
|
-
end
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
|
-
end
|
data/lib/zzq/socket.rb
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "async"
|
|
4
|
-
require "async/queue"
|
|
5
|
-
require_relative "error"
|
|
6
|
-
require_relative "monitor_event"
|
|
7
|
-
require_relative "options"
|
|
8
|
-
require_relative "engine"
|
|
9
|
-
|
|
10
|
-
module ZZQ
|
|
11
|
-
# Abstract base class for {ZZQ::Client} and {ZZQ::Broker}. Owns the
|
|
12
|
-
# Engine and exposes the infrastructure API (bind / connect / close /
|
|
13
|
-
# monitor / peer_connected / all_peers_gone). The role-specific API
|
|
14
|
-
# (subscribe / publish / ingest) lives on the subclasses.
|
|
15
|
-
#
|
|
16
|
-
# ZZQ is pure-Async: every public method that performs I/O must run
|
|
17
|
-
# inside an Async::Task. Either wrap calls in `Async do … end`, or
|
|
18
|
-
# use the block form of #connect / #bind which enters `Async{}` for
|
|
19
|
-
# you and closes the socket on block exit.
|
|
20
|
-
class Socket
|
|
21
|
-
attr_reader :options
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def self.bind(endpoint, **opts, &block)
|
|
25
|
-
sock = new(**opts)
|
|
26
|
-
if block
|
|
27
|
-
sock.bind(endpoint, &block)
|
|
28
|
-
else
|
|
29
|
-
sock.bind(endpoint)
|
|
30
|
-
sock
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def self.connect(endpoint, **opts, &block)
|
|
36
|
-
sock = new(**opts)
|
|
37
|
-
if block
|
|
38
|
-
sock.connect(endpoint, &block)
|
|
39
|
-
else
|
|
40
|
-
sock.connect(endpoint)
|
|
41
|
-
sock
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def initialize(**opts)
|
|
47
|
-
@options = Options.new(**opts)
|
|
48
|
-
@engine = Engine.new(options: @options) { |engine| build_routing(engine) }
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
# Binds to +endpoint+. If a block is given, wraps the call in
|
|
53
|
-
# `Async do … end` (if not already inside one), yields the owning
|
|
54
|
-
# Async::Task to the block, and closes the socket on block exit.
|
|
55
|
-
def bind(endpoint, **opts, &block)
|
|
56
|
-
block ? run_in_async(endpoint, :bind, opts, &block) : run_sync(:bind, endpoint, **opts)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# Connects to +endpoint+. Block form yields the owning Async::Task
|
|
61
|
-
# and auto-closes on block exit, same as {#bind}.
|
|
62
|
-
def connect(endpoint, **opts, &block)
|
|
63
|
-
block ? run_in_async(endpoint, :connect, opts, &block) : run_sync(:connect, endpoint, **opts)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def close
|
|
68
|
-
if Async::Task.current?
|
|
69
|
-
@engine.close
|
|
70
|
-
else
|
|
71
|
-
Async { @engine.close }.wait
|
|
72
|
-
end
|
|
73
|
-
nil
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def last_endpoint = @engine.last_endpoint
|
|
78
|
-
def connection_count = @engine.connections.size
|
|
79
|
-
def peer_connected = @engine.peer_connected
|
|
80
|
-
def all_peers_gone = @engine.all_peers_gone
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
# Yields lifecycle events for this socket until it's closed. Must
|
|
84
|
-
# be called from within an Async::Task.
|
|
85
|
-
def monitor(&block)
|
|
86
|
-
raise NotInAsyncContext unless Async::Task.current?
|
|
87
|
-
queue = Async::Queue.new
|
|
88
|
-
@engine.monitor_queue = queue
|
|
89
|
-
@engine.spawn_task(annotation: "zzq monitor") do
|
|
90
|
-
while (event = queue.dequeue)
|
|
91
|
-
block.call(event)
|
|
92
|
-
end
|
|
93
|
-
rescue Async::Stop
|
|
94
|
-
ensure
|
|
95
|
-
@engine.monitor_queue = nil
|
|
96
|
-
block.call(MonitorEvent.new(type: :monitor_stopped))
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
# Coerces +body+ to a frozen BINARY String. MQTT payloads are
|
|
102
|
-
# opaque bytes — uniform frozen+BINARY contract mirrors NNQ.
|
|
103
|
-
def coerce_binary(body)
|
|
104
|
-
body = body.to_str unless body.is_a?(String)
|
|
105
|
-
body.force_encoding(Encoding::BINARY) unless body.frozen? || body.encoding == Encoding::BINARY
|
|
106
|
-
body.freeze
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
private
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def run_sync(op, endpoint, **opts)
|
|
114
|
-
raise NotInAsyncContext unless Async::Task.current?
|
|
115
|
-
@engine.capture_parent_task
|
|
116
|
-
@engine.public_send(op, endpoint, **opts)
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def run_in_async(endpoint, op, opts, &block)
|
|
121
|
-
if (task = Async::Task.current?)
|
|
122
|
-
run_sync(op, endpoint, **opts)
|
|
123
|
-
begin
|
|
124
|
-
block.call(task)
|
|
125
|
-
ensure
|
|
126
|
-
close
|
|
127
|
-
end
|
|
128
|
-
else
|
|
129
|
-
Async do |task|
|
|
130
|
-
run_sync(op, endpoint, **opts)
|
|
131
|
-
begin
|
|
132
|
-
block.call(task)
|
|
133
|
-
ensure
|
|
134
|
-
close
|
|
135
|
-
end
|
|
136
|
-
end.wait
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
# Subclass hook — return a role-specific routing strategy instance.
|
|
142
|
-
def build_routing(_engine)
|
|
143
|
-
raise NotImplementedError, "#{self.class} must implement #build_routing"
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
end
|
data/lib/zzq/subscription.rb
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "async/queue"
|
|
4
|
-
require "async/limited_queue"
|
|
5
|
-
|
|
6
|
-
module ZZQ
|
|
7
|
-
# Handle returned by {Client#subscribe} and {Broker#subscribe}. A
|
|
8
|
-
# thin wrapper around an +Async::LimitedQueue+ that also owns the
|
|
9
|
-
# UNSUBSCRIBE lifecycle.
|
|
10
|
-
#
|
|
11
|
-
# Callers consume with {#pop} or {#each}; {#close} sends UNSUBSCRIBE
|
|
12
|
-
# and wakes any blocked consumer with +nil+.
|
|
13
|
-
#
|
|
14
|
-
# Created internally — users should never call +.new+ directly.
|
|
15
|
-
class Subscription
|
|
16
|
-
# @return [Array<String>] topic filters this subscription matches.
|
|
17
|
-
attr_reader :filters
|
|
18
|
-
|
|
19
|
-
# @return [Integer] subscription identifier (v5) or nil (v3).
|
|
20
|
-
attr_reader :subscription_id
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def initialize(filters:, hwm:, subscription_id: nil, on_close: nil)
|
|
24
|
-
@filters = Array(filters).freeze
|
|
25
|
-
@subscription_id = subscription_id
|
|
26
|
-
@queue = Async::LimitedQueue.new(hwm)
|
|
27
|
-
@on_close = on_close
|
|
28
|
-
@closed = false
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
# Blocks the fiber until a message arrives. Returns +nil+ when
|
|
33
|
-
# the subscription is closed.
|
|
34
|
-
def pop
|
|
35
|
-
return nil if @closed && @queue.empty?
|
|
36
|
-
@queue.dequeue
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
# Yields each message until the subscription is closed.
|
|
41
|
-
def each
|
|
42
|
-
return enum_for(:each) unless block_given?
|
|
43
|
-
while (msg = pop)
|
|
44
|
-
yield msg
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
# Sends UNSUBSCRIBE and wakes the consumer. Idempotent.
|
|
50
|
-
def close
|
|
51
|
-
return if @closed
|
|
52
|
-
@closed = true
|
|
53
|
-
@on_close&.call(self)
|
|
54
|
-
@queue.enqueue(nil)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
def closed?
|
|
59
|
-
@closed
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
# Internal: deliver a message to the subscriber's queue. Blocks
|
|
64
|
-
# the caller if the queue is at high-water-mark.
|
|
65
|
-
def deliver(message)
|
|
66
|
-
return if @closed
|
|
67
|
-
@queue.enqueue(message)
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
data/lib/zzq/transport/ipc.rb
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "socket"
|
|
4
|
-
require "io/stream"
|
|
5
|
-
require_relative "../engine"
|
|
6
|
-
|
|
7
|
-
module ZZQ
|
|
8
|
-
module Transport
|
|
9
|
-
# IPC transport using Unix domain sockets.
|
|
10
|
-
#
|
|
11
|
-
# Endpoint forms:
|
|
12
|
-
# * `ipc:///var/run/zzq.sock` — filesystem path (portable).
|
|
13
|
-
# * `ipc://@zzq-local` — Linux abstract namespace (the
|
|
14
|
-
# leading `@` becomes a NUL byte in sockaddr.sun_path).
|
|
15
|
-
module IPC
|
|
16
|
-
Engine.transports["ipc"] = self
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class << self
|
|
20
|
-
def bind(endpoint, engine, **)
|
|
21
|
-
path = parse_path(endpoint)
|
|
22
|
-
sock_path = to_socket_path(path)
|
|
23
|
-
|
|
24
|
-
File.delete(sock_path) if !abstract?(path) && File.exist?(sock_path)
|
|
25
|
-
|
|
26
|
-
server = UNIXServer.new(sock_path)
|
|
27
|
-
Listener.new(endpoint, server, path, engine)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def connect(endpoint, engine, **)
|
|
32
|
-
path = parse_path(endpoint)
|
|
33
|
-
sock_path = to_socket_path(path)
|
|
34
|
-
sock = UNIXSocket.new(sock_path)
|
|
35
|
-
engine.handle_connected(IO::Stream::Buffered.wrap(sock), endpoint: endpoint)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def parse_path(endpoint)
|
|
40
|
-
endpoint.sub(%r{\Aipc://}, "")
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
# Linux abstract-namespace Unix sockets: leading `@` becomes a
|
|
45
|
-
# NUL byte in sun_path. Only supported on Linux.
|
|
46
|
-
def to_socket_path(path)
|
|
47
|
-
abstract?(path) ? "\0#{path[1..]}" : path
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def abstract?(path)
|
|
52
|
-
path.start_with?("@")
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class Listener
|
|
58
|
-
attr_reader :endpoint
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def initialize(endpoint, server, path, engine)
|
|
62
|
-
@endpoint = endpoint
|
|
63
|
-
@server = server
|
|
64
|
-
@path = path
|
|
65
|
-
@engine = engine
|
|
66
|
-
@task = nil
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def start_accept_loop(parent_task, &on_accepted)
|
|
71
|
-
@task = parent_task.async(annotation: "zzq ipc accept #{@endpoint}") do
|
|
72
|
-
loop do
|
|
73
|
-
client = @server.accept
|
|
74
|
-
on_accepted.call(IO::Stream::Buffered.wrap(client))
|
|
75
|
-
rescue Async::Stop, IOError
|
|
76
|
-
break
|
|
77
|
-
end
|
|
78
|
-
ensure
|
|
79
|
-
@server.close rescue nil
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def stop
|
|
85
|
-
@task&.stop
|
|
86
|
-
@server.close rescue nil
|
|
87
|
-
File.delete(@path) rescue nil unless IPC.abstract?(@path)
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
data/lib/zzq/transport/mqtt.rb
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "socket"
|
|
4
|
-
require "uri"
|
|
5
|
-
require "io/stream"
|
|
6
|
-
require_relative "../engine"
|
|
7
|
-
|
|
8
|
-
module ZZQ
|
|
9
|
-
module Transport
|
|
10
|
-
# Plain MQTT-over-TCP transport. Endpoint scheme: `mqtt://host:port`
|
|
11
|
-
# (default port 1883). One server per bind, blocking accept inside
|
|
12
|
-
# an Async fiber.
|
|
13
|
-
module MQTT
|
|
14
|
-
Engine.transports["mqtt"] = self
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class << self
|
|
18
|
-
def bind(endpoint, engine, **)
|
|
19
|
-
host, port = parse_endpoint(endpoint)
|
|
20
|
-
host = "0.0.0.0" if host == "*"
|
|
21
|
-
server = TCPServer.new(host, port)
|
|
22
|
-
actual = server.local_address.ip_port
|
|
23
|
-
host_part = host.include?(":") ? "[#{host}]" : host
|
|
24
|
-
Listener.new("mqtt://#{host_part}:#{actual}", server, actual, engine)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def connect(endpoint, engine, **)
|
|
29
|
-
host, port = parse_endpoint(endpoint)
|
|
30
|
-
sock = ::Socket.tcp(host, port, connect_timeout: connect_timeout(engine.options))
|
|
31
|
-
engine.handle_connected(IO::Stream::Buffered.wrap(sock), endpoint: endpoint)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def connect_timeout(options)
|
|
36
|
-
ri = options.reconnect_interval
|
|
37
|
-
ri = ri.end if ri.is_a?(Range)
|
|
38
|
-
[ri, 0.5].max
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def parse_endpoint(endpoint)
|
|
43
|
-
uri = URI.parse(endpoint.sub("mqtt://", "tcp://"))
|
|
44
|
-
[uri.hostname, uri.port]
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
class Listener
|
|
50
|
-
attr_reader :endpoint, :port
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def initialize(endpoint, server, port, engine)
|
|
54
|
-
@endpoint = endpoint
|
|
55
|
-
@server = server
|
|
56
|
-
@port = port
|
|
57
|
-
@engine = engine
|
|
58
|
-
@task = nil
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def start_accept_loop(parent_task, &on_accepted)
|
|
63
|
-
@task = parent_task.async(annotation: "zzq mqtt accept #{@endpoint}") do
|
|
64
|
-
loop do
|
|
65
|
-
client = @server.accept
|
|
66
|
-
on_accepted.call(IO::Stream::Buffered.wrap(client))
|
|
67
|
-
rescue Async::Stop, IOError
|
|
68
|
-
break
|
|
69
|
-
end
|
|
70
|
-
ensure
|
|
71
|
-
@server.close rescue nil
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def stop
|
|
77
|
-
@task&.stop
|
|
78
|
-
@server.close rescue nil
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|