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
@@ -5,6 +5,10 @@
5
5
  //! information from Kafka messages and schedule timer events.
6
6
 
7
7
  use crate::bridge::Bridge;
8
+ use crate::handler::state::{
9
+ NativeJsonDequeState, NativeJsonMapState, NativeJsonValueState, NativeMessageDequeState,
10
+ NativeMessageMapState, NativeMessageValueState, state_error,
11
+ };
8
12
  use crate::tracing_util::extract_opentelemetry_context;
9
13
  use crate::{ROOT_MOD, id};
10
14
  use educe::Educe;
@@ -35,9 +39,8 @@ pub struct Context {
35
39
  ///
36
40
  /// This field is marked as hidden in debug output to prevent logging large
37
41
  /// data.
38
- #[allow(dead_code)]
39
42
  #[educe(Debug(ignore))]
40
- inner: BoxEventContext,
43
+ inner: BoxEventContext<serde_json::Value>,
41
44
 
42
45
  /// Bridge for handling async operations
43
46
  #[educe(Debug(ignore))]
@@ -57,7 +60,7 @@ impl Context {
57
60
  /// * `bridge` - The bridge for handling async operations
58
61
  /// * `propagator` - Shared OpenTelemetry propagator for distributed tracing
59
62
  pub fn new(
60
- inner: BoxEventContext,
63
+ inner: BoxEventContext<serde_json::Value>,
61
64
  bridge: Bridge,
62
65
  propagator: Arc<TextMapCompositePropagator>,
63
66
  ) -> Self {
@@ -302,10 +305,10 @@ impl Context {
302
305
  async move { inner.scheduled(TimerType::Application).await },
303
306
  span,
304
307
  )?
305
- .map_err(|e| {
308
+ .map_err(|error| {
306
309
  Error::new(
307
310
  ruby.exception_runtime_error(),
308
- format!("Failed to get scheduled times: {e}"),
311
+ format!("Failed to get scheduled times: {error:#}"),
309
312
  )
310
313
  })?;
311
314
 
@@ -319,6 +322,131 @@ impl Context {
319
322
 
320
323
  Ok(ruby_array.as_value())
321
324
  }
325
+
326
+ /// Vends the handle for the named JSON value collection.
327
+ ///
328
+ /// Vending verifies the collection's registration (core-side); no span is
329
+ /// opened here — vended handles outlive the call, and every operation opens
330
+ /// its own span.
331
+ ///
332
+ /// # Errors
333
+ ///
334
+ /// Returns a permanent state error if the name is unregistered or its
335
+ /// registered identity mismatches.
336
+ #[allow(clippy::needless_pass_by_value, reason = "Magnus method argument type")]
337
+ fn value_state(ruby: &Ruby, this: &Self, name: String) -> Result<NativeJsonValueState, Error> {
338
+ let handle = this
339
+ .inner
340
+ .value_state(&name)
341
+ .map_err(|error| state_error(ruby, &error))?;
342
+ Ok(NativeJsonValueState::new(
343
+ Arc::from(handle),
344
+ this.bridge.clone(),
345
+ Arc::clone(&this.propagator),
346
+ ))
347
+ }
348
+
349
+ /// Vends the handle for the named JSON map collection.
350
+ ///
351
+ /// # Errors
352
+ ///
353
+ /// See [`value_state`](Self::value_state).
354
+ #[allow(clippy::needless_pass_by_value, reason = "Magnus method argument type")]
355
+ fn map_state(ruby: &Ruby, this: &Self, name: String) -> Result<NativeJsonMapState, Error> {
356
+ let handle = this
357
+ .inner
358
+ .map_state(&name)
359
+ .map_err(|error| state_error(ruby, &error))?;
360
+ Ok(NativeJsonMapState::new(
361
+ Arc::from(handle),
362
+ this.bridge.clone(),
363
+ Arc::clone(&this.propagator),
364
+ ))
365
+ }
366
+
367
+ /// Vends the handle for the named JSON deque collection.
368
+ ///
369
+ /// # Errors
370
+ ///
371
+ /// See [`value_state`](Self::value_state).
372
+ #[allow(clippy::needless_pass_by_value, reason = "Magnus method argument type")]
373
+ fn deque_state(ruby: &Ruby, this: &Self, name: String) -> Result<NativeJsonDequeState, Error> {
374
+ let handle = this
375
+ .inner
376
+ .deque_state(&name)
377
+ .map_err(|error| state_error(ruby, &error))?;
378
+ Ok(NativeJsonDequeState::new(
379
+ Arc::from(handle),
380
+ this.bridge.clone(),
381
+ Arc::clone(&this.propagator),
382
+ ))
383
+ }
384
+
385
+ /// Vends the handle for the named Kafka-message value collection.
386
+ ///
387
+ /// # Errors
388
+ ///
389
+ /// See [`value_state`](Self::value_state).
390
+ #[allow(clippy::needless_pass_by_value, reason = "Magnus method argument type")]
391
+ fn message_value_state(
392
+ ruby: &Ruby,
393
+ this: &Self,
394
+ name: String,
395
+ ) -> Result<NativeMessageValueState, Error> {
396
+ let handle = this
397
+ .inner
398
+ .message_value_state(&name)
399
+ .map_err(|error| state_error(ruby, &error))?;
400
+ Ok(NativeMessageValueState::new(
401
+ Arc::from(handle),
402
+ this.bridge.clone(),
403
+ Arc::clone(&this.propagator),
404
+ ))
405
+ }
406
+
407
+ /// Vends the handle for the named Kafka-message map collection.
408
+ ///
409
+ /// # Errors
410
+ ///
411
+ /// See [`value_state`](Self::value_state).
412
+ #[allow(clippy::needless_pass_by_value, reason = "Magnus method argument type")]
413
+ fn message_map_state(
414
+ ruby: &Ruby,
415
+ this: &Self,
416
+ name: String,
417
+ ) -> Result<NativeMessageMapState, Error> {
418
+ let handle = this
419
+ .inner
420
+ .message_map_state(&name)
421
+ .map_err(|error| state_error(ruby, &error))?;
422
+ Ok(NativeMessageMapState::new(
423
+ Arc::from(handle),
424
+ this.bridge.clone(),
425
+ Arc::clone(&this.propagator),
426
+ ))
427
+ }
428
+
429
+ /// Vends the handle for the named Kafka-message deque collection.
430
+ ///
431
+ /// # Errors
432
+ ///
433
+ /// See [`value_state`](Self::value_state).
434
+ #[allow(clippy::needless_pass_by_value, reason = "Magnus method argument type")]
435
+ fn message_deque_state(
436
+ ruby: &Ruby,
437
+ this: &Self,
438
+ name: String,
439
+ ) -> Result<NativeMessageDequeState, Error> {
440
+ let handle = this
441
+ .inner
442
+ .message_deque_state(&name)
443
+ .map_err(|error| state_error(ruby, &error))?;
444
+ Ok(NativeMessageDequeState::new(
445
+ Arc::from(handle),
446
+ this.bridge.clone(),
447
+ Arc::clone(&this.propagator),
448
+ ))
449
+ }
322
450
  }
323
451
 
324
452
  /// Initializes the Context class in Ruby.
@@ -357,6 +485,23 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> {
357
485
  )?;
358
486
  class.define_method(id!(ruby, "scheduled"), method!(Context::scheduled, 0))?;
359
487
 
488
+ // Keyed-state vend methods
489
+ class.define_method(id!(ruby, "value_state"), method!(Context::value_state, 1))?;
490
+ class.define_method(id!(ruby, "map_state"), method!(Context::map_state, 1))?;
491
+ class.define_method(id!(ruby, "deque_state"), method!(Context::deque_state, 1))?;
492
+ class.define_method(
493
+ id!(ruby, "message_value_state"),
494
+ method!(Context::message_value_state, 1),
495
+ )?;
496
+ class.define_method(
497
+ id!(ruby, "message_map_state"),
498
+ method!(Context::message_map_state, 1),
499
+ )?;
500
+ class.define_method(
501
+ id!(ruby, "message_deque_state"),
502
+ method!(Context::message_deque_state, 1),
503
+ )?;
504
+
360
505
  Ok(())
361
506
  }
362
507
 
@@ -24,6 +24,15 @@ pub struct Message {
24
24
  inner: ConsumerMessage<serde_json::Value>,
25
25
  }
26
26
 
27
+ /// Ruby wrapper for an excise record.
28
+ #[derive(Educe, Clone)]
29
+ #[educe(Debug)]
30
+ #[magnus::wrap(class = "Prosody::ExciseMessage", frozen_shareable)]
31
+ pub struct ExciseMessage {
32
+ #[educe(Debug(ignore))]
33
+ inner: ConsumerMessage<()>,
34
+ }
35
+
27
36
  impl Message {
28
37
  /// Returns the topic name this message was published to.
29
38
  ///
@@ -100,9 +109,54 @@ impl Message {
100
109
  /// # Errors
101
110
  ///
102
111
  /// Returns an error if payload deserialization fails.
112
+ ///
113
+ /// The public RBS exposes this JSON value as `Message[Payload]#payload`.
114
+ /// That generic parameter is static-only: this native boundary continues
115
+ /// to deserialize JSON into ordinary Ruby objects without runtime schema
116
+ /// validation.
103
117
  fn payload(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
104
118
  serialize(ruby, this.inner.payload())
105
119
  }
120
+
121
+ /// Clones the wrapped `ConsumerMessage` for a message-collection write.
122
+ ///
123
+ /// The wrapper holds the real `ConsumerMessage`, so a message write clones
124
+ /// the inner value directly — a cheap operation.
125
+ ///
126
+ /// # Returns
127
+ ///
128
+ /// An owned clone of the wrapped `ConsumerMessage`.
129
+ pub(crate) fn consumer_message(&self) -> ConsumerMessage<serde_json::Value> {
130
+ self.inner.clone()
131
+ }
132
+ }
133
+
134
+ impl ExciseMessage {
135
+ fn topic(&self) -> &'static str {
136
+ self.inner.topic().as_ref()
137
+ }
138
+
139
+ fn partition(&self) -> i32 {
140
+ self.inner.partition()
141
+ }
142
+
143
+ fn offset(&self) -> i64 {
144
+ self.inner.offset()
145
+ }
146
+
147
+ fn key(&self) -> &str {
148
+ self.inner.key()
149
+ }
150
+
151
+ fn timestamp(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
152
+ let epoch_micros = this.inner.timestamp().timestamp_micros();
153
+ ruby.module_kernel()
154
+ .const_get::<_, RClass>(id!(ruby, "Time"))?
155
+ .funcall(
156
+ id!(ruby, "at"),
157
+ (epoch_micros, ruby.to_symbol("microsecond")),
158
+ )
159
+ }
106
160
  }
107
161
 
108
162
  impl From<ConsumerMessage<serde_json::Value>> for Message {
@@ -116,6 +170,12 @@ impl From<ConsumerMessage<serde_json::Value>> for Message {
116
170
  }
117
171
  }
118
172
 
173
+ impl From<ConsumerMessage<()>> for ExciseMessage {
174
+ fn from(inner: ConsumerMessage<()>) -> Self {
175
+ Self { inner }
176
+ }
177
+ }
178
+
119
179
  /// Initializes the `Prosody::Message` Ruby class and defines its methods.
120
180
  ///
121
181
  /// # Arguments
@@ -140,5 +200,12 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> {
140
200
  class.define_method(id!(ruby, "timestamp"), method!(Message::timestamp, 0))?;
141
201
  class.define_method(id!(ruby, "payload"), method!(Message::payload, 0))?;
142
202
 
203
+ let class = module.define_class(id!(ruby, "ExciseMessage"), ruby.class_object())?;
204
+ class.define_method(id!(ruby, "topic"), method!(ExciseMessage::topic, 0))?;
205
+ class.define_method(id!(ruby, "partition"), method!(ExciseMessage::partition, 0))?;
206
+ class.define_method(id!(ruby, "offset"), method!(ExciseMessage::offset, 0))?;
207
+ class.define_method(id!(ruby, "key"), method!(ExciseMessage::key, 0))?;
208
+ class.define_method(id!(ruby, "timestamp"), method!(ExciseMessage::timestamp, 0))?;
209
+
143
210
  Ok(())
144
211
  }
@@ -11,7 +11,7 @@
11
11
 
12
12
  use crate::bridge::{Bridge, BridgeError};
13
13
  use crate::handler::context::Context;
14
- use crate::handler::message::Message;
14
+ use crate::handler::message::{ExciseMessage, Message};
15
15
  use crate::handler::trigger::Timer;
16
16
  use crate::id;
17
17
  use crate::scheduler::result::ProcessingError;
@@ -19,7 +19,7 @@ use crate::scheduler::{Scheduler, SchedulerError};
19
19
  use crate::util::ThreadSafeValue;
20
20
  use futures::pin_mut;
21
21
  use magnus::value::ReprValue;
22
- use magnus::{Error, Ruby, Value};
22
+ use magnus::{Error, IntoValue, Ruby, Value};
23
23
  use opentelemetry::propagation::TextMapCompositePropagator;
24
24
  use opentelemetry::trace::Status;
25
25
  use prosody::consumer::event_context::EventContext;
@@ -27,19 +27,26 @@ use prosody::consumer::message::ConsumerMessage;
27
27
  use prosody::consumer::middleware::FallibleHandler;
28
28
  use prosody::consumer::{DemandType, Keyed};
29
29
  use prosody::error::{ClassifyError, ErrorCategory};
30
+ use prosody::high_level::{ClientHandler, JsonCodecs};
30
31
  use prosody::propagator::new_propagator;
31
32
  use prosody::timers::{TimerType, Trigger as ProsodyTrigger};
32
33
  use std::collections::HashMap;
33
34
  use std::sync::Arc;
34
35
  use thiserror::Error;
35
36
  use tokio::select;
36
- use tracing::{Instrument, info_span};
37
+ use tracing::{Instrument, Span, info_span};
37
38
  use tracing_opentelemetry::OpenTelemetrySpanExt;
38
39
 
39
40
  mod context;
40
41
  mod message;
42
+ mod state;
41
43
  mod trigger;
42
44
 
45
+ pub(crate) use state::{
46
+ NativeJsonDequeScan, NativeJsonMapScan, NativeMapKeyScan, parse_direction,
47
+ published_deque_scan, published_map_key_scan, published_map_scan,
48
+ };
49
+
43
50
  /// A handler that bridges between Kafka messages and Ruby message processing
44
51
  /// code.
45
52
  ///
@@ -83,11 +90,78 @@ impl RubyHandler {
83
90
  propagator: Arc::new(new_propagator()),
84
91
  })
85
92
  }
93
+
94
+ async fn handle_record<C, P, M>(
95
+ &self,
96
+ context: C,
97
+ message: ConsumerMessage<P>,
98
+ method: &'static str,
99
+ event_type: &'static str,
100
+ span: Span,
101
+ ) -> Result<serde_json::Value, RubyHandlerError>
102
+ where
103
+ C: EventContext<Payload = serde_json::Value>,
104
+ M: From<ConsumerMessage<P>> + IntoValue + Send + 'static,
105
+ P: Send + Sync + 'static,
106
+ {
107
+ let cancel_future = context.clone().on_cancel();
108
+ let handler = self.handler.clone();
109
+ let task_id = format!(
110
+ "{}/{}:{}",
111
+ message.topic(),
112
+ message.partition(),
113
+ message.offset()
114
+ );
115
+ let event_context = HashMap::from([
116
+ ("event_type".into(), event_type.into()),
117
+ ("topic".into(), message.topic().to_string()),
118
+ ("partition".into(), message.partition().to_string()),
119
+ ("key".into(), message.key().to_string()),
120
+ ("offset".into(), message.offset().to_string()),
121
+ ]);
122
+ let context = Context::new(
123
+ context.boxed(),
124
+ self.bridge.clone(),
125
+ self.propagator.clone(),
126
+ );
127
+ let response_requested = message.response_requested();
128
+ let message = M::from(message);
129
+ let cloned_span = span.clone();
130
+
131
+ async move {
132
+ let task_handle = self
133
+ .scheduler
134
+ .schedule(task_id, &cloned_span, event_context, move |ruby| {
135
+ let result = handler.get(ruby).funcall(method, (context, message))?;
136
+ Ok(if response_requested {
137
+ result
138
+ } else {
139
+ ruby.qnil().as_value()
140
+ })
141
+ })
142
+ .await?;
143
+ let result_future = task_handle.result.receive();
144
+ pin_mut!(result_future);
145
+ let result = select! {
146
+ result = &mut result_future => {
147
+ result.inspect_err(|error| cloned_span.set_status(Status::error(error.to_string())))?
148
+ }
149
+ () = cancel_future => {
150
+ task_handle.cancellation_token.cancel(&self.bridge).await
151
+ .inspect_err(|error| cloned_span.set_status(Status::error(error.to_string())))?;
152
+ result_future.await?
153
+ }
154
+ };
155
+ Ok(result)
156
+ }
157
+ .instrument(span)
158
+ .await
159
+ }
86
160
  }
87
161
 
88
162
  impl FallibleHandler for RubyHandler {
89
163
  type Error = RubyHandlerError;
90
- type Output = ();
164
+ type Output = serde_json::Value;
91
165
  type Payload = serde_json::Value;
92
166
 
93
167
  /// Processes a Kafka message by dispatching it to the Ruby handler.
@@ -115,12 +189,10 @@ impl FallibleHandler for RubyHandler {
115
189
  context: C,
116
190
  message: ConsumerMessage<Self::Payload>,
117
191
  _demand_type: DemandType,
118
- ) -> Result<(), Self::Error>
192
+ ) -> Result<Self::Output, Self::Error>
119
193
  where
120
- C: EventContext,
194
+ C: EventContext<Payload = Self::Payload>,
121
195
  {
122
- // Create a new span for the on_message operation as a child of the message's
123
- // span
124
196
  let span = info_span!(
125
197
  parent: message.span(),
126
198
  "on_message",
@@ -129,75 +201,29 @@ impl FallibleHandler for RubyHandler {
129
201
  offset = message.offset(),
130
202
  key = %message.key()
131
203
  );
204
+ self.handle_record::<_, _, Message>(context, message, "on_message", "message", span)
205
+ .await
206
+ }
132
207
 
133
- // Get a future that completes when cancellation is signaled
134
- let cloned_context = context.clone();
135
- let cancel_future = cloned_context.on_cancel();
136
-
137
- // Clone the handler reference for use in the closure
138
- let handler = self.handler.clone();
139
-
140
- // Create a unique task ID for this message
141
- let task_id = format!(
142
- "{}/{}:{}",
143
- message.topic(),
144
- message.partition(),
145
- message.offset()
146
- );
147
-
148
- let event_context = HashMap::from([
149
- ("event_type".into(), "message".into()),
150
- ("topic".into(), message.topic().to_string()),
151
- ("partition".into(), message.partition().to_string()),
152
- ("key".into(), message.key().to_string()),
153
- ("offset".into(), message.offset().to_string()),
154
- ]);
155
-
156
- // Convert the Kafka message and context to Ruby-compatible types
157
- let context = Context::new(
158
- context.boxed(),
159
- self.bridge.clone(),
160
- self.propagator.clone(),
208
+ async fn on_excise<C>(
209
+ &self,
210
+ context: C,
211
+ message: ConsumerMessage<()>,
212
+ _demand_type: DemandType,
213
+ ) -> Result<Self::Output, Self::Error>
214
+ where
215
+ C: EventContext<Payload = Self::Payload>,
216
+ {
217
+ let span = info_span!(
218
+ parent: message.span(),
219
+ "on_excise",
220
+ topic = %message.topic(),
221
+ partition = message.partition(),
222
+ offset = message.offset(),
223
+ key = %message.key()
161
224
  );
162
- let message: Message = message.into();
163
-
164
- // Execute the entire message handling operation within the span
165
- let cloned_span = span.clone();
166
- async move {
167
- // Schedule the task to run in Ruby
168
- let task_handle = self
169
- .scheduler
170
- .schedule(task_id, &cloned_span, event_context, move |ruby| {
171
- let _: Value = handler
172
- .get(ruby)
173
- .funcall(id!(ruby, "on_message"), (context, message))?;
174
-
175
- Ok(())
176
- })
177
- .await?;
178
-
179
- // Get the future that will complete when the task is done
180
- let result_future = task_handle.result.receive();
181
- pin_mut!(result_future);
182
-
183
- // Wait for either task completion or shutdown signal
184
- select! {
185
- result = &mut result_future => {
186
- result.inspect_err(|e| cloned_span.set_status(Status::error(e.to_string())))?;
187
- }
188
- () = cancel_future => {
189
- // A cancel() failure is a genuine bridge error; mark the span.
190
- // The subsequent result_future error is expected cancellation, not a handler bug.
191
- task_handle.cancellation_token.cancel(&self.bridge).await
192
- .inspect_err(|e| cloned_span.set_status(Status::error(e.to_string())))?;
193
- result_future.await?;
194
- }
195
- }
196
-
197
- Ok(())
198
- }
199
- .instrument(span)
200
- .await
225
+ self.handle_record::<_, _, ExciseMessage>(context, message, "on_excise", "excise", span)
226
+ .await
201
227
  }
202
228
 
203
229
  async fn on_timer<C>(
@@ -205,13 +231,13 @@ impl FallibleHandler for RubyHandler {
205
231
  context: C,
206
232
  trigger: ProsodyTrigger,
207
233
  _demand_type: DemandType,
208
- ) -> Result<(), Self::Error>
234
+ ) -> Result<Self::Output, Self::Error>
209
235
  where
210
- C: EventContext,
236
+ C: EventContext<Payload = Self::Payload>,
211
237
  {
212
238
  // Only process application timers; internal timers are handled by middleware
213
239
  if trigger.timer_type != TimerType::Application {
214
- return Ok(());
240
+ return Ok(serde_json::Value::Null);
215
241
  }
216
242
 
217
243
  // Create a new span for the on_timer operation as a child of the trigger's span
@@ -256,8 +282,7 @@ impl FallibleHandler for RubyHandler {
256
282
  let _: Value = handler
257
283
  .get(ruby)
258
284
  .funcall(id!(ruby, "on_timer"), (context, timer))?;
259
-
260
- Ok(())
285
+ Ok(ruby.qnil().as_value())
261
286
  })
262
287
  .await?;
263
288
 
@@ -266,20 +291,20 @@ impl FallibleHandler for RubyHandler {
266
291
  pin_mut!(result_future);
267
292
 
268
293
  // Wait for either task completion or shutdown signal
269
- select! {
294
+ let result = select! {
270
295
  result = &mut result_future => {
271
- result.inspect_err(|e| cloned_span.set_status(Status::error(e.to_string())))?;
296
+ result.inspect_err(|e| cloned_span.set_status(Status::error(e.to_string())))?
272
297
  }
273
298
  () = cancel_future => {
274
299
  // A cancel() failure is a genuine bridge error; mark the span.
275
300
  // The subsequent result_future error is expected cancellation, not a handler bug.
276
301
  task_handle.cancellation_token.cancel(&self.bridge).await
277
302
  .inspect_err(|e| cloned_span.set_status(Status::error(e.to_string())))?;
278
- result_future.await?;
303
+ result_future.await?
279
304
  }
280
- }
305
+ };
281
306
 
282
- Ok(())
307
+ Ok(result)
283
308
  }
284
309
  .instrument(span)
285
310
  .await
@@ -294,6 +319,10 @@ impl FallibleHandler for RubyHandler {
294
319
  }
295
320
  }
296
321
 
322
+ impl ClientHandler for RubyHandler {
323
+ type Codecs = JsonCodecs;
324
+ }
325
+
297
326
  impl ClassifyError for RubyHandlerError {
298
327
  /// Categorizes errors for proper retry handling in the Kafka consumer.
299
328
  ///
@@ -340,6 +369,7 @@ pub enum RubyHandlerError {
340
369
  pub fn init(ruby: &Ruby) -> Result<(), Error> {
341
370
  context::init(ruby)?;
342
371
  message::init(ruby)?;
372
+ state::register(ruby)?;
343
373
  trigger::init(ruby)?;
344
374
 
345
375
  Ok(())