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
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
//! `ZZQ::Native::Op`: a Rust future that outlives one native call, such as
|
|
2
|
+
//! a connect or the wait for a PUBACK. Ruby polls it with a fiber's
|
|
3
|
+
//! [`Wake`](crate::wake::Wake) until it is done.
|
|
4
|
+
|
|
5
|
+
use std::future::Future;
|
|
6
|
+
use std::pin::Pin;
|
|
7
|
+
use std::sync::{Arc, Mutex, OnceLock, PoisonError};
|
|
8
|
+
use std::task::Poll;
|
|
9
|
+
|
|
10
|
+
use rb_sys::VALUE;
|
|
11
|
+
use zzq_broker::FilePersistence;
|
|
12
|
+
use zzq_client::{Acked, ReasonCode};
|
|
13
|
+
|
|
14
|
+
use crate::error::Error;
|
|
15
|
+
use crate::rb::{self, DataType, RbResult};
|
|
16
|
+
use crate::{broker, client, wake};
|
|
17
|
+
|
|
18
|
+
static TYPE: DataType = DataType::new::<Op>(c"ZZQ::Native::Op");
|
|
19
|
+
static CLASS: OnceLock<usize> = OnceLock::new();
|
|
20
|
+
static PENDING: OnceLock<usize> = OnceLock::new();
|
|
21
|
+
|
|
22
|
+
/// What a finished op returns to Ruby.
|
|
23
|
+
pub enum Out {
|
|
24
|
+
Nil,
|
|
25
|
+
Codes(Vec<ReasonCode>),
|
|
26
|
+
Client(zzq_client::Client),
|
|
27
|
+
Broker(zzq_broker::Broker, Option<Arc<FilePersistence>>),
|
|
28
|
+
Endpoint(String),
|
|
29
|
+
Stats(zzq_broker::Stats),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
pub type Fut = Pin<Box<dyn Future<Output = Result<Out, Error>> + Send>>;
|
|
33
|
+
|
|
34
|
+
enum Task {
|
|
35
|
+
Acked(Acked),
|
|
36
|
+
Fut(Fut),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// A future Ruby polls until it is done.
|
|
40
|
+
pub struct Op {
|
|
41
|
+
task: Mutex<Option<Task>>,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
impl std::fmt::Debug for Op {
|
|
45
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
46
|
+
f.debug_struct("Op").finish_non_exhaustive()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// `ZZQ::Native::PENDING`: a poll found the op not done.
|
|
51
|
+
pub fn pending() -> VALUE {
|
|
52
|
+
*PENDING.get().expect("registered") as VALUE
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// A new op for `fut`.
|
|
56
|
+
pub fn new(fut: impl Future<Output = Result<Out, Error>> + Send + 'static) -> RbResult<VALUE> {
|
|
57
|
+
wrap(Task::Fut(Box::pin(fut)))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// A new op for the ack of a queued publish.
|
|
61
|
+
pub fn acked(acked: Acked) -> RbResult<VALUE> {
|
|
62
|
+
wrap(Task::Acked(acked))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fn wrap(task: Task) -> RbResult<VALUE> {
|
|
66
|
+
let op = Op {
|
|
67
|
+
task: Mutex::new(Some(task)),
|
|
68
|
+
};
|
|
69
|
+
TYPE.wrap(*CLASS.get().expect("registered") as VALUE, op)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fn get(value: VALUE) -> RbResult<&'static Op> {
|
|
73
|
+
unsafe { TYPE.get(value) }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fn poll(this: VALUE, wake: VALUE) -> RbResult<VALUE> {
|
|
77
|
+
let op = get(this)?;
|
|
78
|
+
let wake = wake::get(wake)?;
|
|
79
|
+
let mut task = op.task.lock().unwrap_or_else(PoisonError::into_inner);
|
|
80
|
+
let res = match task.as_mut() {
|
|
81
|
+
Some(Task::Acked(acked)) => match wake.poll(Pin::new(acked)) {
|
|
82
|
+
Poll::Ready(res) => res.map(|()| Out::Nil).map_err(Error::from),
|
|
83
|
+
Poll::Pending => return Ok(pending()),
|
|
84
|
+
},
|
|
85
|
+
Some(Task::Fut(fut)) => match wake.poll(fut.as_mut()) {
|
|
86
|
+
Poll::Ready(res) => res,
|
|
87
|
+
Poll::Pending => return Ok(pending()),
|
|
88
|
+
},
|
|
89
|
+
None => return Err(Error::Client(zzq_client::Error::Closed).into()),
|
|
90
|
+
};
|
|
91
|
+
*task = None;
|
|
92
|
+
drop(task);
|
|
93
|
+
to_ruby(res?)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fn to_ruby(out: Out) -> RbResult<VALUE> {
|
|
97
|
+
match out {
|
|
98
|
+
Out::Nil => Ok(rb::qnil()),
|
|
99
|
+
Out::Codes(codes) => {
|
|
100
|
+
let array = rb::array_new_capa(codes.len())?;
|
|
101
|
+
for code in codes {
|
|
102
|
+
rb::array_push(array, rb::u64_value(u8::from(code).into()))?;
|
|
103
|
+
}
|
|
104
|
+
Ok(array)
|
|
105
|
+
}
|
|
106
|
+
Out::Client(c) => client::wrap(c),
|
|
107
|
+
Out::Broker(b, store) => broker::wrap(b, store),
|
|
108
|
+
Out::Endpoint(e) => rb::new_utf8_string(&e),
|
|
109
|
+
Out::Stats(s) => broker::stats_hash(&s),
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
unsafe extern "C" fn op_poll(this: VALUE, wake: VALUE) -> VALUE {
|
|
114
|
+
rb::wrap(|| poll(this, wake))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
unsafe extern "C" fn op_cancel(this: VALUE) -> VALUE {
|
|
118
|
+
rb::wrap(|| {
|
|
119
|
+
let task = get(this)?
|
|
120
|
+
.task
|
|
121
|
+
.lock()
|
|
122
|
+
.unwrap_or_else(PoisonError::into_inner)
|
|
123
|
+
.take();
|
|
124
|
+
drop(task);
|
|
125
|
+
Ok(rb::qnil())
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
pub fn register(native: VALUE) -> RbResult<()> {
|
|
130
|
+
let class = rb::define_class_under(native, c"Op")?;
|
|
131
|
+
let _ = CLASS.set(class as usize);
|
|
132
|
+
rb::define_method(class, c"poll", op_poll as *const (), 1)?;
|
|
133
|
+
rb::define_method(class, c"cancel", op_cancel as *const (), 0)?;
|
|
134
|
+
let pending = rb::freeze(rb::protect(|| unsafe {
|
|
135
|
+
rb_sys::rb_obj_alloc(rb_sys::rb_cObject)
|
|
136
|
+
})?);
|
|
137
|
+
rb::define_const(native, c"PENDING", pending)?;
|
|
138
|
+
let _ = PENDING.set(pending as usize);
|
|
139
|
+
Ok(())
|
|
140
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
//! Client and broker options from the String-keyed Hash that the Ruby
|
|
2
|
+
//! side checked. A nil value keeps the default, except where nil means
|
|
3
|
+
//! "none".
|
|
4
|
+
|
|
5
|
+
use std::sync::Arc;
|
|
6
|
+
use std::time::Duration;
|
|
7
|
+
|
|
8
|
+
use rb_sys::VALUE;
|
|
9
|
+
use zzq_broker::{BrokerOptions, FilePersistence};
|
|
10
|
+
use zzq_client::{ClientOptions, LastWill, QoS, Reconnect, TlsOptions, TransportOptions, Version};
|
|
11
|
+
|
|
12
|
+
use crate::error;
|
|
13
|
+
use crate::rb::{self, RbResult, RubyErr};
|
|
14
|
+
|
|
15
|
+
/// The value under `key`, unless missing or nil.
|
|
16
|
+
fn get(h: VALUE, key: &str) -> RbResult<Option<VALUE>> {
|
|
17
|
+
Ok(rb::hash_get(h, key)?.filter(|v| *v != rb::qnil()))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
fn int<T: TryFrom<i64>>(h: VALUE, key: &str) -> RbResult<Option<T>> {
|
|
21
|
+
get(h, key)?
|
|
22
|
+
.map(|v| {
|
|
23
|
+
let n = rb::value_to_i64(v)?;
|
|
24
|
+
T::try_from(n).map_err(|_| RubyErr::arg(format!("{key} out of range: {n}")))
|
|
25
|
+
})
|
|
26
|
+
.transpose()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn secs(key: &str, v: VALUE) -> RbResult<Duration> {
|
|
30
|
+
let s = rb::value_to_f64(v)?;
|
|
31
|
+
Duration::try_from_secs_f64(s).map_err(|_| RubyErr::arg(format!("{key} must be seconds >= 0")))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fn duration(h: VALUE, key: &str) -> RbResult<Option<Duration>> {
|
|
35
|
+
get(h, key)?.map(|v| secs(key, v)).transpose()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fn string(h: VALUE, key: &str) -> RbResult<Option<String>> {
|
|
39
|
+
get(h, key)?.map(rb::value_to_string).transpose()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn bytes(h: VALUE, key: &str) -> RbResult<Option<Vec<u8>>> {
|
|
43
|
+
get(h, key)?.map(rb::value_to_bytes).transpose()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fn flag(h: VALUE, key: &str) -> RbResult<Option<bool>> {
|
|
47
|
+
get(h, key)?.map(rb::value_to_bool).transpose()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
pub fn qos(n: i64) -> RbResult<QoS> {
|
|
51
|
+
u8::try_from(n)
|
|
52
|
+
.ok()
|
|
53
|
+
.and_then(|n| QoS::try_from(n).ok())
|
|
54
|
+
.ok_or_else(|| RubyErr::arg(format!("QoS must be 0, 1 or 2, not {n}")))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn transport(h: VALUE) -> RbResult<TransportOptions> {
|
|
58
|
+
let mut t = TransportOptions::default();
|
|
59
|
+
if let Some(tls) = get(h, "tls")? {
|
|
60
|
+
t.tls = Some(TlsOptions {
|
|
61
|
+
cert_chain_pem: bytes(tls, "cert")?,
|
|
62
|
+
key_pem: bytes(tls, "key")?,
|
|
63
|
+
trust_pem: bytes(tls, "ca")?,
|
|
64
|
+
client_ca_pem: bytes(tls, "client_ca")?,
|
|
65
|
+
server_name: string(tls, "server_name")?,
|
|
66
|
+
danger_accept_invalid_certs: flag(tls, "insecure")?.unwrap_or(false),
|
|
67
|
+
..TlsOptions::default()
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
Ok(t)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
pub fn client(h: VALUE) -> RbResult<ClientOptions> {
|
|
74
|
+
let mut o = ClientOptions::new(string(h, "client_id")?.unwrap_or_default());
|
|
75
|
+
if let Some(v) = int::<i64>(h, "version")? {
|
|
76
|
+
o.version = match v {
|
|
77
|
+
3 | 4 => Version::V311,
|
|
78
|
+
5 => Version::V5,
|
|
79
|
+
_ => return Err(RubyErr::arg(format!("version must be 3 or 5, not {v}"))),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if let Some(v) = flag(h, "clean_start")? {
|
|
83
|
+
o.clean_start = v;
|
|
84
|
+
}
|
|
85
|
+
if let Some(v) = int(h, "keep_alive")? {
|
|
86
|
+
o.keep_alive = v;
|
|
87
|
+
}
|
|
88
|
+
if let Some(v) = int(h, "session_expiry")? {
|
|
89
|
+
o.session_expiry = v;
|
|
90
|
+
}
|
|
91
|
+
o.username = string(h, "username")?;
|
|
92
|
+
o.password = bytes(h, "password")?.map(Into::into);
|
|
93
|
+
if let Some(w) = get(h, "will")? {
|
|
94
|
+
o.will = Some(LastWill {
|
|
95
|
+
topic: string(w, "topic")?.ok_or_else(|| RubyErr::arg("will needs a topic"))?,
|
|
96
|
+
payload: bytes(w, "payload")?.unwrap_or_default().into(),
|
|
97
|
+
qos: qos(int(w, "qos")?.unwrap_or(0))?,
|
|
98
|
+
retain: flag(w, "retain")?.unwrap_or(false),
|
|
99
|
+
delay: int(w, "delay")?.unwrap_or(0),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if let Some(v) = int(h, "inbox")? {
|
|
103
|
+
o.inbox = v;
|
|
104
|
+
}
|
|
105
|
+
if let Some(r) = get(h, "reconnect")? {
|
|
106
|
+
o.reconnect = if r == rb::qfalse() {
|
|
107
|
+
None
|
|
108
|
+
} else if r == rb::qtrue() {
|
|
109
|
+
Some(Reconnect::default())
|
|
110
|
+
} else {
|
|
111
|
+
let d = Reconnect::default();
|
|
112
|
+
Some(Reconnect {
|
|
113
|
+
min: duration(r, "min")?.unwrap_or(d.min),
|
|
114
|
+
max: duration(r, "max")?.unwrap_or(d.max),
|
|
115
|
+
})
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if let Some(v) = duration(h, "connect_timeout")? {
|
|
119
|
+
o.connect_timeout = v;
|
|
120
|
+
}
|
|
121
|
+
o.transport = transport(h)?;
|
|
122
|
+
Ok(o)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Broker options, and the retained-message store they use.
|
|
126
|
+
pub fn broker(h: VALUE) -> RbResult<(BrokerOptions, Option<Arc<FilePersistence>>)> {
|
|
127
|
+
let mut o = BrokerOptions::default();
|
|
128
|
+
macro_rules! ints {
|
|
129
|
+
($($field:ident),*) => {$(
|
|
130
|
+
if let Some(v) = int(h, stringify!($field))? {
|
|
131
|
+
o.$field = v;
|
|
132
|
+
}
|
|
133
|
+
)*};
|
|
134
|
+
}
|
|
135
|
+
ints!(
|
|
136
|
+
max_packet_size,
|
|
137
|
+
max_inflight,
|
|
138
|
+
max_queued,
|
|
139
|
+
receive_maximum,
|
|
140
|
+
max_subscriptions,
|
|
141
|
+
max_retained,
|
|
142
|
+
max_topic_levels,
|
|
143
|
+
topic_alias_maximum,
|
|
144
|
+
max_session_expiry,
|
|
145
|
+
max_connections
|
|
146
|
+
);
|
|
147
|
+
// Present but nil means no limit.
|
|
148
|
+
if rb::hash_get(h, "max_keep_alive")?.is_some() {
|
|
149
|
+
o.max_keep_alive = int(h, "max_keep_alive")?;
|
|
150
|
+
}
|
|
151
|
+
if rb::hash_get(h, "sys_interval")?.is_some() {
|
|
152
|
+
o.sys_interval = duration(h, "sys_interval")?;
|
|
153
|
+
}
|
|
154
|
+
if let Some(v) = duration(h, "connect_timeout")? {
|
|
155
|
+
o.connect_timeout = v;
|
|
156
|
+
}
|
|
157
|
+
let store = match string(h, "persistence")? {
|
|
158
|
+
Some(path) => {
|
|
159
|
+
Some(Arc::new(FilePersistence::open(&path).map_err(|e| {
|
|
160
|
+
error::zzq(format!("opening {path} failed: {e}"))
|
|
161
|
+
})?))
|
|
162
|
+
}
|
|
163
|
+
None => None,
|
|
164
|
+
};
|
|
165
|
+
o.persistence = store.clone().map(|s| s as Arc<dyn zzq_broker::Persistence>);
|
|
166
|
+
o.transport = transport(h)?;
|
|
167
|
+
Ok((o, store))
|
|
168
|
+
}
|