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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/LICENSE-APACHE +202 -0
  4. data/LICENSE-ISC +15 -0
  5. data/README.md +67 -122
  6. data/ext/zzq_native/Cargo.toml +35 -0
  7. data/ext/zzq_native/extconf.rb +8 -0
  8. data/ext/zzq_native/src/broker.rs +163 -0
  9. data/ext/zzq_native/src/client.rs +294 -0
  10. data/ext/zzq_native/src/context.rs +51 -0
  11. data/ext/zzq_native/src/error.rs +117 -0
  12. data/ext/zzq_native/src/lib.rs +52 -0
  13. data/ext/zzq_native/src/op.rs +140 -0
  14. data/ext/zzq_native/src/options.rs +168 -0
  15. data/ext/zzq_native/src/rb.rs +424 -0
  16. data/ext/zzq_native/src/wake.rs +155 -0
  17. data/lib/zzq/broker.rb +90 -21
  18. data/lib/zzq/client.rb +202 -35
  19. data/lib/zzq/errors.rb +37 -0
  20. data/lib/zzq/message.rb +16 -107
  21. data/lib/zzq/options.rb +27 -86
  22. data/lib/zzq/version.rb +2 -1
  23. data/lib/zzq/waiting.rb +65 -0
  24. data/lib/zzq.rb +22 -19
  25. metadata +33 -77
  26. data/LICENSE +0 -13
  27. data/lib/zzq/engine/connection_lifecycle.rb +0 -139
  28. data/lib/zzq/engine/keepalive.rb +0 -75
  29. data/lib/zzq/engine/socket_lifecycle.rb +0 -79
  30. data/lib/zzq/engine.rb +0 -169
  31. data/lib/zzq/error.rb +0 -42
  32. data/lib/zzq/monitor_event.rb +0 -14
  33. data/lib/zzq/persistence/interface.rb +0 -43
  34. data/lib/zzq/persistence/memory.rb +0 -37
  35. data/lib/zzq/persistence/pstore.rb +0 -66
  36. data/lib/zzq/routing/broker.rb +0 -380
  37. data/lib/zzq/routing/client.rb +0 -327
  38. data/lib/zzq/routing/packet_id_allocator.rb +0 -54
  39. data/lib/zzq/routing/qos_tracker.rb +0 -108
  40. data/lib/zzq/routing/retained_store.rb +0 -67
  41. data/lib/zzq/routing/topic_match.rb +0 -49
  42. data/lib/zzq/routing/topic_trie.rb +0 -232
  43. data/lib/zzq/socket.rb +0 -146
  44. data/lib/zzq/subscription.rb +0 -70
  45. data/lib/zzq/transport/ipc.rb +0 -92
  46. data/lib/zzq/transport/mqtt.rb +0 -83
  47. data/lib/zzq/transport/mqtts.rb +0 -173
@@ -0,0 +1,163 @@
1
+ //! `ZZQ::Native::Broker`: a running broker.
2
+
3
+ use std::sync::{Arc, Mutex, OnceLock, PoisonError};
4
+
5
+ use rb_sys::VALUE;
6
+ use zzq_broker::{Broker, FilePersistence, QoS, Stats};
7
+
8
+ use crate::error::Error;
9
+ use crate::op::{self, Out};
10
+ use crate::rb::{self, DataType, RbResult, RubyErr};
11
+ use crate::{context, options};
12
+
13
+ static TYPE: DataType = DataType::new::<NativeBroker>(c"ZZQ::Native::Broker");
14
+ static CLASS: OnceLock<usize> = OnceLock::new();
15
+
16
+ #[derive(Debug)]
17
+ struct NativeBroker {
18
+ /// `None` once shut down.
19
+ broker: Mutex<Option<Arc<Broker>>>,
20
+ /// Flushed after a shutdown, so a broker started next on the same
21
+ /// file finds every retained message.
22
+ store: Option<Arc<FilePersistence>>,
23
+ }
24
+
25
+ fn get(value: VALUE) -> RbResult<Arc<Broker>> {
26
+ let native: &NativeBroker = unsafe { TYPE.get(value) }?;
27
+ native
28
+ .broker
29
+ .lock()
30
+ .unwrap_or_else(PoisonError::into_inner)
31
+ .clone()
32
+ .ok_or_else(|| Error::Broker(zzq_broker::Error::Closed).into())
33
+ }
34
+
35
+ /// A Ruby object for a running broker.
36
+ pub fn wrap(b: Broker, store: Option<Arc<FilePersistence>>) -> RbResult<VALUE> {
37
+ let native = NativeBroker {
38
+ broker: Mutex::new(Some(Arc::new(b))),
39
+ store,
40
+ };
41
+ TYPE.wrap(*CLASS.get().expect("registered") as VALUE, native)
42
+ }
43
+
44
+ pub fn stats_hash(s: &Stats) -> RbResult<VALUE> {
45
+ let h = rb::hash_new()?;
46
+ for (key, value) in [
47
+ ("connections", s.connections),
48
+ ("sessions", s.sessions),
49
+ ("subscriptions", s.subscriptions),
50
+ ("retained", s.retained),
51
+ ("publishes", s.publishes),
52
+ ("deliveries", s.deliveries),
53
+ ("dropped_buffer_full", s.dropped_buffer_full),
54
+ ("dropped_offline", s.dropped_offline),
55
+ ("dropped_queue_full", s.dropped_queue_full),
56
+ ("dropped_too_large", s.dropped_too_large),
57
+ ("dropped_expired", s.dropped_expired),
58
+ ("retained_rejected", s.retained_rejected),
59
+ ] {
60
+ rb::hash_aset(h, rb::symbol(key)?, rb::u64_value(value))?;
61
+ }
62
+ Ok(h)
63
+ }
64
+
65
+ fn start(opts: VALUE) -> RbResult<VALUE> {
66
+ let (opts, store) = options::broker(opts)?;
67
+ let ctx = context::get()?;
68
+ op::new(async move { Ok(Out::Broker(Broker::new(&ctx, opts).await?, store)) })
69
+ }
70
+
71
+ fn bind(this: VALUE, uri: VALUE) -> RbResult<VALUE> {
72
+ let b = get(this)?;
73
+ let uri = rb::value_to_string(uri)?;
74
+ op::new(async move { Ok(Out::Endpoint(b.bind(&uri).await?.to_string())) })
75
+ }
76
+
77
+ fn publish(
78
+ this: VALUE,
79
+ topic: VALUE,
80
+ payload: VALUE,
81
+ qos: VALUE,
82
+ retain: VALUE,
83
+ ) -> RbResult<VALUE> {
84
+ let b = get(this)?;
85
+ let qos: QoS = options::qos(rb::value_to_i64(qos)?)?;
86
+ let retain = rb::value_to_bool(retain)?;
87
+ let topic = rb::string_value(topic)?;
88
+ let payload = rb::string_value(payload)?;
89
+ // No calls into Ruby while the string slices are in use.
90
+ let (topic, payload) = unsafe { (rb::string_bytes(topic), rb::string_bytes(payload)) };
91
+ let topic = std::str::from_utf8(topic).map_err(|_| RubyErr::arg("topic must be UTF-8"))?;
92
+ let res = if retain {
93
+ b.publish_retained(topic, payload, qos)
94
+ } else {
95
+ b.publish(topic, payload, qos)
96
+ };
97
+ res.map_err(Error::from)?;
98
+ Ok(rb::qnil())
99
+ }
100
+
101
+ fn stats(this: VALUE) -> RbResult<VALUE> {
102
+ let b = get(this)?;
103
+ op::new(async move { Ok(Out::Stats(b.stats().await?)) })
104
+ }
105
+
106
+ /// An op that waits for the shutdown, or nil if an op still holds the
107
+ /// broker: it stops once that op is done.
108
+ fn shutdown(this: VALUE) -> RbResult<VALUE> {
109
+ let native: &NativeBroker = unsafe { TYPE.get(this) }?;
110
+ let taken = native
111
+ .broker
112
+ .lock()
113
+ .unwrap_or_else(PoisonError::into_inner)
114
+ .take();
115
+ let store = native.store.clone();
116
+ match taken.map(Arc::try_unwrap) {
117
+ Some(Ok(b)) => op::new(async move {
118
+ b.shutdown().await;
119
+ if let Some(store) = store {
120
+ store.flush();
121
+ }
122
+ Ok(Out::Nil)
123
+ }),
124
+ _ => Ok(rb::qnil()),
125
+ }
126
+ }
127
+
128
+ unsafe extern "C" fn broker_start(_class: VALUE, opts: VALUE) -> VALUE {
129
+ rb::wrap(|| start(opts))
130
+ }
131
+
132
+ unsafe extern "C" fn broker_bind(this: VALUE, uri: VALUE) -> VALUE {
133
+ rb::wrap(|| bind(this, uri))
134
+ }
135
+
136
+ unsafe extern "C" fn broker_publish(
137
+ this: VALUE,
138
+ topic: VALUE,
139
+ payload: VALUE,
140
+ qos: VALUE,
141
+ retain: VALUE,
142
+ ) -> VALUE {
143
+ rb::wrap(|| publish(this, topic, payload, qos, retain))
144
+ }
145
+
146
+ unsafe extern "C" fn broker_stats(this: VALUE) -> VALUE {
147
+ rb::wrap(|| stats(this))
148
+ }
149
+
150
+ unsafe extern "C" fn broker_shutdown(this: VALUE) -> VALUE {
151
+ rb::wrap(|| shutdown(this))
152
+ }
153
+
154
+ pub fn register(native: VALUE) -> RbResult<()> {
155
+ let class = rb::define_class_under(native, c"Broker")?;
156
+ let _ = CLASS.set(class as usize);
157
+ rb::define_singleton_method(class, c"start", broker_start as *const (), 1)?;
158
+ rb::define_method(class, c"bind", broker_bind as *const (), 1)?;
159
+ rb::define_method(class, c"publish", broker_publish as *const (), 4)?;
160
+ rb::define_method(class, c"stats", broker_stats as *const (), 0)?;
161
+ rb::define_method(class, c"shutdown", broker_shutdown as *const (), 0)?;
162
+ Ok(())
163
+ }
@@ -0,0 +1,294 @@
1
+ //! `ZZQ::Native::Client`: a connected client.
2
+ //!
3
+ //! Publishes go through a [`Handle`] and copy the Ruby string straight
4
+ //! into the send queue. Receiving takes what the inbox holds without a
5
+ //! stored future. Only once the inbox is empty does a future wait, kept in
6
+ //! `recv`, so that a wake reaches the fiber waiting for it.
7
+
8
+ use std::future::Future;
9
+ use std::pin::{Pin, pin};
10
+ use std::sync::{Arc, Mutex, OnceLock, PoisonError};
11
+ use std::task::{Context, Poll, Waker};
12
+
13
+ use rb_sys::VALUE;
14
+ use zzq_client::{Client, Handle, Message, QoS, ReasonCode, SubscriptionOptions};
15
+
16
+ use crate::error::{self, Error};
17
+ use crate::op::{self, Out};
18
+ use crate::rb::{self, DataType, RbResult, RubyErr};
19
+ use crate::{context, options, wake};
20
+
21
+ static TYPE: DataType = DataType::new::<NativeClient>(c"ZZQ::Native::Client");
22
+ static CLASS: OnceLock<usize> = OnceLock::new();
23
+ /// `ZZQ::Message` and a frozen empty Array for messages without
24
+ /// Subscription Identifiers.
25
+ static MESSAGE: OnceLock<(usize, usize)> = OnceLock::new();
26
+
27
+ type Recv = Pin<Box<dyn Future<Output = Option<Message>> + Send>>;
28
+
29
+ struct NativeClient {
30
+ handle: Handle,
31
+ client: Arc<tokio::sync::Mutex<Client>>,
32
+ /// The wait for the next message, once the inbox ran empty.
33
+ recv: Mutex<Option<Recv>>,
34
+ session_present: bool,
35
+ assigned_client_id: Option<String>,
36
+ }
37
+
38
+ impl std::fmt::Debug for NativeClient {
39
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40
+ f.debug_struct("NativeClient")
41
+ .field("session_present", &self.session_present)
42
+ .finish_non_exhaustive()
43
+ }
44
+ }
45
+
46
+ fn get(value: VALUE) -> RbResult<&'static NativeClient> {
47
+ unsafe { TYPE.get(value) }
48
+ }
49
+
50
+ /// A Ruby object for a connected client.
51
+ pub fn wrap(c: Client) -> RbResult<VALUE> {
52
+ let connected = c.connected();
53
+ let native = NativeClient {
54
+ handle: c.handle(),
55
+ session_present: connected.session_present,
56
+ assigned_client_id: connected.assigned_client_id.clone(),
57
+ client: Arc::new(tokio::sync::Mutex::new(c)),
58
+ recv: Mutex::new(None),
59
+ };
60
+ TYPE.wrap(*CLASS.get().expect("registered") as VALUE, native)
61
+ }
62
+
63
+ fn connect(uri: VALUE, opts: VALUE) -> RbResult<VALUE> {
64
+ let uri = rb::value_to_string(uri)?;
65
+ let opts = options::client(opts)?;
66
+ let ctx = context::get()?;
67
+ op::new(async move { Ok(Out::Client(Client::connect(&ctx, &uri, opts).await?)) })
68
+ }
69
+
70
+ fn publish(
71
+ this: VALUE,
72
+ topic: VALUE,
73
+ payload: VALUE,
74
+ qos: VALUE,
75
+ retain: VALUE,
76
+ ) -> RbResult<VALUE> {
77
+ let c = get(this)?;
78
+ let qos = options::qos(rb::value_to_i64(qos)?)?;
79
+ let retain = rb::value_to_bool(retain)?;
80
+ let topic = rb::string_value(topic)?;
81
+ let payload = rb::string_value(payload)?;
82
+ // No calls into Ruby while the string slices are in use.
83
+ let (topic, payload) = unsafe { (rb::string_bytes(topic), rb::string_bytes(payload)) };
84
+ let topic = std::str::from_utf8(topic).map_err(|_| RubyErr::arg("topic must be UTF-8"))?;
85
+ let queued = {
86
+ let fut = pin!(c.handle.enqueue(topic, payload, qos, retain));
87
+ fut.poll(&mut Context::from_waker(Waker::noop()))
88
+ };
89
+ match queued {
90
+ Poll::Ready(Ok(_)) if qos == QoS::AtMostOnce => Ok(rb::qnil()),
91
+ Poll::Ready(Ok(acked)) => op::acked(acked),
92
+ Poll::Ready(Err(e)) => Err(Error::from(e).into()),
93
+ Poll::Pending => {
94
+ // The queue is full: wait with a copy.
95
+ let (topic, payload, h) = (topic.to_owned(), payload.to_vec(), c.handle.clone());
96
+ op::new(async move {
97
+ h.enqueue(&topic, &payload, qos, retain).await?.await?;
98
+ Ok(Out::Nil)
99
+ })
100
+ }
101
+ }
102
+ }
103
+
104
+ fn subscribe(this: VALUE, filters: VALUE, sub_id: VALUE) -> RbResult<VALUE> {
105
+ let h = get(this)?.handle.clone();
106
+ let sub_id = u32::try_from(rb::value_to_i64(sub_id)?)
107
+ .map_err(|_| RubyErr::arg("subscription_id out of range"))?;
108
+ let mut parsed = Vec::new();
109
+ for f in rb::array_entries(filters)? {
110
+ let [filter, qos, no_local, rap, rh] = rb::array_entries(f)?[..] else {
111
+ return Err(RubyErr::arg("expected [filter, qos, no_local, rap, rh]"));
112
+ };
113
+ let opts = SubscriptionOptions {
114
+ qos: options::qos(rb::value_to_i64(qos)?)?,
115
+ no_local: rb::value_to_bool(no_local)?,
116
+ retain_as_published: rb::value_to_bool(rap)?,
117
+ retain_handling: u8::try_from(rb::value_to_i64(rh)?)
118
+ .ok()
119
+ .filter(|n| *n <= 2)
120
+ .ok_or_else(|| RubyErr::arg("retain_handling must be 0, 1 or 2"))?,
121
+ };
122
+ parsed.push((rb::value_to_string(filter)?, opts));
123
+ }
124
+ op::new(async move {
125
+ let refs: Vec<_> = parsed.iter().map(|(f, o)| (f.as_str(), *o)).collect();
126
+ Ok(Out::Codes(h.subscribe_with(&refs, sub_id).await?))
127
+ })
128
+ }
129
+
130
+ fn unsubscribe(this: VALUE, filters: VALUE) -> RbResult<VALUE> {
131
+ let h = get(this)?.handle.clone();
132
+ let filters = rb::array_entries(filters)?
133
+ .into_iter()
134
+ .map(rb::value_to_string)
135
+ .collect::<RbResult<Vec<_>>>()?;
136
+ op::new(async move {
137
+ let refs: Vec<_> = filters.iter().map(String::as_str).collect();
138
+ Ok(Out::Codes(h.unsubscribe(&refs).await?))
139
+ })
140
+ }
141
+
142
+ /// Up to `max` messages as an Array, `PENDING` if none came yet, or nil
143
+ /// once the client is closed.
144
+ fn receive(this: VALUE, wake: VALUE, max: VALUE) -> RbResult<VALUE> {
145
+ let c = get(this)?;
146
+ let wake = wake::get(wake)?;
147
+ let max = usize::try_from(rb::value_to_i64(max)?).unwrap_or(1).max(1);
148
+ let mut got = Vec::new();
149
+ let mut slot = c.recv.lock().unwrap_or_else(PoisonError::into_inner);
150
+ if let Some(fut) = slot.as_mut() {
151
+ let Poll::Ready(m) = wake.poll(fut.as_mut()) else {
152
+ return Ok(op::pending());
153
+ };
154
+ *slot = None;
155
+ match m {
156
+ Some(m) => got.push(m),
157
+ None => return Ok(rb::qnil()),
158
+ }
159
+ }
160
+ {
161
+ let Ok(mut client) = c.client.try_lock() else {
162
+ return Err(error::zzq("another receive is in progress"));
163
+ };
164
+ let mut cx = Context::from_waker(Waker::noop());
165
+ while got.len() < max {
166
+ match pin!(client.recv()).poll(&mut cx) {
167
+ Poll::Ready(Some(m)) => got.push(m),
168
+ Poll::Ready(None) if got.is_empty() => return Ok(rb::qnil()),
169
+ Poll::Ready(None) | Poll::Pending => break,
170
+ }
171
+ }
172
+ }
173
+ if got.is_empty() {
174
+ let Ok(mut client) = c.client.clone().try_lock_owned() else {
175
+ return Err(error::zzq("another receive is in progress"));
176
+ };
177
+ let mut fut: Recv = Box::pin(async move { client.recv().await });
178
+ match wake.poll(fut.as_mut()) {
179
+ Poll::Pending => {
180
+ *slot = Some(fut);
181
+ return Ok(op::pending());
182
+ }
183
+ Poll::Ready(None) => return Ok(rb::qnil()),
184
+ Poll::Ready(Some(m)) => got.push(m),
185
+ }
186
+ }
187
+ drop(slot);
188
+ messages(got)
189
+ }
190
+
191
+ fn messages(got: Vec<Message>) -> RbResult<VALUE> {
192
+ let &(class, no_ids) = MESSAGE.get().expect("registered");
193
+ let array = rb::array_new_capa(got.len())?;
194
+ for m in got {
195
+ let ids = if m.subscription_ids.is_empty() {
196
+ no_ids as VALUE
197
+ } else {
198
+ let ids = rb::array_new_capa(m.subscription_ids.len())?;
199
+ for id in &m.subscription_ids {
200
+ rb::array_push(ids, rb::u64_value((*id).into()))?;
201
+ }
202
+ rb::freeze(ids)
203
+ };
204
+ let args = [
205
+ rb::interned_utf8_string(m.topic.as_str())?,
206
+ rb::new_binary_string(&m.payload)?,
207
+ rb::u64_value(m.qos as u64),
208
+ rb::bool_value(m.retain),
209
+ ids,
210
+ ];
211
+ rb::array_push(array, rb::call(class as VALUE, c"new", &args)?)?;
212
+ }
213
+ Ok(array)
214
+ }
215
+
216
+ fn disconnect(this: VALUE, reason: VALUE) -> RbResult<VALUE> {
217
+ let h = get(this)?.handle.clone();
218
+ let reason = u8::try_from(rb::value_to_i64(reason)?)
219
+ .map_err(|_| RubyErr::arg("reason code out of range"))?;
220
+ op::new(async move {
221
+ h.disconnect_with(ReasonCode::from(reason)).await;
222
+ Ok(Out::Nil)
223
+ })
224
+ }
225
+
226
+ unsafe extern "C" fn client_connect(_class: VALUE, uri: VALUE, opts: VALUE) -> VALUE {
227
+ rb::wrap(|| connect(uri, opts))
228
+ }
229
+
230
+ unsafe extern "C" fn client_publish(
231
+ this: VALUE,
232
+ topic: VALUE,
233
+ payload: VALUE,
234
+ qos: VALUE,
235
+ retain: VALUE,
236
+ ) -> VALUE {
237
+ rb::wrap(|| publish(this, topic, payload, qos, retain))
238
+ }
239
+
240
+ unsafe extern "C" fn client_subscribe(this: VALUE, filters: VALUE, sub_id: VALUE) -> VALUE {
241
+ rb::wrap(|| subscribe(this, filters, sub_id))
242
+ }
243
+
244
+ unsafe extern "C" fn client_unsubscribe(this: VALUE, filters: VALUE) -> VALUE {
245
+ rb::wrap(|| unsubscribe(this, filters))
246
+ }
247
+
248
+ unsafe extern "C" fn client_receive(this: VALUE, wake: VALUE, max: VALUE) -> VALUE {
249
+ rb::wrap(|| receive(this, wake, max))
250
+ }
251
+
252
+ unsafe extern "C" fn client_disconnect(this: VALUE, reason: VALUE) -> VALUE {
253
+ rb::wrap(|| disconnect(this, reason))
254
+ }
255
+
256
+ unsafe extern "C" fn client_session_present(this: VALUE) -> VALUE {
257
+ rb::wrap(|| Ok(rb::bool_value(get(this)?.session_present)))
258
+ }
259
+
260
+ unsafe extern "C" fn client_assigned_client_id(this: VALUE) -> VALUE {
261
+ rb::wrap(|| match &get(this)?.assigned_client_id {
262
+ Some(id) => rb::new_utf8_string(id),
263
+ None => Ok(rb::qnil()),
264
+ })
265
+ }
266
+
267
+ pub fn register(zzq: VALUE, native: VALUE) -> RbResult<()> {
268
+ let message = rb::const_get(zzq, c"Message")?;
269
+ let no_ids = rb::freeze(rb::array_new_capa(0)?);
270
+ rb::define_const(native, c"NO_SUBSCRIPTION_IDS", no_ids)?;
271
+ let _ = MESSAGE.set((message as usize, no_ids as usize));
272
+
273
+ let class = rb::define_class_under(native, c"Client")?;
274
+ let _ = CLASS.set(class as usize);
275
+ rb::define_singleton_method(class, c"connect", client_connect as *const (), 2)?;
276
+ rb::define_method(class, c"publish", client_publish as *const (), 4)?;
277
+ rb::define_method(class, c"subscribe", client_subscribe as *const (), 2)?;
278
+ rb::define_method(class, c"unsubscribe", client_unsubscribe as *const (), 1)?;
279
+ rb::define_method(class, c"receive", client_receive as *const (), 2)?;
280
+ rb::define_method(class, c"disconnect", client_disconnect as *const (), 1)?;
281
+ rb::define_method(
282
+ class,
283
+ c"session_present?",
284
+ client_session_present as *const (),
285
+ 0,
286
+ )?;
287
+ rb::define_method(
288
+ class,
289
+ c"assigned_client_id",
290
+ client_assigned_client_id as *const (),
291
+ 0,
292
+ )?;
293
+ Ok(())
294
+ }
@@ -0,0 +1,51 @@
1
+ //! The one `zzq_tokio::Context` that clients and brokers of this process
2
+ //! share.
3
+
4
+ use std::sync::atomic::{AtomicUsize, Ordering};
5
+ use std::sync::{Mutex, PoisonError};
6
+
7
+ use zzq_client::{Config, Context};
8
+
9
+ use crate::error::Error;
10
+
11
+ static IO_THREADS: AtomicUsize = AtomicUsize::new(1);
12
+ static CONTEXT: Mutex<Option<(u32, Context)>> = Mutex::new(None);
13
+
14
+ /// The shared context, started on first use. A forked child gets its own:
15
+ /// the parent's lane threads do not exist in it.
16
+ pub fn get() -> Result<Context, Error> {
17
+ let pid = std::process::id();
18
+ let mut slot = CONTEXT.lock().unwrap_or_else(PoisonError::into_inner);
19
+ if let Some((owner, ctx)) = slot.as_ref()
20
+ && *owner == pid
21
+ {
22
+ return Ok(ctx.clone());
23
+ }
24
+ // Dropping the parent's context would wait for threads this process
25
+ // does not have.
26
+ std::mem::forget(slot.take());
27
+ let config = Config {
28
+ io_threads: IO_THREADS.load(Ordering::Relaxed),
29
+ ..Config::default()
30
+ };
31
+ let ctx = Context::with_config(&config).map_err(zzq_client::Error::from)?;
32
+ *slot = Some((pid, ctx.clone()));
33
+ Ok(ctx)
34
+ }
35
+
36
+ pub fn io_threads() -> usize {
37
+ IO_THREADS.load(Ordering::Relaxed)
38
+ }
39
+
40
+ /// Sets the lane count, unless the context runs already.
41
+ pub fn set_io_threads(n: usize) -> bool {
42
+ let slot = CONTEXT.lock().unwrap_or_else(PoisonError::into_inner);
43
+ if slot
44
+ .as_ref()
45
+ .is_some_and(|(owner, _)| *owner == std::process::id())
46
+ {
47
+ return false;
48
+ }
49
+ IO_THREADS.store(n.max(1), Ordering::Relaxed);
50
+ true
51
+ }
@@ -0,0 +1,117 @@
1
+ //! Rust errors as `ZZQ::Error` and its subclasses, which `lib/zzq/errors.rb`
2
+ //! defines before the extension loads.
3
+
4
+ use std::sync::OnceLock;
5
+
6
+ use rb_sys::VALUE;
7
+
8
+ use crate::rb::{self, RbResult, RubyErr};
9
+
10
+ /// An error from the client or the broker crate.
11
+ #[derive(Debug)]
12
+ pub enum Error {
13
+ Client(zzq_client::Error),
14
+ Broker(zzq_broker::Error),
15
+ }
16
+
17
+ impl From<zzq_client::Error> for Error {
18
+ fn from(e: zzq_client::Error) -> Self {
19
+ Error::Client(e)
20
+ }
21
+ }
22
+
23
+ impl From<zzq_broker::Error> for Error {
24
+ fn from(e: zzq_broker::Error) -> Self {
25
+ Error::Broker(e)
26
+ }
27
+ }
28
+
29
+ struct Classes {
30
+ error: VALUE,
31
+ closed: VALUE,
32
+ transport: VALUE,
33
+ protocol: VALUE,
34
+ invalid_topic: VALUE,
35
+ too_large: VALUE,
36
+ refused: VALUE,
37
+ rejected: VALUE,
38
+ }
39
+
40
+ unsafe impl Send for Classes {}
41
+ unsafe impl Sync for Classes {}
42
+
43
+ static CLASSES: OnceLock<Classes> = OnceLock::new();
44
+
45
+ pub fn init(zzq: VALUE) -> RbResult<()> {
46
+ let classes = Classes {
47
+ error: rb::const_get(zzq, c"Error")?,
48
+ closed: rb::const_get(zzq, c"ClosedError")?,
49
+ transport: rb::const_get(zzq, c"TransportError")?,
50
+ protocol: rb::const_get(zzq, c"ProtocolError")?,
51
+ invalid_topic: rb::const_get(zzq, c"InvalidTopic")?,
52
+ too_large: rb::const_get(zzq, c"TooLarge")?,
53
+ refused: rb::const_get(zzq, c"ConnectionRefused")?,
54
+ rejected: rb::const_get(zzq, c"Rejected")?,
55
+ };
56
+ let _ = CLASSES.set(classes);
57
+ Ok(())
58
+ }
59
+
60
+ fn classes() -> &'static Classes {
61
+ CLASSES.get().expect("error classes loaded")
62
+ }
63
+
64
+ /// `ZZQ::Error` with `message`.
65
+ pub fn zzq(message: impl Into<String>) -> RubyErr {
66
+ RubyErr::new(classes().error, message)
67
+ }
68
+
69
+ /// The message of `e` and of each error that caused it.
70
+ fn chain(e: &dyn std::error::Error) -> String {
71
+ let mut message = e.to_string();
72
+ let mut source = e.source();
73
+ while let Some(s) = source {
74
+ message.push_str(": ");
75
+ message.push_str(&s.to_string());
76
+ source = s.source();
77
+ }
78
+ message
79
+ }
80
+
81
+ /// An exception that carries the reason code.
82
+ fn with_code(class: VALUE, message: &str, code: zzq_client::ReasonCode) -> RubyErr {
83
+ let built = rb::new_utf8_string(message).and_then(|m| {
84
+ let code = rb::u64_value(u8::from(code).into());
85
+ rb::call(class, c"new", &[m, code])
86
+ });
87
+ match built {
88
+ Ok(exc) => RubyErr::Exception(exc),
89
+ Err(e) => e,
90
+ }
91
+ }
92
+
93
+ impl From<Error> for RubyErr {
94
+ fn from(e: Error) -> Self {
95
+ use zzq_client::Error as C;
96
+ let c = classes();
97
+ let message = match &e {
98
+ Error::Client(e) => chain(e),
99
+ Error::Broker(e) => chain(e),
100
+ };
101
+ let class = match e {
102
+ Error::Client(C::Refused(code)) => return with_code(c.refused, &message, code),
103
+ Error::Client(C::Rejected(code)) => return with_code(c.rejected, &message, code),
104
+ Error::Client(C::Transport(_)) | Error::Broker(zzq_broker::Error::Transport(_)) => {
105
+ c.transport
106
+ }
107
+ Error::Client(C::Protocol(_)) => c.protocol,
108
+ Error::Client(C::InvalidTopic(_))
109
+ | Error::Broker(zzq_broker::Error::InvalidTopic(_)) => c.invalid_topic,
110
+ Error::Client(C::TooLarge | C::TooLong(_))
111
+ | Error::Broker(zzq_broker::Error::TooLarge) => c.too_large,
112
+ Error::Client(C::Closed) | Error::Broker(zzq_broker::Error::Closed) => c.closed,
113
+ _ => c.error,
114
+ };
115
+ RubyErr::new(class, message)
116
+ }
117
+ }
@@ -0,0 +1,52 @@
1
+ //! Native part of the `zzq` gem: `ZZQ::Native`, which `lib/zzq` wraps.
2
+
3
+ mod broker;
4
+ mod client;
5
+ mod context;
6
+ mod error;
7
+ mod op;
8
+ mod options;
9
+ mod rb;
10
+ mod wake;
11
+
12
+ use rb_sys::VALUE;
13
+
14
+ use crate::rb::RbResult;
15
+
16
+ unsafe extern "C" fn io_threads(_module: VALUE) -> VALUE {
17
+ rb::wrap(|| Ok(rb::u64_value(context::io_threads() as u64)))
18
+ }
19
+
20
+ unsafe extern "C" fn set_io_threads(_module: VALUE, n: VALUE) -> VALUE {
21
+ rb::wrap(|| {
22
+ let n = usize::try_from(rb::value_to_i64(n)?)
23
+ .map_err(|_| rb::RubyErr::arg("io_threads must be positive"))?;
24
+ if !context::set_io_threads(n) {
25
+ return Err(error::zzq(
26
+ "set io_threads before the first client or broker",
27
+ ));
28
+ }
29
+ Ok(rb::qnil())
30
+ })
31
+ }
32
+
33
+ /// Called once by Ruby when it loads the extension.
34
+ #[unsafe(no_mangle)]
35
+ pub extern "C" fn Init_zzq_native() {
36
+ if let Err(e) = init() {
37
+ rb::raise(e);
38
+ }
39
+ }
40
+
41
+ fn init() -> RbResult<()> {
42
+ let zzq = rb::define_module(c"ZZQ")?;
43
+ let native = rb::define_module_under(zzq, c"Native")?;
44
+ error::init(zzq)?;
45
+ rb::define_singleton_method(native, c"io_threads", io_threads as *const (), 0)?;
46
+ rb::define_singleton_method(native, c"io_threads=", set_io_threads as *const (), 1)?;
47
+ wake::register(native)?;
48
+ op::register(native)?;
49
+ client::register(zzq, native)?;
50
+ broker::register(native)?;
51
+ Ok(())
52
+ }