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.
- checksums.yaml +4 -4
- data/.cargo/config.toml +3 -0
- data/.release-please-manifest.json +1 -1
- data/AGENTS.md +395 -0
- data/ARCHITECTURE.md +14 -4
- data/CHANGELOG.md +28 -0
- data/CLAUDE.md +1 -0
- data/CONFIGURATION.md +167 -0
- data/Cargo.lock +1115 -645
- data/Cargo.toml +7 -6
- data/README.md +436 -146
- data/Rakefile +11 -1
- data/examples/keyed_state.rb +70 -0
- data/examples/keyed_state.rbs +18 -0
- data/examples/keyed_state_windowing.rb +55 -0
- data/examples/keyed_state_windowing.rbs +16 -0
- data/ext/prosody/Cargo.toml +1 -0
- data/ext/prosody/src/admin.rs +1 -5
- data/ext/prosody/src/bridge/mod.rs +17 -32
- data/ext/prosody/src/client/config.rs +501 -28
- data/ext/prosody/src/client/mod.rs +167 -74
- data/ext/prosody/src/client/request.rs +132 -0
- data/ext/prosody/src/client/support.rs +122 -0
- data/ext/prosody/src/handler/context.rs +150 -5
- data/ext/prosody/src/handler/message.rs +67 -0
- data/ext/prosody/src/handler/mod.rs +115 -85
- data/ext/prosody/src/handler/state/mod.rs +488 -0
- data/ext/prosody/src/handler/state/registration.rs +104 -0
- data/ext/prosody/src/handler/state/scan.rs +218 -0
- data/ext/prosody/src/lib.rs +15 -3
- data/ext/prosody/src/published.rs +273 -0
- data/ext/prosody/src/scheduler/mod.rs +2 -2
- data/ext/prosody/src/scheduler/processor.rs +2 -2
- data/ext/prosody/src/scheduler/result.rs +7 -4
- data/ext/prosody/src/util.rs +86 -5
- data/lib/prosody/configuration.rb +71 -11
- data/lib/prosody/handler.rb +65 -8
- data/lib/prosody/native_stubs.rb +550 -9
- data/lib/prosody/request.rb +45 -0
- data/lib/prosody/state.rb +816 -0
- data/lib/prosody/version.rb +1 -1
- data/lib/prosody.rb +6 -0
- data/release-please-config.json +4 -0
- data/sig/configuration.rbs +70 -11
- data/sig/handler.rbs +17 -5
- data/sig/processor.rbs +28 -12
- data/sig/prosody.rbs +53 -7
- data/sig/request.rbs +66 -0
- data/sig/sentry.rbs +6 -0
- data/sig/state.rbs +390 -0
- data/steep_expectations.yml +57 -0
- data/typecheck/payload_types.rb +54 -0
- data/typecheck/payload_types.rbs +22 -0
- data/typecheck_negative/payload_types.rb +20 -0
- data/typecheck_negative/payload_types.rbs +9 -0
- 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(|
|
|
308
|
+
.map_err(|error| {
|
|
306
309
|
Error::new(
|
|
307
310
|
ruby.exception_runtime_error(),
|
|
308
|
-
format!("Failed to get scheduled times: {
|
|
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<
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
message.
|
|
144
|
-
|
|
145
|
-
message.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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<
|
|
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(())
|