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,43 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ZZQ
|
|
4
|
-
module Persistence
|
|
5
|
-
# Abstract base for persistence backends.
|
|
6
|
-
#
|
|
7
|
-
# Concrete backends (Memory, PStore, future Redis/SQLite) implement
|
|
8
|
-
# the methods below. Broker state calls through this interface; the
|
|
9
|
-
# in-memory runtime state in {Routing::Broker} is the source of
|
|
10
|
-
# truth during a session, and the backend is the durable record
|
|
11
|
-
# that survives restarts.
|
|
12
|
-
#
|
|
13
|
-
# v1 scope: retained messages only. Session persistence will land
|
|
14
|
-
# alongside session-expiry (M8's deferred piece).
|
|
15
|
-
class Interface
|
|
16
|
-
# Yields each persisted {ZZQ::Message} (retained). Called once at
|
|
17
|
-
# broker start to warm the in-memory RetainedStore.
|
|
18
|
-
def load_retained
|
|
19
|
-
raise NotImplementedError
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# Persist a retained message under +message.topic+. Replaces any
|
|
24
|
-
# existing retained message for that topic.
|
|
25
|
-
def save_retained(_message)
|
|
26
|
-
raise NotImplementedError
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
# Remove the retained message for +topic+ (zero-length-payload
|
|
31
|
-
# rule, MQTT-3.3.1-6).
|
|
32
|
-
def delete_retained(_topic)
|
|
33
|
-
raise NotImplementedError
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
# Wipe all retained state. Mostly for tests and operator tooling.
|
|
38
|
-
def clear_retained
|
|
39
|
-
raise NotImplementedError
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "interface"
|
|
4
|
-
|
|
5
|
-
module ZZQ
|
|
6
|
-
module Persistence
|
|
7
|
-
# In-process persistence backend. Default for ephemeral brokers and
|
|
8
|
-
# tests. State is lost on process exit.
|
|
9
|
-
class Memory < Interface
|
|
10
|
-
def initialize
|
|
11
|
-
super()
|
|
12
|
-
@retained = {} # topic => ZZQ::Message
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def load_retained
|
|
17
|
-
return enum_for(:load_retained) unless block_given?
|
|
18
|
-
@retained.each_value { |msg| yield msg }
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def save_retained(message)
|
|
23
|
-
@retained[message.topic] = message
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def delete_retained(topic)
|
|
28
|
-
@retained.delete(topic)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def clear_retained
|
|
33
|
-
@retained.clear
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "fileutils"
|
|
4
|
-
require "pstore"
|
|
5
|
-
require "protocol/mqtt"
|
|
6
|
-
require_relative "interface"
|
|
7
|
-
require_relative "../message"
|
|
8
|
-
|
|
9
|
-
module ZZQ
|
|
10
|
-
module Persistence
|
|
11
|
-
# PStore-backed durable persistence. Single-process deployments
|
|
12
|
-
# only — PStore uses file locking but doesn't coordinate well
|
|
13
|
-
# across processes.
|
|
14
|
-
#
|
|
15
|
-
# `ultra_safe` is enabled by default: each commit does a full fsync
|
|
16
|
-
# of the data file AND its parent directory, surviving power loss
|
|
17
|
-
# (not just clean crashes). Fits a broker where "I told the
|
|
18
|
-
# publisher QoS 1 ack" must survive a kernel panic.
|
|
19
|
-
#
|
|
20
|
-
# Messages are serialized via {ZZQ::Message#to_wire}, so the
|
|
21
|
-
# on-disk format is the PUBLISH wire encoding — self-describing
|
|
22
|
-
# and version-tagged.
|
|
23
|
-
class PStore < Interface
|
|
24
|
-
DEFAULT_FILENAME = "retained.pstore"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def initialize(data_dir:, filename: DEFAULT_FILENAME, ultra_safe: true)
|
|
28
|
-
super()
|
|
29
|
-
FileUtils.mkdir_p(data_dir)
|
|
30
|
-
@store = ::PStore.new(File.join(data_dir, filename))
|
|
31
|
-
@store.ultra_safe = ultra_safe
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def load_retained
|
|
36
|
-
return enum_for(:load_retained) unless block_given?
|
|
37
|
-
@store.transaction(true) do
|
|
38
|
-
(@store[:retained] || {}).each_value do |bytes|
|
|
39
|
-
yield ZZQ::Message.from_wire(bytes)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def save_retained(message)
|
|
46
|
-
bytes = message.to_wire
|
|
47
|
-
@store.transaction do
|
|
48
|
-
@store[:retained] ||= {}
|
|
49
|
-
@store[:retained][message.topic] = bytes
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def delete_retained(topic)
|
|
55
|
-
@store.transaction do
|
|
56
|
-
(@store[:retained] || {}).delete(topic)
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def clear_retained
|
|
62
|
-
@store.transaction { @store[:retained] = {} }
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
data/lib/zzq/routing/broker.rb
DELETED
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "async"
|
|
4
|
-
require "securerandom"
|
|
5
|
-
require "protocol/mqtt"
|
|
6
|
-
require_relative "../engine/keepalive"
|
|
7
|
-
require_relative "../message"
|
|
8
|
-
require_relative "packet_id_allocator"
|
|
9
|
-
require_relative "qos_tracker"
|
|
10
|
-
require_relative "retained_store"
|
|
11
|
-
require_relative "topic_match"
|
|
12
|
-
require_relative "topic_trie"
|
|
13
|
-
|
|
14
|
-
module ZZQ
|
|
15
|
-
module Routing
|
|
16
|
-
# Broker-side routing.
|
|
17
|
-
#
|
|
18
|
-
# Current scope:
|
|
19
|
-
# * Read CONNECT, send CONNACK.
|
|
20
|
-
# * Per-connection read loop: PINGREQ/PINGRESP, DISCONNECT,
|
|
21
|
-
# PUBLISH fan-out to matching sessions (QoS 0/1/2), SUBSCRIBE /
|
|
22
|
-
# UNSUBSCRIBE with SUBACK / UNSUBACK, and PUBACK/PUBREC/PUBREL/
|
|
23
|
-
# PUBCOMP to drive outbound QoS state per session.
|
|
24
|
-
# * Simple linear session map; proper topic trie + retained store
|
|
25
|
-
# come in M7.
|
|
26
|
-
class Broker
|
|
27
|
-
CONNECT_TIMEOUT = 10 # seconds to wait for the initial CONNECT
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
Session = Struct.new(
|
|
31
|
-
:client_id, :conn, :subscriptions, :packet_ids, :qos_tracker,
|
|
32
|
-
:will, :keepalive, :clean_disconnect,
|
|
33
|
-
keyword_init: true,
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def initialize(engine)
|
|
38
|
-
@engine = engine
|
|
39
|
-
@sessions = {} # client_id => Session
|
|
40
|
-
@by_conn = {} # Connection => Session
|
|
41
|
-
@trie = TopicTrie.new
|
|
42
|
-
@retained = RetainedStore.new(persistence: engine.options.persistence)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def handle_accepted(mqtt, lifecycle)
|
|
47
|
-
connect = nil
|
|
48
|
-
Async::Task.current.with_timeout(CONNECT_TIMEOUT) do
|
|
49
|
-
connect = mqtt.read_connect_packet
|
|
50
|
-
raise ConnectionRejected, "no CONNECT received" if connect.nil?
|
|
51
|
-
|
|
52
|
-
client_id = connect.client_id
|
|
53
|
-
if client_id.empty? && mqtt.version == 3 && !connect.clean_start
|
|
54
|
-
write_connack(mqtt, reason: Protocol::MQTT::ReasonCodes::V3Connack::IDENTIFIER_REJECTED)
|
|
55
|
-
raise ConnectionRejected, "empty ClientId with clean_session=false is illegal in v3"
|
|
56
|
-
end
|
|
57
|
-
assigned = client_id.empty?
|
|
58
|
-
client_id = generate_client_id if assigned
|
|
59
|
-
|
|
60
|
-
# v5 Receive Maximum: client advertises the max number of
|
|
61
|
-
# outstanding QoS > 0 publishes it is willing to accept. Sized
|
|
62
|
-
# on the packet-id allocator used for fan-out to this session.
|
|
63
|
-
client_rcv_max = connect.properties[:receive_maximum] || 65_535
|
|
64
|
-
session = build_session(client_id, mqtt, will: connect.will,
|
|
65
|
-
receive_maximum: client_rcv_max)
|
|
66
|
-
@sessions[client_id] = session
|
|
67
|
-
@by_conn[mqtt] = session
|
|
68
|
-
|
|
69
|
-
write_connack(mqtt, reason: 0, assigned_client_id: assigned ? client_id : nil)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
lifecycle.handshake_succeeded!
|
|
73
|
-
start_read_loop(mqtt, lifecycle)
|
|
74
|
-
start_keepalive(mqtt, lifecycle, connect.keep_alive)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def connection_added(_conn); end
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def connection_removed(conn)
|
|
82
|
-
session = @by_conn.delete(conn)
|
|
83
|
-
return unless session
|
|
84
|
-
publish_will(session) unless session.clean_disconnect
|
|
85
|
-
@sessions.delete(session.client_id)
|
|
86
|
-
@trie.remove_session(session)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
# Publish the client's Last Will and Testament via the normal
|
|
91
|
-
# dispatch path. Called when the session is torn down without
|
|
92
|
-
# having seen a clean DISCONNECT.
|
|
93
|
-
def publish_will(session)
|
|
94
|
-
will = session.will
|
|
95
|
-
return unless will
|
|
96
|
-
msg = Message.new(
|
|
97
|
-
topic: will.fetch(:topic),
|
|
98
|
-
payload: will.fetch(:payload),
|
|
99
|
-
qos: will.fetch(:qos, 0),
|
|
100
|
-
retain: will.fetch(:retain, false),
|
|
101
|
-
properties: will.fetch(:properties, {}) || {},
|
|
102
|
-
)
|
|
103
|
-
# Also honor RETAIN on wills.
|
|
104
|
-
apply_retain(msg) if msg.retain
|
|
105
|
-
dispatch_message(msg)
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def close; end
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
# Used by tests.
|
|
113
|
-
def session_count = @sessions.size
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
# Fan out a Message to every session subscribed to a matching
|
|
117
|
-
# filter. Used by both inbound PUBLISH handling and
|
|
118
|
-
# {Broker#ingest} from a non-MQTT source.
|
|
119
|
-
def dispatch_message(message, origin: nil)
|
|
120
|
-
delivered = {} # session.object_id => true (one copy per session)
|
|
121
|
-
@trie.match(message.topic).each do |hit|
|
|
122
|
-
# v5 No Local (MQTT-3.8.3-3): when set on the subscription,
|
|
123
|
-
# don't echo a publish back to the publishing client.
|
|
124
|
-
next if hit.entry[:no_local] && origin && hit.session.conn.equal?(origin)
|
|
125
|
-
next if delivered[hit.session.object_id]
|
|
126
|
-
delivered[hit.session.object_id] = true
|
|
127
|
-
forward_publish(hit.session, message, hit.entry)
|
|
128
|
-
end
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
private
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def build_session(client_id, mqtt, will: nil, receive_maximum: 65_535)
|
|
136
|
-
Session.new(
|
|
137
|
-
client_id: client_id,
|
|
138
|
-
conn: mqtt,
|
|
139
|
-
subscriptions: [],
|
|
140
|
-
packet_ids: PacketIdAllocator.new(receive_maximum: receive_maximum),
|
|
141
|
-
qos_tracker: QosTracker.new,
|
|
142
|
-
will: will,
|
|
143
|
-
keepalive: nil,
|
|
144
|
-
clean_disconnect: false,
|
|
145
|
-
)
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def start_keepalive(mqtt, lifecycle, keep_alive)
|
|
150
|
-
return if keep_alive.nil? || keep_alive.zero?
|
|
151
|
-
session = @by_conn[mqtt] or return
|
|
152
|
-
session.keepalive = Engine::Keepalive.new(
|
|
153
|
-
mqtt, keep_alive: keep_alive, role: :watcher,
|
|
154
|
-
on_timeout: -> { lifecycle.lost! },
|
|
155
|
-
)
|
|
156
|
-
lifecycle.barrier.async(annotation: "zzq broker keepalive #{lifecycle.endpoint}") do
|
|
157
|
-
session.keepalive.run
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def write_connack(mqtt, reason:, assigned_client_id: nil, session_present: false)
|
|
163
|
-
props = {}
|
|
164
|
-
props[:assigned_client_identifier] = assigned_client_id if assigned_client_id && mqtt.version == 5
|
|
165
|
-
mqtt.write_packet(
|
|
166
|
-
Protocol::MQTT::Packet::Connack.new(
|
|
167
|
-
session_present: session_present,
|
|
168
|
-
reason_code: reason,
|
|
169
|
-
properties: props,
|
|
170
|
-
),
|
|
171
|
-
)
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def generate_client_id
|
|
176
|
-
"zzq-#{SecureRandom.hex(6)}"
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
def start_read_loop(mqtt, lifecycle)
|
|
181
|
-
lifecycle.barrier.async(annotation: "zzq broker read #{lifecycle.endpoint}") do
|
|
182
|
-
loop do
|
|
183
|
-
pkt = mqtt.read_packet
|
|
184
|
-
break if pkt.nil?
|
|
185
|
-
@by_conn[mqtt]&.keepalive&.note_received
|
|
186
|
-
dispatch(pkt, mqtt)
|
|
187
|
-
rescue *CONNECTION_LOST, Protocol::MQTT::MalformedPacket
|
|
188
|
-
break
|
|
189
|
-
end
|
|
190
|
-
ensure
|
|
191
|
-
lifecycle.lost! unless lifecycle.state == :closed
|
|
192
|
-
end
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def dispatch(pkt, mqtt)
|
|
197
|
-
case pkt
|
|
198
|
-
when Protocol::MQTT::Packet::Pingreq
|
|
199
|
-
mqtt.write_packet(Protocol::MQTT::Packet::Pingresp.new)
|
|
200
|
-
when Protocol::MQTT::Packet::Disconnect
|
|
201
|
-
# Clean DISCONNECT: v3 = any disconnect; v5 = reason 0x00
|
|
202
|
-
# (NORMAL_DISCONNECTION). v5 reason 0x04 explicitly requests
|
|
203
|
-
# that the will still be published. Others suppress (spec).
|
|
204
|
-
session = @by_conn[mqtt]
|
|
205
|
-
if session
|
|
206
|
-
reason = mqtt.version == 5 ? pkt.reason_code : 0
|
|
207
|
-
session.clean_disconnect = (reason != 0x04)
|
|
208
|
-
end
|
|
209
|
-
raise EOFError # cleanly unwind the read loop
|
|
210
|
-
when Protocol::MQTT::Packet::Publish
|
|
211
|
-
handle_publish(pkt, mqtt)
|
|
212
|
-
when Protocol::MQTT::Packet::Subscribe
|
|
213
|
-
handle_subscribe(pkt, mqtt)
|
|
214
|
-
when Protocol::MQTT::Packet::Unsubscribe
|
|
215
|
-
handle_unsubscribe(pkt, mqtt)
|
|
216
|
-
when Protocol::MQTT::Packet::Puback
|
|
217
|
-
handle_puback(pkt, mqtt)
|
|
218
|
-
when Protocol::MQTT::Packet::Pubrec
|
|
219
|
-
handle_pubrec(pkt, mqtt)
|
|
220
|
-
when Protocol::MQTT::Packet::Pubrel
|
|
221
|
-
handle_pubrel(pkt, mqtt)
|
|
222
|
-
when Protocol::MQTT::Packet::Pubcomp
|
|
223
|
-
handle_pubcomp(pkt, mqtt)
|
|
224
|
-
end
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
def handle_publish(pkt, mqtt)
|
|
229
|
-
session = @by_conn[mqtt] or return
|
|
230
|
-
message = Message.from_publish(pkt)
|
|
231
|
-
case pkt.qos
|
|
232
|
-
when 0
|
|
233
|
-
apply_retain(message) if pkt.retain
|
|
234
|
-
dispatch_message(message, origin: mqtt)
|
|
235
|
-
when 1
|
|
236
|
-
apply_retain(message) if pkt.retain
|
|
237
|
-
dispatch_message(message, origin: mqtt)
|
|
238
|
-
mqtt.write_packet(Protocol::MQTT::Packet::Puback.new(packet_id: pkt.packet_id))
|
|
239
|
-
when 2
|
|
240
|
-
# Fan out only on first receipt; duplicates are still PUBRECed.
|
|
241
|
-
if session.qos_tracker.on_qos2_publish(pkt.packet_id)
|
|
242
|
-
apply_retain(message) if pkt.retain
|
|
243
|
-
dispatch_message(message, origin: mqtt)
|
|
244
|
-
end
|
|
245
|
-
mqtt.write_packet(Protocol::MQTT::Packet::Pubrec.new(packet_id: pkt.packet_id))
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
def apply_retain(message)
|
|
251
|
-
@retained.store(message)
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
def handle_subscribe(pkt, mqtt)
|
|
256
|
-
session = @by_conn[mqtt] or return
|
|
257
|
-
added = []
|
|
258
|
-
reasons = pkt.filters.map do |entry|
|
|
259
|
-
sub_entry = {
|
|
260
|
-
filter: entry.fetch(:filter),
|
|
261
|
-
qos: entry.fetch(:qos, 0),
|
|
262
|
-
no_local: entry.fetch(:no_local, false),
|
|
263
|
-
retain_as_published: entry.fetch(:retain_as_published, false),
|
|
264
|
-
retain_handling: entry.fetch(:retain_handling, 0),
|
|
265
|
-
}
|
|
266
|
-
session.subscriptions << sub_entry
|
|
267
|
-
@trie.add(sub_entry[:filter], session, sub_entry)
|
|
268
|
-
added << sub_entry
|
|
269
|
-
sub_entry[:qos] # granted QoS == requested (MVP)
|
|
270
|
-
end
|
|
271
|
-
mqtt.write_packet(Protocol::MQTT::Packet::Suback.new(
|
|
272
|
-
packet_id: pkt.packet_id, reason_codes: reasons,
|
|
273
|
-
))
|
|
274
|
-
# Replay retained messages for each newly-added filter.
|
|
275
|
-
added.each { |sub_entry| replay_retained(session, sub_entry) }
|
|
276
|
-
end
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
def handle_unsubscribe(pkt, mqtt)
|
|
280
|
-
session = @by_conn[mqtt]
|
|
281
|
-
reasons = pkt.filters.map do |filter|
|
|
282
|
-
matched = session&.subscriptions&.reject! { |s| s[:filter] == filter }
|
|
283
|
-
@trie.remove(filter, session) if matched && session
|
|
284
|
-
matched && !matched.empty? ? 0x00 : 0x11 # 0x11 = No subscription existed
|
|
285
|
-
end
|
|
286
|
-
mqtt.write_packet(Protocol::MQTT::Packet::Unsuback.new(
|
|
287
|
-
packet_id: pkt.packet_id, reason_codes: reasons,
|
|
288
|
-
))
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
# For each retained message matching the new filter, deliver a
|
|
293
|
-
# PUBLISH copy with RETAIN=1 (MQTT-3.3.1-8). Retain handling per
|
|
294
|
-
# v5: 0 = send retained on every SUBSCRIBE; 1 = only if the sub
|
|
295
|
-
# didn't exist; 2 = never. MVP honours 0/2; 1 is a future polish.
|
|
296
|
-
def replay_retained(session, sub_entry)
|
|
297
|
-
return if sub_entry[:retain_handling] == 2
|
|
298
|
-
@retained.each_matching(sub_entry[:filter]) do |msg|
|
|
299
|
-
forward_publish(session, msg, sub_entry, retain: true)
|
|
300
|
-
end
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
# --- outbound-QoS ack handlers -------------------------------------
|
|
305
|
-
|
|
306
|
-
def handle_puback(pkt, mqtt)
|
|
307
|
-
session = @by_conn[mqtt] or return
|
|
308
|
-
pending = session.qos_tracker.on_puback(pkt.packet_id)
|
|
309
|
-
session.packet_ids.release(pkt.packet_id) if pending
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
def handle_pubrec(pkt, mqtt)
|
|
314
|
-
session = @by_conn[mqtt] or return
|
|
315
|
-
entry = session.qos_tracker.on_pubrec(pkt.packet_id)
|
|
316
|
-
return unless entry
|
|
317
|
-
mqtt.write_packet(Protocol::MQTT::Packet::Pubrel.new(packet_id: pkt.packet_id))
|
|
318
|
-
end
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
# Inbound PUBREL: the client is telling us to release its own
|
|
322
|
-
# inbound QoS 2 dedup entry (it published at QoS 2; we PUBRECed;
|
|
323
|
-
# now it's clearing). Respond with PUBCOMP.
|
|
324
|
-
def handle_pubrel(pkt, mqtt)
|
|
325
|
-
session = @by_conn[mqtt] or return
|
|
326
|
-
session.qos_tracker.on_pubrel(pkt.packet_id)
|
|
327
|
-
mqtt.write_packet(Protocol::MQTT::Packet::Pubcomp.new(packet_id: pkt.packet_id))
|
|
328
|
-
end
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
def handle_pubcomp(pkt, mqtt)
|
|
332
|
-
session = @by_conn[mqtt] or return
|
|
333
|
-
pending = session.qos_tracker.on_pubcomp(pkt.packet_id)
|
|
334
|
-
session.packet_ids.release(pkt.packet_id) if pending
|
|
335
|
-
end
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
def forward_publish(session, message, entry, retain: false)
|
|
339
|
-
granted_qos = [message.qos, entry[:qos]].min
|
|
340
|
-
# retain_as_published (v5): when true, preserve publisher's
|
|
341
|
-
# RETAIN flag; otherwise spec forces RETAIN=0 on fan-out (and
|
|
342
|
-
# =1 only on retained-replay, tracked via the +retain+ arg).
|
|
343
|
-
out_retain =
|
|
344
|
-
if retain
|
|
345
|
-
true
|
|
346
|
-
elsif entry[:retain_as_published]
|
|
347
|
-
message.retain
|
|
348
|
-
else
|
|
349
|
-
false
|
|
350
|
-
end
|
|
351
|
-
|
|
352
|
-
if granted_qos.zero?
|
|
353
|
-
out = Protocol::MQTT::Packet::Publish.new(
|
|
354
|
-
topic: message.topic, payload: message.payload,
|
|
355
|
-
qos: 0, retain: out_retain, dup: false, packet_id: nil,
|
|
356
|
-
properties: message.properties,
|
|
357
|
-
)
|
|
358
|
-
session.conn.write_packet(out)
|
|
359
|
-
return
|
|
360
|
-
end
|
|
361
|
-
|
|
362
|
-
packet_id = session.packet_ids.acquire
|
|
363
|
-
out = Protocol::MQTT::Packet::Publish.new(
|
|
364
|
-
topic: message.topic, payload: message.payload,
|
|
365
|
-
qos: granted_qos, retain: out_retain, dup: false,
|
|
366
|
-
packet_id: packet_id, properties: message.properties,
|
|
367
|
-
)
|
|
368
|
-
if granted_qos == 1
|
|
369
|
-
session.qos_tracker.track_qos1(packet_id, out)
|
|
370
|
-
else
|
|
371
|
-
session.qos_tracker.track_qos2(packet_id, out)
|
|
372
|
-
end
|
|
373
|
-
session.conn.write_packet(out)
|
|
374
|
-
rescue *CONNECTION_LOST, ClosedError
|
|
375
|
-
# recipient is gone; tear-down handled by their read loop.
|
|
376
|
-
session.packet_ids.release(packet_id) if packet_id
|
|
377
|
-
end
|
|
378
|
-
end
|
|
379
|
-
end
|
|
380
|
-
end
|