zzq 0.1.1 → 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 +24 -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 -169
- 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
data/lib/zzq/client.rb
CHANGED
|
@@ -1,51 +1,218 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "socket"
|
|
4
|
-
|
|
5
3
|
module ZZQ
|
|
6
|
-
# MQTT
|
|
7
|
-
#
|
|
8
|
-
#
|
|
4
|
+
# An MQTT 3.1.1 or 5.0 client. Calls wait without blocking other fibers
|
|
5
|
+
# under a fiber scheduler such as Async, and release the GVL in threads.
|
|
6
|
+
#
|
|
7
|
+
# Publishing and subscribing work from any fiber or thread. One fiber at
|
|
8
|
+
# a time receives.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
# @example
|
|
11
|
+
# ZZQ::Client.connect("mqtt://localhost", client_id: "reader") do |client|
|
|
12
|
+
# client.subscribe("sensors/#", qos: 1)
|
|
13
|
+
# client.each { |message| puts "#{message.topic}: #{message.payload}" }
|
|
14
|
+
# end
|
|
15
|
+
class Client
|
|
16
|
+
OPTIONS = %i[
|
|
17
|
+
client_id version clean_start keep_alive session_expiry username password will inbox
|
|
18
|
+
reconnect connect_timeout tls
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
# Messages a receive takes from the native inbox at once.
|
|
22
|
+
BATCH = 64
|
|
23
|
+
|
|
24
|
+
# Connects a new client. With a block, yields it and closes it after.
|
|
25
|
+
#
|
|
26
|
+
# @param endpoint [String] see {#connect}
|
|
27
|
+
# @param options [Hash] see {#initialize}
|
|
28
|
+
# @return [Client, Object] the client, or the block's result
|
|
29
|
+
def self.connect(endpoint, **options, &block)
|
|
30
|
+
new(**options).connect(endpoint, &block)
|
|
16
31
|
end
|
|
17
32
|
|
|
33
|
+
# @param client_id [String] empty lets a v5 broker assign one
|
|
34
|
+
# @param version [Integer] 5 (default) or 3 for MQTT 3.1.1
|
|
35
|
+
# @param clean_start [Boolean] start a new session (default true)
|
|
36
|
+
# @param keep_alive [Integer] seconds, 60 by default
|
|
37
|
+
# @param session_expiry [Integer] v5 seconds the broker keeps the session
|
|
38
|
+
# @param username [String, nil]
|
|
39
|
+
# @param password [String, nil]
|
|
40
|
+
# @param will [Hash, nil] +topic:+, +payload:+, +qos:+, +retain:+, +delay:+
|
|
41
|
+
# @param inbox [Integer] messages the client buffers before it stops
|
|
42
|
+
# reading, 4096 by default
|
|
43
|
+
# @param reconnect [Boolean, Hash] reconnect after a loss, optionally
|
|
44
|
+
# with +min:+ and +max:+ seconds between attempts
|
|
45
|
+
# @param connect_timeout [Numeric] seconds, 10 by default
|
|
46
|
+
# @param tls [Hash, nil] +ca:+, +cert:+, +key:+ (PEM strings),
|
|
47
|
+
# +server_name:+, +insecure:+
|
|
48
|
+
def initialize(**options)
|
|
49
|
+
@options = Options.check(options, OPTIONS)
|
|
50
|
+
@native = nil
|
|
51
|
+
@inbox = []
|
|
52
|
+
@receiving = false
|
|
53
|
+
@closed = false
|
|
54
|
+
end
|
|
18
55
|
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
|
|
22
|
-
|
|
56
|
+
# Connects and waits for CONNACK. With a block, yields the client and
|
|
57
|
+
# closes it after.
|
|
58
|
+
#
|
|
59
|
+
# @param endpoint [String] +mqtt://host[:port]+, +mqtts://+, +tcp://+,
|
|
60
|
+
# +tls://+, +ws://+, +wss://+, +quic://+, +ipc://+ or +inproc://+
|
|
61
|
+
# @return [self, Object] the client, or the block's result
|
|
62
|
+
# @raise [ConnectionRefused] if the broker refuses the connection
|
|
63
|
+
# @raise [TransportError] if the connection fails
|
|
64
|
+
def connect(endpoint)
|
|
65
|
+
raise Error, "already connected" if @native
|
|
23
66
|
|
|
67
|
+
@native = Waiting.await(Native::Client.connect(Options.endpoint(endpoint), @options))
|
|
68
|
+
return self unless block_given?
|
|
69
|
+
|
|
70
|
+
begin
|
|
71
|
+
yield self
|
|
72
|
+
ensure
|
|
73
|
+
close
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @return [Boolean] whether the broker resumed a session
|
|
78
|
+
def session_present?
|
|
79
|
+
native.session_present?
|
|
80
|
+
end
|
|
24
81
|
|
|
25
|
-
#
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def publish(topic, payload, qos: 0, retain: false, properties: {})
|
|
29
|
-
raise NotInAsyncContext unless Async::Task.current?
|
|
30
|
-
@engine.routing.publish(
|
|
31
|
-
topic: topic, payload: coerce_binary(payload),
|
|
32
|
-
qos: qos, retain: retain, properties: properties,
|
|
33
|
-
)
|
|
82
|
+
# @return [String, nil] the client ID a v5 broker assigned
|
|
83
|
+
def assigned_client_id
|
|
84
|
+
native.assigned_client_id
|
|
34
85
|
end
|
|
35
86
|
|
|
87
|
+
# Publishes a message. QoS 0 returns once queued, QoS 1 and 2 once the
|
|
88
|
+
# broker acked it.
|
|
89
|
+
#
|
|
90
|
+
# @param topic [String]
|
|
91
|
+
# @param payload [String]
|
|
92
|
+
# @param qos [Integer] 0, 1 or 2
|
|
93
|
+
# @param retain [Boolean] the broker keeps it for later subscribers;
|
|
94
|
+
# an empty payload clears the topic
|
|
95
|
+
# @return [nil]
|
|
96
|
+
def publish(topic, payload, qos: 0, retain: false)
|
|
97
|
+
payload = payload.to_s unless payload.is_a?(String)
|
|
98
|
+
op = native.publish(topic, payload, qos, retain)
|
|
99
|
+
Waiting.await(op) if op
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
36
102
|
|
|
37
|
-
#
|
|
38
|
-
#
|
|
103
|
+
# Subscribes to a topic filter.
|
|
104
|
+
#
|
|
105
|
+
# @param filter [String]
|
|
106
|
+
# @param qos [Integer] maximum QoS
|
|
107
|
+
# @param no_local [Boolean] v5: skip this client's own publishes
|
|
108
|
+
# @param retain_as_published [Boolean] v5: keep the publisher's retain
|
|
109
|
+
# flag
|
|
110
|
+
# @param retain_handling [Integer] v5: 0 send retained messages, 1 only
|
|
111
|
+
# for a new subscription, 2 never
|
|
112
|
+
# @param subscription_id [Integer] v5 Subscription Identifier, 0 for
|
|
113
|
+
# none
|
|
114
|
+
# @return [Integer] the granted QoS
|
|
115
|
+
# @raise [Rejected] if the broker refuses the filter
|
|
39
116
|
def subscribe(filter, qos: 0, no_local: false, retain_as_published: false,
|
|
40
|
-
retain_handling: 0)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
117
|
+
retain_handling: 0, subscription_id: 0)
|
|
118
|
+
spec = [filter, qos, no_local, retain_as_published, retain_handling]
|
|
119
|
+
code = Waiting.await(native.subscribe([spec], subscription_id)).first
|
|
120
|
+
raise Rejected.new("subscribe to #{filter.inspect} refused", code) if code >= 0x80
|
|
121
|
+
|
|
122
|
+
code
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Unsubscribes from topic filters.
|
|
126
|
+
#
|
|
127
|
+
# @param filters [Array<String>]
|
|
128
|
+
# @return [nil]
|
|
129
|
+
# @raise [Rejected] if a v5 broker refuses one
|
|
130
|
+
def unsubscribe(*filters)
|
|
131
|
+
codes = Waiting.await(native.unsubscribe(filters.flatten))
|
|
132
|
+
refused = codes.find { |code| code >= 0x80 }
|
|
133
|
+
raise Rejected.new("unsubscribe refused", refused) if refused
|
|
134
|
+
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Takes the next message, waiting for one.
|
|
139
|
+
#
|
|
140
|
+
# @param timeout [Numeric, nil] seconds
|
|
141
|
+
# @return [Message, nil] nil once the client is closed
|
|
142
|
+
# @raise [IO::TimeoutError] if no message came within +timeout+
|
|
143
|
+
def receive(timeout: nil)
|
|
144
|
+
return @inbox.shift unless @inbox.empty?
|
|
145
|
+
return nil unless @native
|
|
146
|
+
raise Error, "another fiber is receiving" if @receiving
|
|
147
|
+
|
|
148
|
+
@receiving = true
|
|
149
|
+
begin
|
|
150
|
+
next_batch(timeout)
|
|
151
|
+
ensure
|
|
152
|
+
@receiving = false
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Yields each message until the client is closed.
|
|
157
|
+
#
|
|
158
|
+
# @yieldparam message [Message]
|
|
159
|
+
# @return [Enumerator, nil] an Enumerator without a block
|
|
160
|
+
def each
|
|
161
|
+
return enum_for(:each) unless block_given?
|
|
162
|
+
|
|
163
|
+
while (message = receive)
|
|
164
|
+
yield message
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Sends DISCONNECT and closes. A fiber waiting in {#receive} gets nil.
|
|
169
|
+
#
|
|
170
|
+
# @param reason [Integer] v5 reason code; 0x04 asks for the will
|
|
171
|
+
# @return [nil]
|
|
172
|
+
def disconnect(reason: 0)
|
|
173
|
+
return if @closed || @native.nil?
|
|
174
|
+
|
|
175
|
+
@closed = true
|
|
176
|
+
Waiting.await(@native.disconnect(reason))
|
|
177
|
+
nil
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Closes with a normal DISCONNECT.
|
|
181
|
+
#
|
|
182
|
+
# @return [nil]
|
|
183
|
+
def close
|
|
184
|
+
disconnect
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# @return [Boolean]
|
|
188
|
+
def closed?
|
|
189
|
+
@closed
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
private
|
|
193
|
+
|
|
194
|
+
# Waits for messages, keeps them in the inbox and returns the first.
|
|
195
|
+
def next_batch(timeout)
|
|
196
|
+
Waiting.with_wake do |wake|
|
|
197
|
+
limit = Waiting.deadline(timeout)
|
|
198
|
+
loop do
|
|
199
|
+
batch = @native.receive(wake, BATCH)
|
|
200
|
+
return nil if batch.nil?
|
|
201
|
+
|
|
202
|
+
if batch.is_a?(Array)
|
|
203
|
+
@inbox = batch
|
|
204
|
+
return @inbox.shift
|
|
205
|
+
end
|
|
206
|
+
wake.wait(limit)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def native
|
|
212
|
+
raise ClosedError, "client closed" if @closed
|
|
213
|
+
raise ClosedError, "client not connected" unless @native
|
|
214
|
+
|
|
215
|
+
@native
|
|
47
216
|
end
|
|
48
217
|
end
|
|
49
218
|
end
|
|
50
|
-
|
|
51
|
-
require_relative "routing/client"
|
data/lib/zzq/errors.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ZZQ
|
|
4
|
+
# Base class of zzq errors.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# The client or broker is closed, or the connection is gone.
|
|
8
|
+
class ClosedError < Error; end
|
|
9
|
+
|
|
10
|
+
# The endpoint, the connection or TLS failed.
|
|
11
|
+
class TransportError < Error; end
|
|
12
|
+
|
|
13
|
+
# The peer broke the MQTT protocol.
|
|
14
|
+
class ProtocolError < Error; end
|
|
15
|
+
|
|
16
|
+
# A topic name or filter is not valid.
|
|
17
|
+
class InvalidTopic < Error; end
|
|
18
|
+
|
|
19
|
+
# A message or field is larger than MQTT can carry.
|
|
20
|
+
class TooLarge < Error; end
|
|
21
|
+
|
|
22
|
+
# The broker rejected a request, such as a subscription.
|
|
23
|
+
class Rejected < Error
|
|
24
|
+
# @return [Integer, nil] MQTT reason code
|
|
25
|
+
attr_reader :reason_code
|
|
26
|
+
|
|
27
|
+
# @param message [String, nil]
|
|
28
|
+
# @param reason_code [Integer, nil]
|
|
29
|
+
def initialize(message = nil, reason_code = nil)
|
|
30
|
+
super(message)
|
|
31
|
+
@reason_code = reason_code
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The broker refused the connection.
|
|
36
|
+
class ConnectionRefused < Rejected; end
|
|
37
|
+
end
|
data/lib/zzq/message.rb
CHANGED
|
@@ -1,113 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "protocol/mqtt"
|
|
4
|
-
|
|
5
3
|
module ZZQ
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# `Message` is independent of any particular connection: it carries
|
|
10
|
-
# every wire-level field that a PUBLISH packet does, so it can be
|
|
11
|
-
# shipped across non-MQTT transports (OMQ, NNQ, a file, a queue) via
|
|
12
|
-
# {#to_wire} / {.from_wire} and re-injected at the other end.
|
|
4
|
+
# A received message. The topic is a frozen UTF-8 String and the payload
|
|
5
|
+
# a frozen binary String.
|
|
13
6
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@properties = properties
|
|
29
|
-
@on_ack = on_ack
|
|
30
|
-
@acked = false
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def ack!
|
|
35
|
-
return if @qos.zero? || @acked
|
|
36
|
-
@acked = true
|
|
37
|
-
@on_ack&.call(self)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def acked?
|
|
42
|
-
@acked
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
# Encode this message as a self-describing byte string using the
|
|
47
|
-
# PUBLISH packet codec. The output can be cross-shipped over a
|
|
48
|
-
# non-MQTT transport and reconstructed via {.from_wire}. v5 wire
|
|
49
|
-
# format is used so the full property block round-trips.
|
|
50
|
-
def to_wire
|
|
51
|
-
packet = to_publish
|
|
52
|
-
packet.encode(version: 5)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
# Inverse of {#to_wire}. Raises +Protocol::MQTT::MalformedPacket+
|
|
57
|
-
# if the bytes don't decode as a PUBLISH packet.
|
|
58
|
-
def self.from_wire(bytes)
|
|
59
|
-
pkt, consumed = Protocol::MQTT::Packet.decode(bytes, version: 5)
|
|
60
|
-
raise Protocol::MQTT::MalformedPacket, "expected PUBLISH, got #{pkt.class}" unless pkt.is_a?(Protocol::MQTT::Packet::Publish)
|
|
61
|
-
raise Protocol::MQTT::MalformedPacket, "trailing bytes after PUBLISH" if consumed != bytes.bytesize
|
|
62
|
-
from_publish(pkt)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
# Build a PUBLISH packet that represents this message. Used both
|
|
67
|
-
# internally (for over-the-wire transmission) and by {#to_wire}.
|
|
68
|
-
def to_publish
|
|
69
|
-
Protocol::MQTT::Packet::Publish.new(
|
|
70
|
-
topic: @topic,
|
|
71
|
-
payload: @payload,
|
|
72
|
-
qos: @qos,
|
|
73
|
-
retain: @retain,
|
|
74
|
-
dup: @dup,
|
|
75
|
-
packet_id: @packet_id,
|
|
76
|
-
properties: @properties,
|
|
77
|
-
)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
# Build a Message from a decoded PUBLISH packet.
|
|
82
|
-
def self.from_publish(pkt, on_ack: nil)
|
|
83
|
-
new(
|
|
84
|
-
topic: pkt.topic,
|
|
85
|
-
payload: pkt.payload,
|
|
86
|
-
qos: pkt.qos,
|
|
87
|
-
retain: pkt.retain,
|
|
88
|
-
dup: pkt.dup,
|
|
89
|
-
packet_id: pkt.packet_id,
|
|
90
|
-
properties: pkt.properties,
|
|
91
|
-
on_ack: on_ack,
|
|
92
|
-
)
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def ==(other)
|
|
97
|
-
other.is_a?(Message) &&
|
|
98
|
-
other.topic == @topic &&
|
|
99
|
-
other.payload == @payload &&
|
|
100
|
-
other.qos == @qos &&
|
|
101
|
-
other.retain == @retain &&
|
|
102
|
-
other.dup == @dup &&
|
|
103
|
-
other.packet_id == @packet_id &&
|
|
104
|
-
other.properties == @properties
|
|
105
|
-
end
|
|
106
|
-
alias eql? ==
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def hash
|
|
110
|
-
[self.class, @topic, @payload, @qos, @retain, @dup, @packet_id, @properties].hash
|
|
111
|
-
end
|
|
7
|
+
# @!attribute [r] topic
|
|
8
|
+
# @return [String]
|
|
9
|
+
# @!attribute [r] payload
|
|
10
|
+
# @return [String]
|
|
11
|
+
# @!attribute [r] qos
|
|
12
|
+
# @return [Integer] delivery QoS, 0 to 2
|
|
13
|
+
# @!attribute [r] retain
|
|
14
|
+
# @return [Boolean] a retained message, or the publisher's flag under
|
|
15
|
+
# Retain As Published
|
|
16
|
+
# @!attribute [r] subscription_ids
|
|
17
|
+
# @return [Array<Integer>] v5 Subscription Identifiers of the matching
|
|
18
|
+
# subscriptions
|
|
19
|
+
Message = Data.define(:topic, :payload, :qos, :retain, :subscription_ids) do
|
|
20
|
+
alias_method :retain?, :retain
|
|
112
21
|
end
|
|
113
22
|
end
|
data/lib/zzq/options.rb
CHANGED
|
@@ -1,94 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module ZZQ
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
attr_accessor :write_timeout
|
|
18
|
-
attr_accessor :reconnect_interval
|
|
19
|
-
attr_accessor :send_hwm
|
|
20
|
-
attr_accessor :recv_hwm
|
|
21
|
-
|
|
22
|
-
# --- MQTT wire ------------------------------------------------------
|
|
23
|
-
# 3 = MQTT v3.1.1, 5 = MQTT v5.0
|
|
24
|
-
attr_accessor :version
|
|
25
|
-
|
|
26
|
-
# Client identifier. Client-only; required unless empty-id + v5 is
|
|
27
|
-
# used (server assigns it).
|
|
28
|
-
attr_accessor :client_id
|
|
29
|
-
|
|
30
|
-
# Clean Start (v5) / Clean Session (v3.1.1). When true, any prior
|
|
31
|
-
# session state for this client id is discarded on CONNECT.
|
|
32
|
-
attr_accessor :clean_start
|
|
33
|
-
|
|
34
|
-
# Keep-alive interval in seconds. The broker will close the
|
|
35
|
-
# connection after 1.5× this period with no traffic. 0 disables.
|
|
36
|
-
attr_accessor :keep_alive
|
|
37
|
-
|
|
38
|
-
# Session expiry interval (v5 only), in seconds. Only meaningful
|
|
39
|
-
# when clean_start is false.
|
|
40
|
-
attr_accessor :session_expiry_interval
|
|
41
|
-
|
|
42
|
-
# Maximum outstanding unacknowledged QoS 1/2 messages (v5).
|
|
43
|
-
attr_accessor :receive_maximum
|
|
44
|
-
|
|
45
|
-
# Maximum packet size this peer is willing to accept (v5). nil =
|
|
46
|
-
# unlimited.
|
|
47
|
-
attr_accessor :max_packet_size
|
|
48
|
-
|
|
49
|
-
# Topic Alias Maximum we advertise (v5).
|
|
50
|
-
attr_accessor :topic_alias_maximum
|
|
51
|
-
|
|
52
|
-
# MQTT username / password (both optional).
|
|
53
|
-
attr_accessor :username
|
|
54
|
-
attr_accessor :password
|
|
55
|
-
|
|
56
|
-
# Last Will and Testament, client-side. Nil or a Hash of
|
|
57
|
-
# { topic:, payload:, qos:, retain:, properties: }.
|
|
58
|
-
attr_accessor :will
|
|
59
|
-
|
|
60
|
-
# Broker-side persistence backend. Nil = ephemeral (no durability).
|
|
61
|
-
# Pass an instance of a {ZZQ::Persistence::Interface} subclass
|
|
62
|
-
# (e.g. {ZZQ::Persistence::PStore}) to survive process restarts.
|
|
63
|
-
attr_accessor :persistence
|
|
4
|
+
# @api private
|
|
5
|
+
module Options
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# +given+ with String keys, after checking every key is +allowed+.
|
|
9
|
+
def check(given, allowed)
|
|
10
|
+
unknown = given.keys - allowed
|
|
11
|
+
unless unknown.empty?
|
|
12
|
+
raise ArgumentError, "unknown option#{"s" if unknown.size > 1}: #{unknown.join(", ")}"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
stringify(given)
|
|
16
|
+
end
|
|
64
17
|
|
|
18
|
+
def stringify(hash)
|
|
19
|
+
hash.to_h { |key, value| [key.to_s, value.is_a?(Hash) ? stringify(value) : value] }
|
|
20
|
+
end
|
|
65
21
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
@reconnect_interval = 0.1
|
|
78
|
-
@send_hwm = send_hwm
|
|
79
|
-
@recv_hwm = recv_hwm
|
|
80
|
-
@version = version
|
|
81
|
-
@client_id = client_id
|
|
82
|
-
@clean_start = clean_start
|
|
83
|
-
@keep_alive = keep_alive
|
|
84
|
-
@session_expiry_interval = session_expiry_interval
|
|
85
|
-
@receive_maximum = receive_maximum
|
|
86
|
-
@max_packet_size = max_packet_size
|
|
87
|
-
@topic_alias_maximum = topic_alias_maximum
|
|
88
|
-
@username = username
|
|
89
|
-
@password = password
|
|
90
|
-
@will = will
|
|
91
|
-
@persistence = persistence
|
|
22
|
+
# +mqtt://+ and +mqtts://+ as the +tcp://+ and +tls://+ endpoints they
|
|
23
|
+
# name. Other schemes pass through.
|
|
24
|
+
def endpoint(uri)
|
|
25
|
+
uri = uri.to_s
|
|
26
|
+
if uri.start_with?("mqtt://")
|
|
27
|
+
"tcp://#{uri.delete_prefix("mqtt://")}"
|
|
28
|
+
elsif uri.start_with?("mqtts://")
|
|
29
|
+
"tls://#{uri.delete_prefix("mqtts://")}"
|
|
30
|
+
else
|
|
31
|
+
uri
|
|
32
|
+
end
|
|
92
33
|
end
|
|
93
34
|
end
|
|
94
35
|
end
|
data/lib/zzq/version.rb
CHANGED
data/lib/zzq/waiting.rb
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ZZQ
|
|
4
|
+
module Native
|
|
5
|
+
# A pipe a fiber waits on while a Rust future is pending.
|
|
6
|
+
class Wake
|
|
7
|
+
# Waits for a wake, raising IO::TimeoutError past +deadline+.
|
|
8
|
+
#
|
|
9
|
+
# @param deadline [Float, nil] monotonic clock time
|
|
10
|
+
def wait(deadline)
|
|
11
|
+
timeout = deadline && [deadline - Waiting.now, 0].max
|
|
12
|
+
@io ||= IO.for_fd(fd, autoclose: false)
|
|
13
|
+
raise IO::TimeoutError, "timed out" unless @io.wait_readable(timeout)
|
|
14
|
+
|
|
15
|
+
clear
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Waits for native ops without blocking other fibers: under a fiber
|
|
21
|
+
# scheduler only the calling fiber waits, otherwise the thread waits
|
|
22
|
+
# without holding the GVL.
|
|
23
|
+
#
|
|
24
|
+
# @api private
|
|
25
|
+
module Waiting
|
|
26
|
+
# Wakes kept for reuse. More are made while more fibers wait at once.
|
|
27
|
+
WAKES = []
|
|
28
|
+
KEEP = 64
|
|
29
|
+
|
|
30
|
+
module_function
|
|
31
|
+
|
|
32
|
+
def now
|
|
33
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def deadline(timeout)
|
|
37
|
+
timeout && now + timeout
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Yields a wake for this call's waits.
|
|
41
|
+
def with_wake
|
|
42
|
+
wake = WAKES.pop || Native::Wake.new
|
|
43
|
+
yield wake
|
|
44
|
+
ensure
|
|
45
|
+
WAKES.push(wake) if wake && WAKES.size < KEEP
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Polls +op+ until it is done and returns its result. An interrupted
|
|
49
|
+
# wait cancels it.
|
|
50
|
+
def await(op, timeout = nil)
|
|
51
|
+
with_wake do |wake|
|
|
52
|
+
limit = deadline(timeout)
|
|
53
|
+
loop do
|
|
54
|
+
result = op.poll(wake)
|
|
55
|
+
return result unless Native::PENDING.equal?(result)
|
|
56
|
+
|
|
57
|
+
wake.wait(limit)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
61
|
+
op.cancel
|
|
62
|
+
raise
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/zzq.rb
CHANGED
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
#
|
|
5
|
-
# Built on `async`, `io-stream`, and `protocol-mqtt`. Applies the
|
|
6
|
-
# omq/nnq architecture (Socket base, Engine, pluggable transports,
|
|
7
|
-
# barrier-scoped lifecycle) to MQTT 3.1.1 and 5.0.
|
|
8
|
-
module ZZQ
|
|
9
|
-
end
|
|
3
|
+
require "io/wait"
|
|
10
4
|
|
|
11
5
|
require_relative "zzq/version"
|
|
12
|
-
require_relative "zzq/
|
|
13
|
-
require_relative "zzq/monitor_event"
|
|
14
|
-
require_relative "zzq/options"
|
|
6
|
+
require_relative "zzq/errors"
|
|
15
7
|
require_relative "zzq/message"
|
|
16
|
-
require_relative "zzq/
|
|
17
|
-
require_relative "zzq/
|
|
18
|
-
require_relative "zzq/
|
|
19
|
-
require_relative "zzq/transport/mqtt"
|
|
20
|
-
require_relative "zzq/transport/ipc"
|
|
21
|
-
require_relative "zzq/transport/mqtts"
|
|
22
|
-
require_relative "zzq/persistence/interface"
|
|
23
|
-
require_relative "zzq/persistence/memory"
|
|
24
|
-
require_relative "zzq/persistence/pstore"
|
|
8
|
+
require_relative "zzq/zzq_native"
|
|
9
|
+
require_relative "zzq/options"
|
|
10
|
+
require_relative "zzq/waiting"
|
|
25
11
|
require_relative "zzq/client"
|
|
26
12
|
require_relative "zzq/broker"
|
|
13
|
+
|
|
14
|
+
# MQTT 3.1.1 and 5.0 client and broker, backed by the zzq Rust crates.
|
|
15
|
+
module ZZQ
|
|
16
|
+
class << self
|
|
17
|
+
# @return [Integer] IO lanes: threads that run connections
|
|
18
|
+
def io_threads
|
|
19
|
+
Native.io_threads
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Sets the number of IO lanes, before the first client or broker.
|
|
23
|
+
#
|
|
24
|
+
# @param count [Integer]
|
|
25
|
+
def io_threads=(count)
|
|
26
|
+
Native.io_threads = count
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|