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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -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 -163
  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,424 @@
1
+ //! Calls into the Ruby C API. Every call that may raise runs under
2
+ //! `rb_protect`, so a Ruby exception never unwinds through Rust frames.
3
+
4
+ use std::ffi::{CStr, CString, c_char, c_long, c_void};
5
+ use std::panic::{AssertUnwindSafe, catch_unwind};
6
+ use std::ptr;
7
+
8
+ use rb_sys::{VALUE, rb_data_type_struct__bindgen_ty_1, rb_data_type_t, size_t};
9
+
10
+ pub type RbResult<T = VALUE> = Result<T, RubyErr>;
11
+
12
+ #[derive(Debug)]
13
+ pub enum RubyErr {
14
+ /// An exception object to raise.
15
+ Exception(VALUE),
16
+ /// An exception to build from a class and a message.
17
+ Error { class: VALUE, message: String },
18
+ }
19
+
20
+ impl RubyErr {
21
+ pub fn new(class: VALUE, message: impl Into<String>) -> Self {
22
+ Self::Error {
23
+ class,
24
+ message: message.into(),
25
+ }
26
+ }
27
+
28
+ pub fn arg(message: impl Into<String>) -> Self {
29
+ Self::new(unsafe { rb_sys::rb_eArgError }, message)
30
+ }
31
+
32
+ pub fn runtime(message: impl Into<String>) -> Self {
33
+ Self::new(unsafe { rb_sys::rb_eRuntimeError }, message)
34
+ }
35
+
36
+ pub fn type_error(message: impl Into<String>) -> Self {
37
+ Self::new(unsafe { rb_sys::rb_eTypeError }, message)
38
+ }
39
+
40
+ fn current_exception() -> Self {
41
+ let err = unsafe { rb_sys::rb_errinfo() };
42
+ unsafe { rb_sys::rb_set_errinfo(qnil()) };
43
+ if err == qnil() {
44
+ Self::runtime("Ruby exception")
45
+ } else {
46
+ Self::Exception(err)
47
+ }
48
+ }
49
+ }
50
+
51
+ /// Runs a method body: raises its error, and turns a panic into an error.
52
+ pub fn wrap<F>(f: F) -> VALUE
53
+ where
54
+ F: FnOnce() -> RbResult<VALUE>,
55
+ {
56
+ match catch_unwind(AssertUnwindSafe(f)) {
57
+ Ok(Ok(value)) => value,
58
+ Ok(Err(err)) => raise(err),
59
+ Err(_) => raise(RubyErr::runtime("native Rust panic")),
60
+ }
61
+ }
62
+
63
+ pub fn raise(err: RubyErr) -> ! {
64
+ match err {
65
+ RubyErr::Exception(exc) => unsafe { rb_sys::rb_exc_raise(exc) },
66
+ RubyErr::Error { class, message } => {
67
+ let message = CString::new(message.replace('\0', "\\0")).unwrap_or_default();
68
+ let exc = unsafe { rb_sys::rb_exc_new_cstr(class, message.as_ptr()) };
69
+ unsafe { rb_sys::rb_exc_raise(exc) }
70
+ }
71
+ }
72
+ }
73
+
74
+ struct ProtectData<F> {
75
+ func: Option<F>,
76
+ panicked: bool,
77
+ }
78
+
79
+ /// Runs `func`, which calls into Ruby, catching a Ruby exception.
80
+ pub fn protect<F>(func: F) -> RbResult<VALUE>
81
+ where
82
+ F: FnOnce() -> VALUE,
83
+ {
84
+ unsafe extern "C" fn call<F>(arg: VALUE) -> VALUE
85
+ where
86
+ F: FnOnce() -> VALUE,
87
+ {
88
+ let data = unsafe { &mut *(arg as *mut ProtectData<F>) };
89
+ let Some(func) = data.func.take() else {
90
+ data.panicked = true;
91
+ return qnil();
92
+ };
93
+ if let Ok(value) = catch_unwind(AssertUnwindSafe(func)) {
94
+ value
95
+ } else {
96
+ data.panicked = true;
97
+ qnil()
98
+ }
99
+ }
100
+
101
+ let mut data = ProtectData {
102
+ func: Some(func),
103
+ panicked: false,
104
+ };
105
+ let mut state = 0;
106
+ let value = unsafe {
107
+ rb_sys::rb_protect(
108
+ Some(call::<F>),
109
+ (&raw mut data).cast::<ProtectData<F>>() as VALUE,
110
+ &raw mut state,
111
+ )
112
+ };
113
+ if state != 0 {
114
+ Err(RubyErr::current_exception())
115
+ } else if data.panicked {
116
+ Err(RubyErr::runtime("native Rust panic"))
117
+ } else {
118
+ Ok(value)
119
+ }
120
+ }
121
+
122
+ pub const fn qnil() -> VALUE {
123
+ rb_sys::ruby_special_consts::RUBY_Qnil as VALUE
124
+ }
125
+
126
+ pub const fn qtrue() -> VALUE {
127
+ rb_sys::ruby_special_consts::RUBY_Qtrue as VALUE
128
+ }
129
+
130
+ pub const fn qfalse() -> VALUE {
131
+ rb_sys::ruby_special_consts::RUBY_Qfalse as VALUE
132
+ }
133
+
134
+ const fn qundef() -> VALUE {
135
+ rb_sys::ruby_special_consts::RUBY_Qundef as VALUE
136
+ }
137
+
138
+ pub fn bool_value(value: bool) -> VALUE {
139
+ if value { qtrue() } else { qfalse() }
140
+ }
141
+
142
+ fn is_kind_of(value: VALUE, class: VALUE) -> bool {
143
+ unsafe { rb_sys::rb_obj_is_kind_of(value, class) == qtrue() }
144
+ }
145
+
146
+ /// The value under a String key, `None` if the key is missing.
147
+ pub fn hash_get(hash: VALUE, key: &str) -> RbResult<Option<VALUE>> {
148
+ if !is_kind_of(hash, unsafe { rb_sys::rb_cHash }) {
149
+ return Err(RubyErr::type_error("expected a Hash"));
150
+ }
151
+ let key = new_utf8_string(key)?;
152
+ let value = protect(|| unsafe { rb_sys::rb_hash_lookup2(hash, key, qundef()) })?;
153
+ Ok((value != qundef()).then_some(value))
154
+ }
155
+
156
+ pub fn hash_new() -> RbResult<VALUE> {
157
+ protect(|| unsafe { rb_sys::rb_hash_new() })
158
+ }
159
+
160
+ pub fn hash_aset(hash: VALUE, key: VALUE, value: VALUE) -> RbResult<()> {
161
+ protect(|| unsafe { rb_sys::rb_hash_aset(hash, key, value) })?;
162
+ Ok(())
163
+ }
164
+
165
+ pub fn array_new_capa(capacity: usize) -> RbResult<VALUE> {
166
+ let capacity = c_long_len(capacity)?;
167
+ protect(|| unsafe { rb_sys::rb_ary_new_capa(capacity) })
168
+ }
169
+
170
+ pub fn array_entries(array: VALUE) -> RbResult<Vec<VALUE>> {
171
+ if !is_kind_of(array, unsafe { rb_sys::rb_cArray }) {
172
+ return Err(RubyErr::type_error("expected an Array"));
173
+ }
174
+ let len = unsafe { rb_sys::RARRAY_LEN(array) };
175
+ (0..len)
176
+ .map(|i| protect(|| unsafe { rb_sys::rb_ary_entry(array, i) }))
177
+ .collect()
178
+ }
179
+
180
+ pub fn array_push(array: VALUE, value: VALUE) -> RbResult<()> {
181
+ protect(|| unsafe { rb_sys::rb_ary_push(array, value) })?;
182
+ Ok(())
183
+ }
184
+
185
+ pub fn freeze(value: VALUE) -> VALUE {
186
+ unsafe { rb_sys::rb_obj_freeze(value) }
187
+ }
188
+
189
+ pub fn symbol(name: &str) -> RbResult<VALUE> {
190
+ let len = c_long_len(name.len())?;
191
+ protect(|| unsafe { rb_sys::rb_id2sym(rb_sys::rb_intern2(name.as_ptr().cast(), len)) })
192
+ }
193
+
194
+ /// A frozen binary String.
195
+ pub fn new_binary_string(bytes: &[u8]) -> RbResult<VALUE> {
196
+ let len = c_long_len(bytes.len())?;
197
+ let ptr = if bytes.is_empty() {
198
+ ptr::null()
199
+ } else {
200
+ bytes.as_ptr().cast::<c_char>()
201
+ };
202
+ let value = protect(|| unsafe { rb_sys::rb_str_new(ptr, len) })?;
203
+ Ok(freeze(value))
204
+ }
205
+
206
+ pub fn new_utf8_string(text: &str) -> RbResult<VALUE> {
207
+ let len = c_long_len(text.len())?;
208
+ protect(|| unsafe { rb_sys::rb_utf8_str_new(text.as_ptr().cast::<c_char>(), len) })
209
+ }
210
+
211
+ /// A frozen, deduplicated UTF-8 String, for values that repeat, such as
212
+ /// topics.
213
+ pub fn interned_utf8_string(text: &str) -> RbResult<VALUE> {
214
+ let len = c_long_len(text.len())?;
215
+ protect(|| unsafe {
216
+ rb_sys::rb_enc_interned_str(
217
+ text.as_ptr().cast::<c_char>(),
218
+ len,
219
+ rb_sys::rb_utf8_encoding(),
220
+ )
221
+ })
222
+ }
223
+
224
+ /// `value` as a String, calling `to_str` if needed.
225
+ pub fn string_value(value: VALUE) -> RbResult<VALUE> {
226
+ protect(|| unsafe { rb_sys::rb_str_to_str(value) })
227
+ }
228
+
229
+ /// The bytes of a String from [`string_value`].
230
+ ///
231
+ /// # Safety
232
+ ///
233
+ /// The slice points into the Ruby string. Use it before the next call
234
+ /// into Ruby, which may run GC or change the string.
235
+ pub unsafe fn string_bytes<'a>(string: VALUE) -> &'a [u8] {
236
+ let len = unsafe { rb_sys::RSTRING_LEN(string) };
237
+ let ptr = unsafe { rb_sys::RSTRING_PTR(string) };
238
+ if len <= 0 || ptr.is_null() {
239
+ return &[];
240
+ }
241
+ unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len as usize) }
242
+ }
243
+
244
+ pub fn value_to_bytes(value: VALUE) -> RbResult<Vec<u8>> {
245
+ let string = string_value(value)?;
246
+ Ok(unsafe { string_bytes(string) }.to_vec())
247
+ }
248
+
249
+ pub fn value_to_string(value: VALUE) -> RbResult<String> {
250
+ String::from_utf8(value_to_bytes(value)?).map_err(|_| RubyErr::arg("expected a UTF-8 String"))
251
+ }
252
+
253
+ pub fn value_to_i64(value: VALUE) -> RbResult<i64> {
254
+ let mut out = 0i64;
255
+ protect(|| {
256
+ out = unsafe { rb_sys::rb_num2ll(value) };
257
+ qnil()
258
+ })?;
259
+ Ok(out)
260
+ }
261
+
262
+ pub fn value_to_f64(value: VALUE) -> RbResult<f64> {
263
+ let mut out = 0.0f64;
264
+ protect(|| {
265
+ out = unsafe { rb_sys::rb_num2dbl(value) };
266
+ qnil()
267
+ })?;
268
+ Ok(out)
269
+ }
270
+
271
+ pub fn value_to_bool(value: VALUE) -> RbResult<bool> {
272
+ if value == qtrue() {
273
+ Ok(true)
274
+ } else if value == qfalse() || value == qnil() {
275
+ Ok(false)
276
+ } else {
277
+ Err(RubyErr::type_error("expected true or false"))
278
+ }
279
+ }
280
+
281
+ pub fn u64_value(value: u64) -> VALUE {
282
+ unsafe { rb_sys::rb_ull2inum(value) }
283
+ }
284
+
285
+ /// Calls `receiver.name(*args)`.
286
+ pub fn call(receiver: VALUE, name: &CStr, args: &[VALUE]) -> RbResult<VALUE> {
287
+ let count = std::os::raw::c_int::try_from(args.len())
288
+ .map_err(|_| RubyErr::arg("too many arguments"))?;
289
+ protect(|| unsafe {
290
+ rb_sys::rb_funcallv(
291
+ receiver,
292
+ rb_sys::rb_intern(name.as_ptr()),
293
+ count,
294
+ args.as_ptr(),
295
+ )
296
+ })
297
+ }
298
+
299
+ pub fn const_get(module: VALUE, name: &CStr) -> RbResult<VALUE> {
300
+ protect(|| unsafe { rb_sys::rb_const_get(module, rb_sys::rb_intern(name.as_ptr())) })
301
+ }
302
+
303
+ /// A Ruby type for a boxed `T`, which GC frees with the object.
304
+ pub struct DataType(rb_data_type_t);
305
+
306
+ unsafe impl Send for DataType {}
307
+ unsafe impl Sync for DataType {}
308
+
309
+ impl DataType {
310
+ pub const fn new<T>(name: &'static CStr) -> Self {
311
+ DataType(rb_data_type_t {
312
+ wrap_struct_name: name.as_ptr(),
313
+ function: rb_data_type_struct__bindgen_ty_1 {
314
+ dmark: None,
315
+ dfree: Some(free_box::<T>),
316
+ dsize: Some(size_box::<T>),
317
+ dcompact: None,
318
+ reserved: [ptr::null_mut(); 1],
319
+ },
320
+ parent: ptr::null(),
321
+ data: ptr::null_mut(),
322
+ // RUBY_TYPED_FREE_IMMEDIATELY: dropping never blocks or calls Ruby.
323
+ flags: 1,
324
+ })
325
+ }
326
+
327
+ const fn ptr(&'static self) -> *const rb_data_type_t {
328
+ &raw const self.0
329
+ }
330
+
331
+ /// Wraps `value` in a new object of `class`.
332
+ pub fn wrap<T>(&'static self, class: VALUE, value: T) -> RbResult<VALUE> {
333
+ let raw = Box::into_raw(Box::new(value));
334
+ protect(|| unsafe { rb_sys::rb_data_typed_object_wrap(class, raw.cast(), self.ptr()) })
335
+ .inspect_err(|_| drop(unsafe { Box::from_raw(raw) }))
336
+ }
337
+
338
+ /// The `T` inside `value`.
339
+ ///
340
+ /// # Safety
341
+ ///
342
+ /// `T` must be the type this data type was made for.
343
+ pub unsafe fn get<T>(&'static self, value: VALUE) -> RbResult<&'static T> {
344
+ let mut ptr = ptr::null_mut();
345
+ protect(|| unsafe {
346
+ ptr = rb_sys::rb_check_typeddata(value, self.ptr());
347
+ qnil()
348
+ })?;
349
+ if ptr.is_null() {
350
+ return Err(RubyErr::runtime("native object is empty"));
351
+ }
352
+ Ok(unsafe { &*ptr.cast::<T>() })
353
+ }
354
+ }
355
+
356
+ unsafe extern "C" fn free_box<T>(ptr: *mut c_void) {
357
+ if !ptr.is_null() {
358
+ let _ = catch_unwind(AssertUnwindSafe(|| {
359
+ drop(unsafe { Box::from_raw(ptr.cast::<T>()) });
360
+ }));
361
+ }
362
+ }
363
+
364
+ unsafe extern "C" fn size_box<T>(_ptr: *const c_void) -> size_t {
365
+ std::mem::size_of::<T>() as size_t
366
+ }
367
+
368
+ pub fn define_module(name: &CStr) -> RbResult<VALUE> {
369
+ protect(|| unsafe { rb_sys::rb_define_module(name.as_ptr()) })
370
+ }
371
+
372
+ pub fn define_module_under(outer: VALUE, name: &CStr) -> RbResult<VALUE> {
373
+ protect(|| unsafe { rb_sys::rb_define_module_under(outer, name.as_ptr()) })
374
+ }
375
+
376
+ /// A class whose objects only native code creates.
377
+ pub fn define_class_under(outer: VALUE, name: &CStr) -> RbResult<VALUE> {
378
+ let class = protect(|| unsafe {
379
+ rb_sys::rb_define_class_under(outer, name.as_ptr(), rb_sys::rb_cObject)
380
+ })?;
381
+ protect(|| unsafe {
382
+ rb_sys::rb_undef_alloc_func(class);
383
+ qnil()
384
+ })?;
385
+ Ok(class)
386
+ }
387
+
388
+ pub fn define_const(module: VALUE, name: &CStr, value: VALUE) -> RbResult<()> {
389
+ protect(|| unsafe {
390
+ rb_sys::rb_define_const(module, name.as_ptr(), value);
391
+ qnil()
392
+ })?;
393
+ Ok(())
394
+ }
395
+
396
+ /// A method with `arity` arguments. `func` takes `self` and then each
397
+ /// argument as a `VALUE`.
398
+ pub fn define_method(class: VALUE, name: &CStr, func: *const (), arity: i32) -> RbResult<()> {
399
+ let func = unsafe { std::mem::transmute::<*const (), unsafe extern "C" fn() -> VALUE>(func) };
400
+ protect(|| unsafe {
401
+ rb_sys::rb_define_method(class, name.as_ptr(), Some(func), arity);
402
+ qnil()
403
+ })?;
404
+ Ok(())
405
+ }
406
+
407
+ /// A singleton method, as [`define_method`].
408
+ pub fn define_singleton_method(
409
+ object: VALUE,
410
+ name: &CStr,
411
+ func: *const (),
412
+ arity: i32,
413
+ ) -> RbResult<()> {
414
+ let func = unsafe { std::mem::transmute::<*const (), unsafe extern "C" fn() -> VALUE>(func) };
415
+ protect(|| unsafe {
416
+ rb_sys::rb_define_singleton_method(object, name.as_ptr(), Some(func), arity);
417
+ qnil()
418
+ })?;
419
+ Ok(())
420
+ }
421
+
422
+ fn c_long_len(len: usize) -> RbResult<c_long> {
423
+ c_long::try_from(len).map_err(|_| RubyErr::arg("length too large"))
424
+ }
@@ -0,0 +1,155 @@
1
+ //! `ZZQ::Native::Wake`: how a Ruby fiber waits for a Rust future.
2
+ //!
3
+ //! A native call polls the future with a waker that writes one byte to a
4
+ //! pipe. If the future is pending, Ruby waits for the pipe to turn
5
+ //! readable, which suspends only the fiber under a fiber scheduler and
6
+ //! releases the GVL otherwise, then polls again.
7
+
8
+ use std::future::Future;
9
+ use std::os::fd::RawFd;
10
+ use std::pin::Pin;
11
+ use std::sync::Arc;
12
+ use std::sync::atomic::{AtomicBool, Ordering};
13
+ use std::task::{Context, Poll, Waker};
14
+
15
+ use rb_sys::VALUE;
16
+
17
+ use crate::rb::{self, DataType, RbResult, RubyErr};
18
+
19
+ static TYPE: DataType = DataType::new::<Wake>(c"ZZQ::Native::Wake");
20
+ static CLASS: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
21
+
22
+ /// A pipe one fiber waits on.
23
+ #[derive(Debug)]
24
+ pub struct Wake {
25
+ pipe: Arc<Pipe>,
26
+ waker: Waker,
27
+ }
28
+
29
+ #[derive(Debug)]
30
+ struct Pipe {
31
+ read_fd: RawFd,
32
+ write_fd: RawFd,
33
+ /// The fiber is about to wait, so a wake must write.
34
+ armed: AtomicBool,
35
+ }
36
+
37
+ impl std::task::Wake for Pipe {
38
+ fn wake(self: Arc<Self>) {
39
+ self.wake_by_ref();
40
+ }
41
+
42
+ fn wake_by_ref(self: &Arc<Self>) {
43
+ if self.armed.swap(false, Ordering::AcqRel) {
44
+ let byte = 1u8;
45
+ loop {
46
+ let n = unsafe { libc::write(self.write_fd, (&raw const byte).cast(), 1) };
47
+ // A full pipe holds a wake already.
48
+ if n >= 0 || std::io::Error::last_os_error().raw_os_error() != Some(libc::EINTR) {
49
+ break;
50
+ }
51
+ }
52
+ }
53
+ }
54
+ }
55
+
56
+ impl Drop for Pipe {
57
+ fn drop(&mut self) {
58
+ unsafe {
59
+ libc::close(self.read_fd);
60
+ libc::close(self.write_fd);
61
+ }
62
+ }
63
+ }
64
+
65
+ impl Wake {
66
+ fn new() -> std::io::Result<Wake> {
67
+ let mut fds = [0; 2];
68
+ if unsafe { pipe(&mut fds) } != 0 {
69
+ return Err(std::io::Error::last_os_error());
70
+ }
71
+ let pipe = Arc::new(Pipe {
72
+ read_fd: fds[0],
73
+ write_fd: fds[1],
74
+ armed: AtomicBool::new(false),
75
+ });
76
+ let waker = Waker::from(pipe.clone());
77
+ Ok(Wake { pipe, waker })
78
+ }
79
+
80
+ /// Polls `fut`. While it is pending, a wake writes to the pipe.
81
+ pub fn poll<F: Future + ?Sized>(&self, fut: Pin<&mut F>) -> Poll<F::Output> {
82
+ self.pipe.armed.store(true, Ordering::Release);
83
+ let out = fut.poll(&mut Context::from_waker(&self.waker));
84
+ if out.is_ready() {
85
+ self.pipe.armed.store(false, Ordering::Release);
86
+ }
87
+ out
88
+ }
89
+
90
+ /// Reads the wakes written so far.
91
+ fn clear(&self) {
92
+ let mut buf = [0u8; 64];
93
+ loop {
94
+ let n = unsafe { libc::read(self.pipe.read_fd, buf.as_mut_ptr().cast(), buf.len()) };
95
+ if n > 0 {
96
+ continue;
97
+ }
98
+ if n < 0 && std::io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
99
+ continue;
100
+ }
101
+ break;
102
+ }
103
+ }
104
+ }
105
+
106
+ #[cfg(any(target_os = "linux", target_os = "android"))]
107
+ unsafe fn pipe(fds: &mut [RawFd; 2]) -> i32 {
108
+ unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_NONBLOCK | libc::O_CLOEXEC) }
109
+ }
110
+
111
+ #[cfg(all(unix, not(any(target_os = "linux", target_os = "android"))))]
112
+ unsafe fn pipe(fds: &mut [RawFd; 2]) -> i32 {
113
+ let ret = unsafe { libc::pipe(fds.as_mut_ptr()) };
114
+ if ret == 0 {
115
+ for fd in *fds {
116
+ unsafe {
117
+ libc::fcntl(fd, libc::F_SETFL, libc::O_NONBLOCK);
118
+ libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC);
119
+ }
120
+ }
121
+ }
122
+ ret
123
+ }
124
+
125
+ /// The `Wake` inside `value`.
126
+ pub fn get(value: VALUE) -> RbResult<&'static Wake> {
127
+ unsafe { TYPE.get(value) }
128
+ }
129
+
130
+ unsafe extern "C" fn wake_new(_class: VALUE) -> VALUE {
131
+ rb::wrap(|| {
132
+ let wake = Wake::new().map_err(|e| RubyErr::runtime(format!("pipe failed: {e}")))?;
133
+ TYPE.wrap(*CLASS.get().expect("registered") as VALUE, wake)
134
+ })
135
+ }
136
+
137
+ unsafe extern "C" fn wake_fd(this: VALUE) -> VALUE {
138
+ rb::wrap(|| Ok(rb::u64_value(get(this)?.pipe.read_fd as u64)))
139
+ }
140
+
141
+ unsafe extern "C" fn wake_clear(this: VALUE) -> VALUE {
142
+ rb::wrap(|| {
143
+ get(this)?.clear();
144
+ Ok(rb::qnil())
145
+ })
146
+ }
147
+
148
+ pub fn register(native: VALUE) -> RbResult<()> {
149
+ let class = rb::define_class_under(native, c"Wake")?;
150
+ let _ = CLASS.set(class as usize);
151
+ rb::define_singleton_method(class, c"new", wake_new as *const (), 0)?;
152
+ rb::define_method(class, c"fd", wake_fd as *const (), 0)?;
153
+ rb::define_method(class, c"clear", wake_clear as *const (), 0)?;
154
+ Ok(())
155
+ }
data/lib/zzq/broker.rb CHANGED
@@ -1,34 +1,103 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "socket"
4
-
5
3
  module ZZQ
6
- # MQTT broker. Owns the session map (by client id), retained-message
7
- # store, topic trie, and shared-subscription groups.
4
+ # An MQTT 3.1.1 and 5.0 broker with an MQTT-SN gateway on +udp://+. It
5
+ # runs on the process's IO lanes; see {ZZQ.io_threads=}.
8
6
  #
9
- # M3 stub: constructor + {#build_routing}. CONNECT handling,
10
- # subscription matching, and retained store come in M7.
11
- class Broker < Socket
12
- def build_routing(engine)
13
- Routing::Broker.new(engine)
7
+ # @example
8
+ # broker = ZZQ::Broker.new
9
+ # broker.bind("mqtt://0.0.0.0:1883")
10
+ class Broker
11
+ OPTIONS = %i[
12
+ max_packet_size max_inflight max_queued receive_maximum max_subscriptions max_retained
13
+ max_topic_levels topic_alias_maximum max_session_expiry max_connections max_keep_alive
14
+ connect_timeout sys_interval persistence tls
15
+ ].freeze
16
+
17
+ # Starts a broker and binds it. With a block, yields it and closes it
18
+ # after.
19
+ #
20
+ # @param endpoint [String] see {#bind}
21
+ # @param options [Hash] see {#initialize}
22
+ # @return [Broker, Object] the broker, or the block's result
23
+ def self.bind(endpoint, **options)
24
+ broker = new(**options)
25
+ broker.bind(endpoint)
26
+ return broker unless block_given?
27
+
28
+ begin
29
+ yield broker
30
+ ensure
31
+ broker.close
32
+ end
14
33
  end
15
34
 
35
+ # @return [String, nil] the endpoint bound last
36
+ attr_reader :last_endpoint
16
37
 
17
- # Number of currently-connected MQTT sessions.
18
- def session_count = @engine.routing.session_count
38
+ # Starts a broker.
39
+ #
40
+ # @param options [Hash] limits as in zzq-broker's +BrokerOptions+, such
41
+ # as +max_connections:+ or +max_queued:+; +max_keep_alive:+ and
42
+ # +sys_interval:+ (seconds, nil for none); +connect_timeout:+
43
+ # (seconds); +persistence:+ (path of a file for retained messages);
44
+ # +tls:+ (+cert:+, +key:+, +client_ca:+ as PEM strings)
45
+ def initialize(**options)
46
+ opts = Options.check(options, OPTIONS)
47
+ @native = Waiting.await(Native::Broker.start(opts))
48
+ @closed = false
49
+ end
19
50
 
51
+ # Listens on an endpoint.
52
+ #
53
+ # @param endpoint [String] +mqtt://host:port+, +mqtts://+, +tcp://+,
54
+ # +tls://+, +ws://+, +wss://+, +quic://+, +udp://+ (MQTT-SN),
55
+ # +ipc://+ or +inproc://+; port 0 picks a free port
56
+ # @return [String] the bound endpoint, with the actual port
57
+ def bind(endpoint)
58
+ @last_endpoint = Waiting.await(native.bind(Options.endpoint(endpoint)))
59
+ end
60
+
61
+ # Publishes a message as the broker itself.
62
+ #
63
+ # @param topic [String]
64
+ # @param payload [String]
65
+ # @param qos [Integer]
66
+ # @param retain [Boolean]
67
+ # @return [nil]
68
+ def publish(topic, payload, qos: 0, retain: false)
69
+ payload = payload.to_s unless payload.is_a?(String)
70
+ native.publish(topic, payload, qos, retain)
71
+ end
72
+
73
+ # @return [Hash{Symbol => Integer}] counters summed over lanes
74
+ def stats
75
+ Waiting.await(native.stats)
76
+ end
20
77
 
21
- # Inject a {ZZQ::Message} into the broker's delivery path as if it
22
- # had arrived via a PUBLISH packet. Fans out to every matching
23
- # subscriber. Use this to bridge non-MQTT inputs (NNQ, OMQ, a
24
- # file) into broker topics.
25
- def ingest(message)
26
- raise NotInAsyncContext unless Async::Task.current?
27
- @engine.routing.dispatch_message(message)
78
+ # Stops the broker and waits until its connections are closed.
79
+ #
80
+ # @return [nil]
81
+ def close
82
+ return if @closed
83
+
84
+ @closed = true
85
+ op = @native.shutdown
86
+ Waiting.await(op) if op
28
87
  nil
29
88
  end
30
- alias_method :<<, :ingest
89
+
90
+ # @return [Boolean]
91
+ def closed?
92
+ @closed
93
+ end
94
+
95
+ private
96
+
97
+ def native
98
+ raise ClosedError, "broker closed" if @closed
99
+
100
+ @native
101
+ end
31
102
  end
32
103
  end
33
-
34
- require_relative "routing/broker"