prosody 0.3.0 → 0.5.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.cargo/config.toml +3 -0
  3. data/.release-please-manifest.json +1 -1
  4. data/AGENTS.md +395 -0
  5. data/ARCHITECTURE.md +14 -4
  6. data/CHANGELOG.md +28 -0
  7. data/CLAUDE.md +1 -0
  8. data/CONFIGURATION.md +167 -0
  9. data/Cargo.lock +1115 -645
  10. data/Cargo.toml +7 -6
  11. data/README.md +436 -146
  12. data/Rakefile +11 -1
  13. data/examples/keyed_state.rb +70 -0
  14. data/examples/keyed_state.rbs +18 -0
  15. data/examples/keyed_state_windowing.rb +55 -0
  16. data/examples/keyed_state_windowing.rbs +16 -0
  17. data/ext/prosody/Cargo.toml +1 -0
  18. data/ext/prosody/src/admin.rs +1 -5
  19. data/ext/prosody/src/bridge/mod.rs +17 -32
  20. data/ext/prosody/src/client/config.rs +501 -28
  21. data/ext/prosody/src/client/mod.rs +167 -74
  22. data/ext/prosody/src/client/request.rs +132 -0
  23. data/ext/prosody/src/client/support.rs +122 -0
  24. data/ext/prosody/src/handler/context.rs +150 -5
  25. data/ext/prosody/src/handler/message.rs +67 -0
  26. data/ext/prosody/src/handler/mod.rs +115 -85
  27. data/ext/prosody/src/handler/state/mod.rs +488 -0
  28. data/ext/prosody/src/handler/state/registration.rs +104 -0
  29. data/ext/prosody/src/handler/state/scan.rs +218 -0
  30. data/ext/prosody/src/lib.rs +15 -3
  31. data/ext/prosody/src/published.rs +273 -0
  32. data/ext/prosody/src/scheduler/mod.rs +2 -2
  33. data/ext/prosody/src/scheduler/processor.rs +2 -2
  34. data/ext/prosody/src/scheduler/result.rs +7 -4
  35. data/ext/prosody/src/util.rs +86 -5
  36. data/lib/prosody/configuration.rb +71 -11
  37. data/lib/prosody/handler.rb +65 -8
  38. data/lib/prosody/native_stubs.rb +550 -9
  39. data/lib/prosody/request.rb +45 -0
  40. data/lib/prosody/state.rb +816 -0
  41. data/lib/prosody/version.rb +1 -1
  42. data/lib/prosody.rb +6 -0
  43. data/release-please-config.json +4 -0
  44. data/sig/configuration.rbs +70 -11
  45. data/sig/handler.rbs +17 -5
  46. data/sig/processor.rbs +28 -12
  47. data/sig/prosody.rbs +53 -7
  48. data/sig/request.rbs +66 -0
  49. data/sig/sentry.rbs +6 -0
  50. data/sig/state.rbs +390 -0
  51. data/steep_expectations.yml +57 -0
  52. data/typecheck/payload_types.rb +54 -0
  53. data/typecheck/payload_types.rbs +22 -0
  54. data/typecheck_negative/payload_types.rb +20 -0
  55. data/typecheck_negative/payload_types.rbs +9 -0
  56. metadata +32 -9
@@ -0,0 +1,218 @@
1
+ //! Concrete typed cursors for native state scans.
2
+ //!
3
+ //! Cancellation can discard an orphaned chunk. The attempt then closes the
4
+ //! cursor through the Ruby `ensure`, so no later operation observes that chunk.
5
+
6
+ use super::state_error;
7
+ use crate::bridge::{Bridge, QUEUE_CLASS};
8
+ use crate::handler::message::Message;
9
+ use crate::id;
10
+ use crate::tracing_util::extract_opentelemetry_context;
11
+ use crate::util::ThreadSafeValue;
12
+ use magnus::value::ReprValue;
13
+ use magnus::{Error, IntoValue, Ruby, Value};
14
+ use opentelemetry::propagation::TextMapCompositePropagator;
15
+ use opentelemetry::trace::FutureExt;
16
+ use prosody::consumer::event_context::StateCursor;
17
+ use prosody::consumer::message::ConsumerMessage;
18
+ use serde_json::Value as JsonValue;
19
+ use serde_magnus::serialize;
20
+ use std::cell::RefCell;
21
+ use std::collections::VecDeque;
22
+ use std::num::NonZeroUsize;
23
+ use std::sync::Arc;
24
+ use tracing::Span;
25
+
26
+ #[allow(clippy::unwrap_used, reason = "256 is a nonzero literal")]
27
+ const SCAN_READY_CHUNK_SIZE: NonZeroUsize = NonZeroUsize::new(256).unwrap();
28
+
29
+ struct ScanInner<T> {
30
+ cursor: Arc<StateCursor<T>>,
31
+ buffer: VecDeque<T>,
32
+ done: bool,
33
+ }
34
+
35
+ macro_rules! drive_scan {
36
+ ($ruby:expr, $this:expr, $inner:expr, |$item:ident| $convert:block) => {{
37
+ loop {
38
+ if let Some($item) = $inner.buffer.pop_front() {
39
+ return $convert;
40
+ }
41
+ if $inner.done {
42
+ return Ok($ruby.qnil().as_value());
43
+ }
44
+ let cursor = Arc::clone(&$inner.cursor);
45
+ let context = extract_opentelemetry_context($ruby, &$this.propagator)?;
46
+ let chunk = $this
47
+ .bridge
48
+ .wait_for(
49
+ $ruby,
50
+ async move {
51
+ cursor
52
+ .next_ready_chunk(SCAN_READY_CHUNK_SIZE)
53
+ .with_context(context)
54
+ .await
55
+ },
56
+ Span::current(),
57
+ )?
58
+ .map_err(|error| state_error($ruby, &error))?;
59
+ match chunk {
60
+ Some(items) => $inner.buffer.extend(items),
61
+ None => {
62
+ $inner.done = true;
63
+ let cursor = Arc::clone(&$inner.cursor);
64
+ $this.bridge.wait_for(
65
+ $ruby,
66
+ async move { cursor.close().await },
67
+ Span::current(),
68
+ )?;
69
+ return Ok($ruby.qnil().as_value());
70
+ }
71
+ }
72
+ }
73
+ }};
74
+ }
75
+
76
+ macro_rules! native_scan {
77
+ ($name:ident, $class:literal, $item:ty, |$ruby:ident, $item_name:ident| $convert:block) => {
78
+ /// Native cursor with one item type.
79
+ #[magnus::wrap(class = $class)]
80
+ pub struct $name {
81
+ inner: RefCell<ScanInner<$item>>,
82
+ lock: ThreadSafeValue,
83
+ bridge: Bridge,
84
+ propagator: Arc<TextMapCompositePropagator>,
85
+ }
86
+
87
+ impl $name {
88
+ pub(super) fn new(
89
+ ruby: &Ruby,
90
+ cursor: Box<StateCursor<$item>>,
91
+ bridge: Bridge,
92
+ propagator: Arc<TextMapCompositePropagator>,
93
+ ) -> Result<Self, Error> {
94
+ let queue: Value = ruby.get_inner(&QUEUE_CLASS).funcall(id!(ruby, "new"), ())?;
95
+ let _: Value = queue.funcall(id!(ruby, "push"), (ruby.qnil(),))?;
96
+ Ok(Self {
97
+ inner: RefCell::new(ScanInner {
98
+ cursor: Arc::from(cursor),
99
+ buffer: VecDeque::new(),
100
+ done: false,
101
+ }),
102
+ lock: ThreadSafeValue::new(queue, bridge.clone()),
103
+ bridge,
104
+ propagator,
105
+ })
106
+ }
107
+
108
+ fn acquire(&self, ruby: &Ruby) -> Result<(), Error> {
109
+ let _: Value = self.lock.get(ruby).funcall(id!(ruby, "pop"), ())?;
110
+ Ok(())
111
+ }
112
+
113
+ fn release(&self, ruby: &Ruby) {
114
+ let _: Result<Value, Error> = self
115
+ .lock
116
+ .get(ruby)
117
+ .funcall(id!(ruby, "push"), (ruby.qnil(),));
118
+ }
119
+
120
+ pub(super) fn next(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
121
+ this.acquire(ruby)?;
122
+ let out = Self::next_locked(ruby, this);
123
+ this.release(ruby);
124
+ out
125
+ }
126
+
127
+ fn next_locked(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
128
+ let inner = &mut *this.inner.borrow_mut();
129
+ drive_scan!(ruby, this, inner, |$item_name| {
130
+ let $ruby = ruby;
131
+ $convert
132
+ })
133
+ }
134
+
135
+ pub(super) fn close(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
136
+ this.acquire(ruby)?;
137
+ let out = Self::close_locked(ruby, this);
138
+ this.release(ruby);
139
+ out
140
+ }
141
+
142
+ fn close_locked(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
143
+ let inner = &mut *this.inner.borrow_mut();
144
+ inner.done = true;
145
+ inner.buffer.clear();
146
+ let cursor = Arc::clone(&inner.cursor);
147
+ this.bridge
148
+ .wait_for(ruby, async move { cursor.close().await }, Span::current())?;
149
+ Ok(ruby.qnil().as_value())
150
+ }
151
+ }
152
+ };
153
+ }
154
+
155
+ native_scan!(
156
+ NativeJsonDequeScan,
157
+ "Prosody::NativeJsonDequeScan",
158
+ JsonValue,
159
+ |ruby, item| { serialize(ruby, &item) }
160
+ );
161
+ native_scan!(
162
+ NativeJsonMapScan,
163
+ "Prosody::NativeJsonMapScan",
164
+ (String, JsonValue),
165
+ |ruby, item| {
166
+ let (key, value) = item;
167
+ let value: Value = serialize(ruby, &value)?;
168
+ Ok((key, value).into_value_with(ruby))
169
+ }
170
+ );
171
+ native_scan!(
172
+ NativeMessageDequeScan,
173
+ "Prosody::NativeMessageDequeScan",
174
+ ConsumerMessage<JsonValue>,
175
+ |ruby, item| { Ok(Message::from(item).into_value_with(ruby)) }
176
+ );
177
+ native_scan!(
178
+ NativeMessageMapScan,
179
+ "Prosody::NativeMessageMapScan",
180
+ (String, ConsumerMessage<JsonValue>),
181
+ |ruby, item| {
182
+ let (key, message) = item;
183
+ Ok((key, Message::from(message)).into_value_with(ruby))
184
+ }
185
+ );
186
+ native_scan!(
187
+ NativeMapKeyScan,
188
+ "Prosody::NativeMapKeyScan",
189
+ String,
190
+ |ruby, item| { Ok(item.into_value_with(ruby)) }
191
+ );
192
+
193
+ pub(crate) fn published_map_scan(
194
+ ruby: &Ruby,
195
+ cursor: Box<StateCursor<(String, JsonValue)>>,
196
+ bridge: Bridge,
197
+ propagator: Arc<TextMapCompositePropagator>,
198
+ ) -> Result<NativeJsonMapScan, Error> {
199
+ NativeJsonMapScan::new(ruby, cursor, bridge, propagator)
200
+ }
201
+
202
+ pub(crate) fn published_map_key_scan(
203
+ ruby: &Ruby,
204
+ cursor: Box<StateCursor<String>>,
205
+ bridge: Bridge,
206
+ propagator: Arc<TextMapCompositePropagator>,
207
+ ) -> Result<NativeMapKeyScan, Error> {
208
+ NativeMapKeyScan::new(ruby, cursor, bridge, propagator)
209
+ }
210
+
211
+ pub(crate) fn published_deque_scan(
212
+ ruby: &Ruby,
213
+ cursor: Box<StateCursor<JsonValue>>,
214
+ bridge: Bridge,
215
+ propagator: Arc<TextMapCompositePropagator>,
216
+ ) -> Result<NativeJsonDequeScan, Error> {
217
+ NativeJsonDequeScan::new(ruby, cursor, bridge, propagator)
218
+ }
@@ -17,6 +17,8 @@ use crate::bridge::Bridge;
17
17
  use magnus::value::Lazy;
18
18
  use magnus::{Error, RModule, Ruby};
19
19
  use mimalloc::MiMalloc;
20
+ use std::io::{self, Write};
21
+ use std::process;
20
22
  use std::sync::{LazyLock, OnceLock};
21
23
  use tokio::runtime::Runtime;
22
24
 
@@ -26,6 +28,7 @@ mod client;
26
28
  mod gvl;
27
29
  mod handler;
28
30
  mod logging;
31
+ mod published;
29
32
  mod scheduler;
30
33
  mod tracing_util;
31
34
  mod util;
@@ -44,9 +47,16 @@ pub static TRACING_INIT: OnceLock<()> = OnceLock::new();
44
47
  ///
45
48
  /// This runtime powers all async operations in the extension, including
46
49
  /// message processing, scheduling, and communication with Ruby.
47
- #[allow(clippy::expect_used)]
48
- static RUNTIME: LazyLock<Runtime> =
49
- LazyLock::new(|| Runtime::new().expect("Failed to create Tokio runtime"));
50
+ static RUNTIME: LazyLock<Runtime> = LazyLock::new(|| match Runtime::new() {
51
+ Ok(runtime) => runtime,
52
+ Err(error) => {
53
+ drop(writeln!(
54
+ io::stderr().lock(),
55
+ "failed to create Tokio runtime: {error:#}"
56
+ ));
57
+ process::abort();
58
+ }
59
+ });
50
60
 
51
61
  /// Reference to the root Ruby module for this extension.
52
62
  ///
@@ -75,7 +85,9 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
75
85
  admin::init(ruby)?;
76
86
  bridge::init(ruby)?;
77
87
  handler::init(ruby)?;
88
+ published::init(ruby)?;
78
89
  client::init(ruby)?;
90
+ util::init(ruby)?;
79
91
 
80
92
  Ok(())
81
93
  }
@@ -0,0 +1,273 @@
1
+ //! Read-only published-state handles for Ruby.
2
+
3
+ use crate::bridge::Bridge;
4
+ use crate::handler::{
5
+ NativeJsonDequeScan, NativeJsonMapScan, NativeMapKeyScan, parse_direction,
6
+ published_deque_scan, published_map_key_scan, published_map_scan,
7
+ };
8
+ use crate::{ROOT_MOD, id};
9
+ use magnus::value::ReprValue;
10
+ use magnus::{Error, Module, Ruby, StaticSymbol, Value, method};
11
+ use opentelemetry::propagation::TextMapCompositePropagator;
12
+ use prosody::JsonCodec;
13
+ use prosody::high_level::erased::{
14
+ ErasedDirection, SharedDequeReader, SharedMapReader, SharedValueReader,
15
+ };
16
+ use prosody::state::Direction;
17
+ use serde_magnus::serialize;
18
+ use std::sync::Arc;
19
+ use tracing::Span;
20
+
21
+ fn read_error(ruby: &Ruby, error: &impl ToString) -> Error {
22
+ Error::new(ruby.exception_runtime_error(), error.to_string())
23
+ }
24
+
25
+ fn erased_direction(direction: Direction) -> ErasedDirection {
26
+ match direction {
27
+ Direction::Forward => ErasedDirection::Forward,
28
+ Direction::Backward => ErasedDirection::Backward,
29
+ }
30
+ }
31
+
32
+ #[magnus::wrap(class = "Prosody::NativePublishedValue")]
33
+ pub(crate) struct NativePublishedValue {
34
+ pub(crate) inner: SharedValueReader<JsonCodec>,
35
+ pub(crate) bridge: Bridge,
36
+ }
37
+
38
+ impl NativePublishedValue {
39
+ fn get(ruby: &Ruby, this: &Self, key: String) -> Result<Value, Error> {
40
+ let inner = Arc::clone(&this.inner);
41
+ let value = this
42
+ .bridge
43
+ .wait_for(ruby, async move { inner.get(key).await }, Span::current())?
44
+ .map_err(|error| read_error(ruby, &error))?;
45
+ match value {
46
+ Some(value) => serialize(ruby, &value),
47
+ None => Ok(ruby.qnil().as_value()),
48
+ }
49
+ }
50
+ }
51
+
52
+ #[magnus::wrap(class = "Prosody::NativePublishedMap")]
53
+ pub(crate) struct NativePublishedMap {
54
+ pub(crate) inner: SharedMapReader<JsonCodec>,
55
+ pub(crate) bridge: Bridge,
56
+ pub(crate) propagator: Arc<TextMapCompositePropagator>,
57
+ }
58
+
59
+ impl NativePublishedMap {
60
+ fn get(ruby: &Ruby, this: &Self, key: String, map_key: String) -> Result<Value, Error> {
61
+ let inner = Arc::clone(&this.inner);
62
+ let value = this
63
+ .bridge
64
+ .wait_for(
65
+ ruby,
66
+ async move { inner.get(key, map_key).await },
67
+ Span::current(),
68
+ )?
69
+ .map_err(|error| read_error(ruby, &error))?;
70
+ match value {
71
+ Some(value) => serialize(ruby, &value),
72
+ None => Ok(ruby.qnil().as_value()),
73
+ }
74
+ }
75
+
76
+ fn get_many(
77
+ ruby: &Ruby,
78
+ this: &Self,
79
+ key: String,
80
+ map_keys: Vec<String>,
81
+ ) -> Result<Value, Error> {
82
+ let inner = Arc::clone(&this.inner);
83
+ let values = this
84
+ .bridge
85
+ .wait_for(
86
+ ruby,
87
+ async move { inner.get_many(key, map_keys).await },
88
+ Span::current(),
89
+ )?
90
+ .map_err(|error| read_error(ruby, &error))?;
91
+ serialize(ruby, &values)
92
+ }
93
+
94
+ fn contains_key(ruby: &Ruby, this: &Self, key: String, map_key: String) -> Result<bool, Error> {
95
+ let inner = Arc::clone(&this.inner);
96
+ this.bridge
97
+ .wait_for(
98
+ ruby,
99
+ async move { inner.contains_key(key, map_key).await },
100
+ Span::current(),
101
+ )?
102
+ .map_err(|error| read_error(ruby, &error))
103
+ }
104
+
105
+ fn scan(
106
+ ruby: &Ruby,
107
+ this: &Self,
108
+ key: String,
109
+ direction: StaticSymbol,
110
+ ) -> Result<NativeJsonMapScan, Error> {
111
+ let direction = erased_direction(parse_direction(ruby, direction)?);
112
+ let inner = Arc::clone(&this.inner);
113
+ let cursor = this
114
+ .bridge
115
+ .wait_for(
116
+ ruby,
117
+ async move { inner.stream(key, direction).await },
118
+ Span::current(),
119
+ )?
120
+ .map_err(|error| read_error(ruby, &error))?;
121
+ published_map_scan(
122
+ ruby,
123
+ cursor,
124
+ this.bridge.clone(),
125
+ Arc::clone(&this.propagator),
126
+ )
127
+ }
128
+
129
+ fn keys(
130
+ ruby: &Ruby,
131
+ this: &Self,
132
+ key: String,
133
+ direction: StaticSymbol,
134
+ ) -> Result<NativeMapKeyScan, Error> {
135
+ let direction = erased_direction(parse_direction(ruby, direction)?);
136
+ let inner = Arc::clone(&this.inner);
137
+ let cursor = this
138
+ .bridge
139
+ .wait_for(
140
+ ruby,
141
+ async move { inner.keys(key, direction).await },
142
+ Span::current(),
143
+ )?
144
+ .map_err(|error| read_error(ruby, &error))?;
145
+ published_map_key_scan(
146
+ ruby,
147
+ cursor,
148
+ this.bridge.clone(),
149
+ Arc::clone(&this.propagator),
150
+ )
151
+ }
152
+ }
153
+
154
+ #[magnus::wrap(class = "Prosody::NativePublishedDeque")]
155
+ pub(crate) struct NativePublishedDeque {
156
+ pub(crate) inner: SharedDequeReader<JsonCodec>,
157
+ pub(crate) bridge: Bridge,
158
+ pub(crate) propagator: Arc<TextMapCompositePropagator>,
159
+ }
160
+
161
+ impl NativePublishedDeque {
162
+ fn get(ruby: &Ruby, this: &Self, key: String, index: usize) -> Result<Value, Error> {
163
+ let inner = Arc::clone(&this.inner);
164
+ let value = this
165
+ .bridge
166
+ .wait_for(
167
+ ruby,
168
+ async move { inner.get(key, index).await },
169
+ Span::current(),
170
+ )?
171
+ .map_err(|error| read_error(ruby, &error))?;
172
+ match value {
173
+ Some(value) => serialize(ruby, &value),
174
+ None => Ok(ruby.qnil().as_value()),
175
+ }
176
+ }
177
+
178
+ fn length(ruby: &Ruby, this: &Self, key: String) -> Result<usize, Error> {
179
+ let inner = Arc::clone(&this.inner);
180
+ this.bridge
181
+ .wait_for(ruby, async move { inner.len(key).await }, Span::current())?
182
+ .map_err(|error| read_error(ruby, &error))
183
+ }
184
+
185
+ fn is_empty(ruby: &Ruby, this: &Self, key: String) -> Result<bool, Error> {
186
+ let inner = Arc::clone(&this.inner);
187
+ this.bridge
188
+ .wait_for(
189
+ ruby,
190
+ async move { inner.is_empty(key).await },
191
+ Span::current(),
192
+ )?
193
+ .map_err(|error| read_error(ruby, &error))
194
+ }
195
+
196
+ fn peek_front(ruby: &Ruby, this: &Self, key: String) -> Result<Value, Error> {
197
+ let inner = Arc::clone(&this.inner);
198
+ let value = this
199
+ .bridge
200
+ .wait_for(
201
+ ruby,
202
+ async move { inner.peek_front(key).await },
203
+ Span::current(),
204
+ )?
205
+ .map_err(|error| read_error(ruby, &error))?;
206
+ match value {
207
+ Some(value) => serialize(ruby, &value),
208
+ None => Ok(ruby.qnil().as_value()),
209
+ }
210
+ }
211
+
212
+ fn peek_back(ruby: &Ruby, this: &Self, key: String) -> Result<Value, Error> {
213
+ let inner = Arc::clone(&this.inner);
214
+ let value = this
215
+ .bridge
216
+ .wait_for(
217
+ ruby,
218
+ async move { inner.peek_back(key).await },
219
+ Span::current(),
220
+ )?
221
+ .map_err(|error| read_error(ruby, &error))?;
222
+ match value {
223
+ Some(value) => serialize(ruby, &value),
224
+ None => Ok(ruby.qnil().as_value()),
225
+ }
226
+ }
227
+
228
+ fn scan(
229
+ ruby: &Ruby,
230
+ this: &Self,
231
+ key: String,
232
+ direction: StaticSymbol,
233
+ ) -> Result<NativeJsonDequeScan, Error> {
234
+ let direction = erased_direction(parse_direction(ruby, direction)?);
235
+ let inner = Arc::clone(&this.inner);
236
+ let cursor = this
237
+ .bridge
238
+ .wait_for(
239
+ ruby,
240
+ async move { inner.stream(key, direction).await },
241
+ Span::current(),
242
+ )?
243
+ .map_err(|error| read_error(ruby, &error))?;
244
+ published_deque_scan(
245
+ ruby,
246
+ cursor,
247
+ this.bridge.clone(),
248
+ Arc::clone(&this.propagator),
249
+ )
250
+ }
251
+ }
252
+
253
+ pub(crate) fn init(ruby: &Ruby) -> Result<(), Error> {
254
+ let module = ruby.get_inner(&ROOT_MOD);
255
+ let value = module.define_class(id!(ruby, "NativePublishedValue"), ruby.class_object())?;
256
+ value.define_method("get", method!(NativePublishedValue::get, 1))?;
257
+
258
+ let map = module.define_class(id!(ruby, "NativePublishedMap"), ruby.class_object())?;
259
+ map.define_method("get", method!(NativePublishedMap::get, 2))?;
260
+ map.define_method("get_many", method!(NativePublishedMap::get_many, 2))?;
261
+ map.define_method("contains_key", method!(NativePublishedMap::contains_key, 2))?;
262
+ map.define_method("scan", method!(NativePublishedMap::scan, 2))?;
263
+ map.define_method("keys", method!(NativePublishedMap::keys, 2))?;
264
+
265
+ let deque = module.define_class(id!(ruby, "NativePublishedDeque"), ruby.class_object())?;
266
+ deque.define_method("get", method!(NativePublishedDeque::get, 2))?;
267
+ deque.define_method("length", method!(NativePublishedDeque::length, 1))?;
268
+ deque.define_method("is_empty", method!(NativePublishedDeque::is_empty, 1))?;
269
+ deque.define_method("peek_front", method!(NativePublishedDeque::peek_front, 1))?;
270
+ deque.define_method("peek_back", method!(NativePublishedDeque::peek_back, 1))?;
271
+ deque.define_method("scan", method!(NativePublishedDeque::scan, 2))?;
272
+ Ok(())
273
+ }
@@ -14,7 +14,7 @@ use crate::bridge::{Bridge, BridgeError};
14
14
  use crate::scheduler::handle::TaskHandle;
15
15
  use crate::scheduler::processor::RubyProcessor;
16
16
  use crate::scheduler::result::result_channel;
17
- use magnus::{Error, Ruby};
17
+ use magnus::{Error, Ruby, Value};
18
18
  use opentelemetry::propagation::{TextMapCompositePropagator, TextMapPropagator};
19
19
  use prosody::propagator::new_propagator;
20
20
  use std::collections::HashMap;
@@ -99,7 +99,7 @@ impl Scheduler {
99
99
  function: F,
100
100
  ) -> Result<TaskHandle, SchedulerError>
101
101
  where
102
- F: FnOnce(&Ruby) -> Result<(), Error> + Send + 'static,
102
+ F: FnOnce(&Ruby) -> Result<Value, Error> + Send + 'static,
103
103
  {
104
104
  let mut carrier: HashMap<String, String> = HashMap::with_capacity(2);
105
105
  self.propagator
@@ -104,7 +104,7 @@ impl RubyProcessor {
104
104
  function: F,
105
105
  ) -> Result<CancellationToken, Error>
106
106
  where
107
- F: FnOnce(&Ruby) -> Result<(), Error> + Send + 'static,
107
+ F: FnOnce(&Ruby) -> Result<Value, Error> + Send + 'static,
108
108
  {
109
109
  if self.is_shutdown.load(Relaxed) {
110
110
  return Err(Error::new(
@@ -122,7 +122,7 @@ impl RubyProcessor {
122
122
  if let Some(function) = maybe_function.take() {
123
123
  function(ruby)
124
124
  } else {
125
- Ok(())
125
+ Ok(ruby.qnil().as_value())
126
126
  }
127
127
  });
128
128
 
@@ -14,6 +14,7 @@ use magnus::block::Proc;
14
14
  use magnus::value::ReprValue;
15
15
  use magnus::{Error, Ruby, TryConvert, Value, kwargs};
16
16
  use prosody::error::{ClassifyError, ErrorCategory};
17
+ use serde_magnus::deserialize;
17
18
  use thiserror::Error;
18
19
  use tokio::sync::oneshot;
19
20
  use tracing::debug;
@@ -40,7 +41,7 @@ pub fn result_channel() -> (ResultSender, ResultReceiver) {
40
41
  #[educe(Debug)]
41
42
  pub struct ResultSender {
42
43
  #[educe(Debug(ignore))]
43
- result_tx: AtomicTake<oneshot::Sender<Result<(), ProcessingError>>>,
44
+ result_tx: AtomicTake<oneshot::Sender<Result<serde_json::Value, ProcessingError>>>,
44
45
  }
45
46
 
46
47
  /// Receives task results in Rust from Ruby.
@@ -51,7 +52,7 @@ pub struct ResultSender {
51
52
  #[educe(Debug)]
52
53
  pub struct ResultReceiver {
53
54
  #[educe(Debug(ignore))]
54
- result_rx: oneshot::Receiver<Result<(), ProcessingError>>,
55
+ result_rx: oneshot::Receiver<Result<serde_json::Value, ProcessingError>>,
55
56
  }
56
57
 
57
58
  impl ResultSender {
@@ -80,7 +81,9 @@ impl ResultSender {
80
81
  };
81
82
 
82
83
  if is_success {
83
- if result_tx.send(Ok(())).is_err() {
84
+ let result = deserialize(ruby, result)
85
+ .map_err(|error| ProcessingError::Permanent(error.to_string()));
86
+ if result_tx.send(result).is_err() {
84
87
  debug!("discarding result; receiver went away");
85
88
  }
86
89
 
@@ -153,7 +156,7 @@ impl ResultReceiver {
153
156
  /// Returns a `ProcessingError` if:
154
157
  /// - The task failed (with either a permanent or transient error)
155
158
  /// - The channel was closed unexpectedly (e.g., the Ruby VM terminated)
156
- pub async fn receive(self) -> Result<(), ProcessingError> {
159
+ pub async fn receive(self) -> Result<serde_json::Value, ProcessingError> {
157
160
  self.result_rx.await.map_err(|_| ProcessingError::Closed)?
158
161
  }
159
162
  }