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
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: zzq
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrik Wenger
|
|
@@ -10,104 +10,60 @@ cert_chain: []
|
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
|
-
name:
|
|
13
|
+
name: rb_sys
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
18
|
+
version: '0.9'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
requirements:
|
|
30
|
-
- - "~>"
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '0.11'
|
|
33
|
-
type: :runtime
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - "~>"
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0.11'
|
|
40
|
-
- !ruby/object:Gem::Dependency
|
|
41
|
-
name: protocol-mqtt
|
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - ">="
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: 0.1.0
|
|
47
|
-
type: :runtime
|
|
48
|
-
prerelease: false
|
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - ">="
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: 0.1.0
|
|
54
|
-
- !ruby/object:Gem::Dependency
|
|
55
|
-
name: pstore
|
|
56
|
-
requirement: !ruby/object:Gem::Requirement
|
|
57
|
-
requirements:
|
|
58
|
-
- - "~>"
|
|
59
|
-
- !ruby/object:Gem::Version
|
|
60
|
-
version: '0.2'
|
|
61
|
-
type: :runtime
|
|
62
|
-
prerelease: false
|
|
63
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
-
requirements:
|
|
65
|
-
- - "~>"
|
|
66
|
-
- !ruby/object:Gem::Version
|
|
67
|
-
version: '0.2'
|
|
68
|
-
description: Pure Ruby implementation of MQTT — client and broker in one gem — built
|
|
69
|
-
on async, io-stream, and protocol-mqtt. Fiber-first, pluggable transports (tcp,
|
|
70
|
-
tls, ws, ipc), pluggable persistence (in-memory, PStore).
|
|
25
|
+
version: '0.9'
|
|
26
|
+
description: 'Ruby binding for zzq: an MQTT 3.1.1 and 5.0 client and broker with an
|
|
27
|
+
MQTT-SN gateway, running on Rust IO threads. Works in threads and under fiber schedulers
|
|
28
|
+
such as Async.'
|
|
71
29
|
email:
|
|
72
30
|
- paddor@gmail.com
|
|
73
31
|
executables: []
|
|
74
|
-
extensions:
|
|
32
|
+
extensions:
|
|
33
|
+
- ext/zzq_native/extconf.rb
|
|
75
34
|
extra_rdoc_files: []
|
|
76
35
|
files:
|
|
77
36
|
- CHANGELOG.md
|
|
78
|
-
- LICENSE
|
|
37
|
+
- LICENSE-APACHE
|
|
38
|
+
- LICENSE-ISC
|
|
79
39
|
- README.md
|
|
40
|
+
- ext/zzq_native/Cargo.toml
|
|
41
|
+
- ext/zzq_native/extconf.rb
|
|
42
|
+
- ext/zzq_native/src/broker.rs
|
|
43
|
+
- ext/zzq_native/src/client.rs
|
|
44
|
+
- ext/zzq_native/src/context.rs
|
|
45
|
+
- ext/zzq_native/src/error.rs
|
|
46
|
+
- ext/zzq_native/src/lib.rs
|
|
47
|
+
- ext/zzq_native/src/op.rs
|
|
48
|
+
- ext/zzq_native/src/options.rs
|
|
49
|
+
- ext/zzq_native/src/rb.rs
|
|
50
|
+
- ext/zzq_native/src/wake.rs
|
|
80
51
|
- lib/zzq.rb
|
|
81
52
|
- lib/zzq/broker.rb
|
|
82
53
|
- lib/zzq/client.rb
|
|
83
|
-
- lib/zzq/
|
|
84
|
-
- lib/zzq/engine/connection_lifecycle.rb
|
|
85
|
-
- lib/zzq/engine/keepalive.rb
|
|
86
|
-
- lib/zzq/engine/socket_lifecycle.rb
|
|
87
|
-
- lib/zzq/error.rb
|
|
54
|
+
- lib/zzq/errors.rb
|
|
88
55
|
- lib/zzq/message.rb
|
|
89
|
-
- lib/zzq/monitor_event.rb
|
|
90
56
|
- lib/zzq/options.rb
|
|
91
|
-
- lib/zzq/persistence/interface.rb
|
|
92
|
-
- lib/zzq/persistence/memory.rb
|
|
93
|
-
- lib/zzq/persistence/pstore.rb
|
|
94
|
-
- lib/zzq/routing/broker.rb
|
|
95
|
-
- lib/zzq/routing/client.rb
|
|
96
|
-
- lib/zzq/routing/packet_id_allocator.rb
|
|
97
|
-
- lib/zzq/routing/qos_tracker.rb
|
|
98
|
-
- lib/zzq/routing/retained_store.rb
|
|
99
|
-
- lib/zzq/routing/topic_match.rb
|
|
100
|
-
- lib/zzq/routing/topic_trie.rb
|
|
101
|
-
- lib/zzq/socket.rb
|
|
102
|
-
- lib/zzq/subscription.rb
|
|
103
|
-
- lib/zzq/transport/ipc.rb
|
|
104
|
-
- lib/zzq/transport/mqtt.rb
|
|
105
|
-
- lib/zzq/transport/mqtts.rb
|
|
106
57
|
- lib/zzq/version.rb
|
|
107
|
-
|
|
58
|
+
- lib/zzq/waiting.rb
|
|
59
|
+
homepage: https://github.com/paddor/zzq/tree/main/bindings/ruby
|
|
108
60
|
licenses:
|
|
109
61
|
- ISC
|
|
110
|
-
|
|
62
|
+
- Apache-2.0
|
|
63
|
+
metadata:
|
|
64
|
+
source_code_uri: https://github.com/paddor/zzq/tree/main/bindings/ruby
|
|
65
|
+
changelog_uri: https://github.com/paddor/zzq/tree/main/bindings/ruby/CHANGELOG.md
|
|
66
|
+
rubygems_mfa_required: 'true'
|
|
111
67
|
rdoc_options: []
|
|
112
68
|
require_paths:
|
|
113
69
|
- lib
|
|
@@ -115,14 +71,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
115
71
|
requirements:
|
|
116
72
|
- - ">="
|
|
117
73
|
- !ruby/object:Gem::Version
|
|
118
|
-
version: '4
|
|
74
|
+
version: '3.4'
|
|
119
75
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
120
76
|
requirements:
|
|
121
77
|
- - ">="
|
|
122
78
|
- !ruby/object:Gem::Version
|
|
123
79
|
version: '0'
|
|
124
80
|
requirements: []
|
|
125
|
-
rubygems_version: 4.0.
|
|
81
|
+
rubygems_version: 4.0.20
|
|
126
82
|
specification_version: 4
|
|
127
|
-
summary:
|
|
83
|
+
summary: MQTT 3.1.1 and 5.0 client and broker for Ruby, backed by Rust
|
|
128
84
|
test_files: []
|
data/LICENSE
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
Copyright (c) 2026, Patrik Wenger
|
|
2
|
-
|
|
3
|
-
Permission to use, copy, modify, and/or distribute this software for any
|
|
4
|
-
purpose with or without fee is hereby granted, provided that the above
|
|
5
|
-
copyright notice and this permission notice appear in all copies.
|
|
6
|
-
|
|
7
|
-
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
8
|
-
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
9
|
-
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
10
|
-
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
11
|
-
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
12
|
-
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
13
|
-
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "async/barrier"
|
|
4
|
-
require "protocol/mqtt"
|
|
5
|
-
|
|
6
|
-
module ZZQ
|
|
7
|
-
class Engine
|
|
8
|
-
# Owns the full arc of one connection: new → handshaking → ready →
|
|
9
|
-
# closed. Centralises side-effect ordering so routing doesn't have
|
|
10
|
-
# to.
|
|
11
|
-
#
|
|
12
|
-
# The MQTT CONNECT/CONNACK exchange itself is NOT performed here —
|
|
13
|
-
# it's role-asymmetric (client writes CONNECT first, broker reads
|
|
14
|
-
# it first) and so lives in the role's routing layer, which calls
|
|
15
|
-
# +handshake_succeeded!+ once the exchange is done.
|
|
16
|
-
#
|
|
17
|
-
# new → handshaking → ready → closed
|
|
18
|
-
#
|
|
19
|
-
# #lost! / #close! are idempotent.
|
|
20
|
-
class ConnectionLifecycle
|
|
21
|
-
class InvalidTransition < RuntimeError; end
|
|
22
|
-
|
|
23
|
-
STATES = %i[new handshaking ready closed].freeze
|
|
24
|
-
|
|
25
|
-
TRANSITIONS = {
|
|
26
|
-
new: %i[handshaking ready closed].freeze,
|
|
27
|
-
handshaking: %i[ready closed].freeze,
|
|
28
|
-
ready: %i[closed].freeze,
|
|
29
|
-
closed: [].freeze,
|
|
30
|
-
}.freeze
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
attr_reader :conn # Protocol::MQTT::Connection
|
|
34
|
-
attr_reader :endpoint
|
|
35
|
-
attr_reader :state
|
|
36
|
-
attr_reader :barrier
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def initialize(engine, endpoint:)
|
|
40
|
-
@engine = engine
|
|
41
|
-
@endpoint = endpoint
|
|
42
|
-
@state = :new
|
|
43
|
-
@conn = nil
|
|
44
|
-
@barrier = Async::Barrier.new(parent: engine.barrier)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
# Wraps the accepted/connected IO in a Protocol::MQTT::Connection
|
|
49
|
-
# and returns it. The handshake itself (CONNECT/CONNACK exchange)
|
|
50
|
-
# is driven by the routing layer — this is "transport ready,
|
|
51
|
-
# MQTT handshake pending".
|
|
52
|
-
def begin_handshake!(io)
|
|
53
|
-
transition!(:handshaking)
|
|
54
|
-
mqtt = Protocol::MQTT::Connection.new(
|
|
55
|
-
io,
|
|
56
|
-
version: @engine.options.version,
|
|
57
|
-
max_packet_size: @engine.options.max_packet_size,
|
|
58
|
-
)
|
|
59
|
-
@conn = mqtt
|
|
60
|
-
mqtt
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
# Called by the routing layer after CONNECT/CONNACK succeeds.
|
|
65
|
-
def handshake_succeeded!
|
|
66
|
-
@engine.connections[@conn] = self
|
|
67
|
-
transition!(:ready)
|
|
68
|
-
begin
|
|
69
|
-
@engine.routing.connection_added(@conn) if @engine.routing.respond_to?(:connection_added)
|
|
70
|
-
rescue ConnectionRejected
|
|
71
|
-
@engine.emit_monitor_event(:connection_rejected, endpoint: @endpoint)
|
|
72
|
-
tear_down!(reconnect: false)
|
|
73
|
-
raise
|
|
74
|
-
end
|
|
75
|
-
@engine.lifecycle.peer_connected.resolve(@conn) unless @engine.lifecycle.peer_connected.resolved?
|
|
76
|
-
@engine.emit_monitor_event(:connected, endpoint: @endpoint)
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
# Called on handshake failure — e.g. refused CONNACK, RST mid-
|
|
81
|
-
# CONNECT. Tears down and requests a reconnect if eligible.
|
|
82
|
-
def handshake_failed!(error:)
|
|
83
|
-
@engine.emit_monitor_event(:handshake_failed, endpoint: @endpoint, detail: { error: error })
|
|
84
|
-
@conn&.close rescue nil
|
|
85
|
-
tear_down!(reconnect: true)
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
# Unexpected loss of an established connection.
|
|
90
|
-
def lost!
|
|
91
|
-
tear_down!(reconnect: true)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
# Deliberate close (engine shutdown, kicked by broker, …).
|
|
96
|
-
def close!
|
|
97
|
-
tear_down!(reconnect: false)
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
# Starts a supervisor task that tears down when any pump exits.
|
|
102
|
-
def start_supervisor!
|
|
103
|
-
return if @barrier.empty?
|
|
104
|
-
@engine.barrier.async(transient: true, annotation: "zzq conn supervisor") do
|
|
105
|
-
@barrier.wait { |task| task.wait; break }
|
|
106
|
-
rescue Async::Stop, Async::Cancel
|
|
107
|
-
rescue *CONNECTION_LOST
|
|
108
|
-
ensure
|
|
109
|
-
lost!
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
private def tear_down!(reconnect: false)
|
|
115
|
-
return if @state == :closed
|
|
116
|
-
transition!(:closed)
|
|
117
|
-
if @conn
|
|
118
|
-
@engine.connections.delete(@conn)
|
|
119
|
-
@engine.routing.connection_removed(@conn) if @engine.routing.respond_to?(:connection_removed)
|
|
120
|
-
@conn.close rescue nil
|
|
121
|
-
@engine.emit_monitor_event(:disconnected, endpoint: @endpoint)
|
|
122
|
-
@engine.resolve_all_peers_gone_if_empty
|
|
123
|
-
end
|
|
124
|
-
@engine.maybe_reconnect(@endpoint) if reconnect
|
|
125
|
-
# Stop after cleanup: when tear_down! is called from a task on
|
|
126
|
-
# this barrier (e.g. the read-loop's ensure), stopping earlier
|
|
127
|
-
# would cancel the current fiber mid-cleanup.
|
|
128
|
-
@barrier.stop
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
private def transition!(new_state)
|
|
133
|
-
allowed = TRANSITIONS[@state]
|
|
134
|
-
raise InvalidTransition, "#{@state} → #{new_state}" unless allowed&.include?(new_state)
|
|
135
|
-
@state = new_state
|
|
136
|
-
end
|
|
137
|
-
end
|
|
138
|
-
end
|
|
139
|
-
end
|
data/lib/zzq/engine/keepalive.rb
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "async"
|
|
4
|
-
require "async/clock"
|
|
5
|
-
require "protocol/mqtt"
|
|
6
|
-
|
|
7
|
-
module ZZQ
|
|
8
|
-
class Engine
|
|
9
|
-
# Keepalive driver for a single connection.
|
|
10
|
-
#
|
|
11
|
-
# +role+:
|
|
12
|
-
# * +:sender+ (client default) — sends PINGREQ if locally idle
|
|
13
|
-
# for keep_alive/2; closes if nothing received for 1.5 × keep_alive.
|
|
14
|
-
# * +:watcher+ (broker default) — does not send; just closes if
|
|
15
|
-
# nothing received for 1.5 × keep_alive. MQTT-3.1.2-22 is
|
|
16
|
-
# enforced by the broker.
|
|
17
|
-
#
|
|
18
|
-
# One fiber per connection, attached to the connection's barrier
|
|
19
|
-
# so it tears down with the connection.
|
|
20
|
-
class Keepalive
|
|
21
|
-
attr_accessor :last_sent_at, :last_received_at
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def initialize(conn, keep_alive:, on_timeout:, role: :sender)
|
|
25
|
-
@conn = conn
|
|
26
|
-
@keep_alive = keep_alive
|
|
27
|
-
@on_timeout = on_timeout
|
|
28
|
-
@role = role
|
|
29
|
-
now = Async::Clock.now
|
|
30
|
-
@last_sent_at = now
|
|
31
|
-
@last_received_at = now
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
# Call this every time a packet is written, to reset the send-idle
|
|
36
|
-
# timer.
|
|
37
|
-
def note_sent
|
|
38
|
-
@last_sent_at = Async::Clock.now
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# Call this every time a packet is read, to reset the recv-idle
|
|
43
|
-
# timer.
|
|
44
|
-
def note_received
|
|
45
|
-
@last_received_at = Async::Clock.now
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def run
|
|
50
|
-
return if @keep_alive.zero? # 0 disables keepalive
|
|
51
|
-
loop do
|
|
52
|
-
sleep(@keep_alive / 2.0)
|
|
53
|
-
now = Async::Clock.now
|
|
54
|
-
|
|
55
|
-
if (now - @last_received_at) > (@keep_alive * 1.5)
|
|
56
|
-
@on_timeout.call
|
|
57
|
-
break
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
next unless @role == :sender
|
|
61
|
-
|
|
62
|
-
if (now - @last_sent_at) >= (@keep_alive / 2.0)
|
|
63
|
-
begin
|
|
64
|
-
@conn.write_packet(Protocol::MQTT::Packet::Pingreq.new)
|
|
65
|
-
note_sent
|
|
66
|
-
rescue IOError, Errno::EPIPE, Errno::ECONNRESET
|
|
67
|
-
break
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
rescue Async::Stop
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "async/barrier"
|
|
4
|
-
require "async/promise"
|
|
5
|
-
|
|
6
|
-
module ZZQ
|
|
7
|
-
class Engine
|
|
8
|
-
# Owns the socket-level state: `:new → :open → :closing → :closed`.
|
|
9
|
-
#
|
|
10
|
-
# Adapted from NNQ::Engine::SocketLifecycle, minus the shared-IO-
|
|
11
|
-
# thread fallback (ZZQ is pure-Async).
|
|
12
|
-
class SocketLifecycle
|
|
13
|
-
class InvalidTransition < RuntimeError; end
|
|
14
|
-
|
|
15
|
-
STATES = %i[new open closing closed].freeze
|
|
16
|
-
|
|
17
|
-
TRANSITIONS = {
|
|
18
|
-
new: %i[open closed].freeze,
|
|
19
|
-
open: %i[closing closed].freeze,
|
|
20
|
-
closing: %i[closed].freeze,
|
|
21
|
-
closed: [].freeze,
|
|
22
|
-
}.freeze
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
attr_reader :state
|
|
26
|
-
attr_reader :parent_task
|
|
27
|
-
attr_reader :peer_connected
|
|
28
|
-
attr_reader :all_peers_gone
|
|
29
|
-
attr_reader :barrier
|
|
30
|
-
attr_accessor :reconnect_enabled
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def initialize
|
|
34
|
-
@state = :new
|
|
35
|
-
@parent_task = nil
|
|
36
|
-
@peer_connected = Async::Promise.new
|
|
37
|
-
@all_peers_gone = Async::Promise.new
|
|
38
|
-
@reconnect_enabled = true
|
|
39
|
-
@barrier = nil
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def open? = @state == :open
|
|
44
|
-
def closing? = @state == :closing
|
|
45
|
-
def closed? = @state == :closed
|
|
46
|
-
def alive? = @state == :new || @state == :open
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
# Must be called from within an Async::Task. Raises
|
|
50
|
-
# NotInAsyncContext otherwise.
|
|
51
|
-
def capture_parent_task
|
|
52
|
-
return false if @parent_task
|
|
53
|
-
task = Async::Task.current? or raise NotInAsyncContext
|
|
54
|
-
@parent_task = task
|
|
55
|
-
@barrier = Async::Barrier.new(parent: @parent_task)
|
|
56
|
-
transition!(:open)
|
|
57
|
-
true
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def start_closing! = transition!(:closing)
|
|
62
|
-
def finish_closing! = transition!(:closed)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def resolve_all_peers_gone_if_empty(connections)
|
|
66
|
-
return unless @peer_connected.resolved? && connections.empty?
|
|
67
|
-
return if @all_peers_gone.resolved?
|
|
68
|
-
@all_peers_gone.resolve(true)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
private def transition!(new_state)
|
|
73
|
-
allowed = TRANSITIONS[@state]
|
|
74
|
-
raise InvalidTransition, "#{@state} → #{new_state}" unless allowed&.include?(new_state)
|
|
75
|
-
@state = new_state
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
data/lib/zzq/engine.rb
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "async"
|
|
4
|
-
require "async/clock"
|
|
5
|
-
require "set"
|
|
6
|
-
require_relative "error"
|
|
7
|
-
require_relative "engine/socket_lifecycle"
|
|
8
|
-
require_relative "engine/connection_lifecycle"
|
|
9
|
-
|
|
10
|
-
module ZZQ
|
|
11
|
-
# Per-socket orchestrator. Owns listeners, the connection map, the
|
|
12
|
-
# transport registry, and the socket-level state machine. Adapted
|
|
13
|
-
# from NNQ::Engine minus reactor / inproc / SP-handshake concerns.
|
|
14
|
-
class Engine
|
|
15
|
-
# Scheme → transport module registry. Populated by each transport
|
|
16
|
-
# file on require.
|
|
17
|
-
@transports = {}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class << self
|
|
21
|
-
attr_reader :transports
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
attr_reader :options
|
|
26
|
-
attr_reader :routing
|
|
27
|
-
attr_reader :connections # Hash{Protocol::MQTT::Connection => ConnectionLifecycle}
|
|
28
|
-
attr_reader :lifecycle
|
|
29
|
-
attr_reader :last_endpoint
|
|
30
|
-
attr_reader :dialed
|
|
31
|
-
attr_accessor :monitor_queue
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# @yieldparam engine [Engine] build_routing hook; returns the
|
|
35
|
-
# role-specific routing strategy.
|
|
36
|
-
def initialize(options:)
|
|
37
|
-
@options = options
|
|
38
|
-
@connections = {}
|
|
39
|
-
@listeners = []
|
|
40
|
-
@lifecycle = SocketLifecycle.new
|
|
41
|
-
@last_endpoint = nil
|
|
42
|
-
@monitor_queue = nil
|
|
43
|
-
@dialed = Set.new
|
|
44
|
-
@dial_opts = {}
|
|
45
|
-
@routing = yield(self)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def emit_monitor_event(type, endpoint: nil, detail: nil)
|
|
50
|
-
return unless @monitor_queue
|
|
51
|
-
@monitor_queue.enqueue(MonitorEvent.new(type: type, endpoint: endpoint, detail: detail))
|
|
52
|
-
rescue Async::Stop
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
def parent_task = @lifecycle.parent_task
|
|
57
|
-
def barrier = @lifecycle.barrier
|
|
58
|
-
def closed? = @lifecycle.closed?
|
|
59
|
-
def peer_connected = @lifecycle.peer_connected
|
|
60
|
-
def all_peers_gone = @lifecycle.all_peers_gone
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def resolve_all_peers_gone_if_empty
|
|
64
|
-
@lifecycle.resolve_all_peers_gone_if_empty(@connections)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
# Must be called from inside an Async::Task.
|
|
69
|
-
def capture_parent_task
|
|
70
|
-
@lifecycle.capture_parent_task
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
# Bind (listener) — role-agnostic. The routing layer (on accept)
|
|
75
|
-
# drives the MQTT handshake.
|
|
76
|
-
def bind(endpoint, **opts)
|
|
77
|
-
transport = transport_for(endpoint)
|
|
78
|
-
listener = transport.bind(endpoint, self, **opts)
|
|
79
|
-
listener.start_accept_loop(@lifecycle.barrier) do |io|
|
|
80
|
-
handle_accepted(io, endpoint: endpoint)
|
|
81
|
-
end
|
|
82
|
-
@listeners << listener
|
|
83
|
-
@last_endpoint = listener.endpoint
|
|
84
|
-
emit_monitor_event(:listening, endpoint: @last_endpoint)
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
# Connect (dialer).
|
|
89
|
-
def connect(endpoint, **opts)
|
|
90
|
-
@dialed << endpoint
|
|
91
|
-
@dial_opts[endpoint] = opts unless opts.empty?
|
|
92
|
-
@last_endpoint = endpoint
|
|
93
|
-
transport_for(endpoint).connect(endpoint, self, **opts)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def dial_opts_for(endpoint)
|
|
98
|
-
@dial_opts[endpoint] || {}
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def maybe_reconnect(endpoint)
|
|
103
|
-
# Reconnect loop not wired up yet (M4 job).
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def transport_for(endpoint)
|
|
108
|
-
scheme = endpoint[/\A([a-z+]+):\/\//i, 1] or raise Error, "no scheme: #{endpoint}"
|
|
109
|
-
Engine.transports[scheme] or raise Error, "unsupported transport: #{scheme}"
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
# Called by transports for each accepted connection. Hands off to
|
|
114
|
-
# routing so the broker can read CONNECT / write CONNACK.
|
|
115
|
-
def handle_accepted(io, endpoint:)
|
|
116
|
-
lifecycle = ConnectionLifecycle.new(self, endpoint: endpoint)
|
|
117
|
-
mqtt = lifecycle.begin_handshake!(io)
|
|
118
|
-
@routing.handle_accepted(mqtt, lifecycle) if @routing.respond_to?(:handle_accepted)
|
|
119
|
-
rescue => e
|
|
120
|
-
lifecycle.handshake_failed!(error: e)
|
|
121
|
-
raise unless e.is_a?(ConnectionRejected) || CONNECTION_LOST.any? { |c| e.is_a?(c) }
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
# Called by transports for each dialed connection. Hands off to
|
|
126
|
-
# routing so the client can write CONNECT / read CONNACK.
|
|
127
|
-
def handle_connected(io, endpoint:)
|
|
128
|
-
lifecycle = ConnectionLifecycle.new(self, endpoint: endpoint)
|
|
129
|
-
mqtt = lifecycle.begin_handshake!(io)
|
|
130
|
-
@routing.handle_connected(mqtt, lifecycle) if @routing.respond_to?(:handle_connected)
|
|
131
|
-
rescue => e
|
|
132
|
-
lifecycle.handshake_failed!(error: e)
|
|
133
|
-
raise unless e.is_a?(ConnectionRejected) || CONNECTION_LOST.any? { |c| e.is_a?(c) }
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def spawn_task(annotation:, parent: @lifecycle.barrier, &block)
|
|
138
|
-
parent.async(annotation: annotation, &block)
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
def close
|
|
143
|
-
return unless @lifecycle.alive?
|
|
144
|
-
|
|
145
|
-
@lifecycle.start_closing!
|
|
146
|
-
@listeners.each(&:stop)
|
|
147
|
-
@routing.close if @routing.respond_to?(:close)
|
|
148
|
-
@connections.values.each(&:close!)
|
|
149
|
-
|
|
150
|
-
emit_monitor_event(:closed)
|
|
151
|
-
@monitor_queue&.enqueue(nil)
|
|
152
|
-
|
|
153
|
-
@lifecycle.barrier&.stop
|
|
154
|
-
@lifecycle.finish_closing!
|
|
155
|
-
@lifecycle.peer_connected.resolve(nil) unless @lifecycle.peer_connected.resolved?
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def handle_connection_lost(conn)
|
|
160
|
-
@connections[conn]&.lost!
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
end
|
data/lib/zzq/error.rb
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ZZQ
|
|
4
|
-
class Error < StandardError; end
|
|
5
|
-
|
|
6
|
-
# Raised when sending on or interacting with a closed socket.
|
|
7
|
-
class ClosedError < Error; end
|
|
8
|
-
|
|
9
|
-
# Raised when the routing layer refuses to accept a peer (e.g. a
|
|
10
|
-
# duplicate client id collision on the broker).
|
|
11
|
-
class ConnectionRejected < Error; end
|
|
12
|
-
|
|
13
|
-
# Raised when a socket method is called outside an Async::Task. ZZQ
|
|
14
|
-
# is fiber-first: wrap calls in `Async do ... end` or use the block
|
|
15
|
-
# form of #connect / #bind.
|
|
16
|
-
class NotInAsyncContext < Error
|
|
17
|
-
def initialize(msg = "ZZQ socket methods must run inside an Async task. " \
|
|
18
|
-
"Wrap in `Async do ... end`, or use the block form " \
|
|
19
|
-
"of #connect/#bind.")
|
|
20
|
-
super
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Server responded with a non-success CONNACK.
|
|
25
|
-
class ConnectionRefused < Error
|
|
26
|
-
attr_reader :reason_code
|
|
27
|
-
|
|
28
|
-
def initialize(reason_code, message = nil)
|
|
29
|
-
@reason_code = reason_code
|
|
30
|
-
super(message || "broker refused connect: reason=0x#{reason_code.to_s(16)}")
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Authentication failed.
|
|
35
|
-
class AuthFailed < Error; end
|
|
36
|
-
|
|
37
|
-
# Network errors that indicate the connection is gone.
|
|
38
|
-
CONNECTION_LOST = [
|
|
39
|
-
Errno::ECONNRESET, Errno::EPIPE, Errno::ENOTCONN, Errno::ECONNABORTED,
|
|
40
|
-
Errno::EBADF, EOFError, IOError
|
|
41
|
-
].freeze
|
|
42
|
-
end
|
data/lib/zzq/monitor_event.rb
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ZZQ
|
|
4
|
-
# A single event emitted by a Socket's #monitor task. Event types
|
|
5
|
-
# cover both infrastructure lifecycle (:listening, :connected,
|
|
6
|
-
# :disconnected, :closed) and MQTT semantics (:session_resumed,
|
|
7
|
-
# :session_expired, :retained_updated, :publish_delivered,
|
|
8
|
-
# :will_published).
|
|
9
|
-
MonitorEvent = Data.define(:type, :endpoint, :detail) do
|
|
10
|
-
def initialize(type:, endpoint: nil, detail: nil)
|
|
11
|
-
super
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|