prosody 0.1.1
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 +7 -0
- data/.cargo/config.toml +2 -0
- data/.release-please-manifest.json +3 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.standard.yml +9 -0
- data/.taplo.toml +6 -0
- data/ARCHITECTURE.md +591 -0
- data/CHANGELOG.md +92 -0
- data/Cargo.lock +3513 -0
- data/Cargo.toml +77 -0
- data/LICENSE +21 -0
- data/Makefile +36 -0
- data/README.md +946 -0
- data/Rakefile +26 -0
- data/ext/prosody/Cargo.toml +38 -0
- data/ext/prosody/extconf.rb +6 -0
- data/ext/prosody/src/admin.rs +171 -0
- data/ext/prosody/src/bridge/callback.rs +60 -0
- data/ext/prosody/src/bridge/mod.rs +332 -0
- data/ext/prosody/src/client/config.rs +819 -0
- data/ext/prosody/src/client/mod.rs +379 -0
- data/ext/prosody/src/gvl.rs +149 -0
- data/ext/prosody/src/handler/context.rs +436 -0
- data/ext/prosody/src/handler/message.rs +144 -0
- data/ext/prosody/src/handler/mod.rs +338 -0
- data/ext/prosody/src/handler/trigger.rs +93 -0
- data/ext/prosody/src/lib.rs +82 -0
- data/ext/prosody/src/logging.rs +353 -0
- data/ext/prosody/src/scheduler/cancellation.rs +67 -0
- data/ext/prosody/src/scheduler/handle.rs +50 -0
- data/ext/prosody/src/scheduler/mod.rs +169 -0
- data/ext/prosody/src/scheduler/processor.rs +166 -0
- data/ext/prosody/src/scheduler/result.rs +197 -0
- data/ext/prosody/src/tracing_util.rs +56 -0
- data/ext/prosody/src/util.rs +219 -0
- data/lib/prosody/configuration.rb +333 -0
- data/lib/prosody/handler.rb +177 -0
- data/lib/prosody/native_stubs.rb +417 -0
- data/lib/prosody/processor.rb +321 -0
- data/lib/prosody/sentry.rb +36 -0
- data/lib/prosody/version.rb +10 -0
- data/lib/prosody.rb +42 -0
- data/release-please-config.json +10 -0
- data/sig/configuration.rbs +252 -0
- data/sig/handler.rbs +79 -0
- data/sig/processor.rbs +100 -0
- data/sig/prosody.rbs +171 -0
- data/sig/version.rbs +9 -0
- metadata +193 -0
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
//! Ruby wrapper for the Prosody message context.
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides a Ruby-compatible wrapper around the Prosody library's
|
|
4
|
+
//! `MessageContext`, allowing Ruby code to interact with message context
|
|
5
|
+
//! information from Kafka messages and schedule timer events.
|
|
6
|
+
|
|
7
|
+
use crate::bridge::Bridge;
|
|
8
|
+
use crate::tracing_util::extract_opentelemetry_context;
|
|
9
|
+
use crate::{ROOT_MOD, id};
|
|
10
|
+
use educe::Educe;
|
|
11
|
+
use magnus::value::ReprValue;
|
|
12
|
+
use magnus::{Error, Module, RClass, Ruby, Value, method};
|
|
13
|
+
use opentelemetry::propagation::TextMapCompositePropagator;
|
|
14
|
+
use prosody::consumer::event_context::BoxEventContext;
|
|
15
|
+
use prosody::timers::TimerType;
|
|
16
|
+
use prosody::timers::datetime::CompactDateTime;
|
|
17
|
+
use std::sync::Arc;
|
|
18
|
+
use tracing::{Span, debug, info_span};
|
|
19
|
+
use tracing_opentelemetry::OpenTelemetrySpanExt;
|
|
20
|
+
|
|
21
|
+
/// Nanosecond threshold for rounding Ruby Time objects to the nearest second.
|
|
22
|
+
/// At 0.5 seconds, times round up; below 0.5 seconds, they round down.
|
|
23
|
+
const NANOSECOND_ROUNDING_THRESHOLD: u32 = 500_000_000;
|
|
24
|
+
|
|
25
|
+
/// Ruby wrapper for a Kafka message context.
|
|
26
|
+
///
|
|
27
|
+
/// This struct wraps the native Prosody `MessageContext` and exposes it to Ruby
|
|
28
|
+
/// code as the `Prosody::Context` class. The context contains metadata and
|
|
29
|
+
/// control capabilities related to the processing of a Kafka message.
|
|
30
|
+
#[derive(Educe, Clone)]
|
|
31
|
+
#[educe(Debug)]
|
|
32
|
+
#[magnus::wrap(class = "Prosody::Context")]
|
|
33
|
+
pub struct Context {
|
|
34
|
+
/// The underlying Prosody message context.
|
|
35
|
+
///
|
|
36
|
+
/// This field is marked as hidden in debug output to prevent logging large
|
|
37
|
+
/// data.
|
|
38
|
+
#[allow(dead_code)]
|
|
39
|
+
#[educe(Debug(ignore))]
|
|
40
|
+
inner: BoxEventContext,
|
|
41
|
+
|
|
42
|
+
/// Bridge for handling async operations
|
|
43
|
+
#[educe(Debug(ignore))]
|
|
44
|
+
bridge: Bridge,
|
|
45
|
+
|
|
46
|
+
/// OpenTelemetry propagator for distributed tracing
|
|
47
|
+
#[educe(Debug(ignore))]
|
|
48
|
+
propagator: Arc<TextMapCompositePropagator>,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
impl Context {
|
|
52
|
+
/// Creates a new `Context` from a Prosody `EventContext` and Bridge.
|
|
53
|
+
///
|
|
54
|
+
/// # Arguments
|
|
55
|
+
///
|
|
56
|
+
/// * `inner` - The Prosody event context to wrap
|
|
57
|
+
/// * `bridge` - The bridge for handling async operations
|
|
58
|
+
/// * `propagator` - Shared OpenTelemetry propagator for distributed tracing
|
|
59
|
+
pub fn new(
|
|
60
|
+
inner: BoxEventContext,
|
|
61
|
+
bridge: Bridge,
|
|
62
|
+
propagator: Arc<TextMapCompositePropagator>,
|
|
63
|
+
) -> Self {
|
|
64
|
+
Self {
|
|
65
|
+
inner,
|
|
66
|
+
bridge,
|
|
67
|
+
propagator,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/// Check if cancellation has been requested.
|
|
72
|
+
///
|
|
73
|
+
/// Cancellation includes both message-level cancellation (e.g., timeout)
|
|
74
|
+
/// and partition shutdown.
|
|
75
|
+
///
|
|
76
|
+
/// # Returns
|
|
77
|
+
///
|
|
78
|
+
/// Boolean indicating whether cancellation has been requested.
|
|
79
|
+
fn should_cancel(&self) -> bool {
|
|
80
|
+
self.inner.should_cancel()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Wait for cancellation to be signaled.
|
|
84
|
+
///
|
|
85
|
+
/// Cancellation includes both message-level cancellation (e.g., timeout)
|
|
86
|
+
/// and partition shutdown. This method blocks until cancellation is
|
|
87
|
+
/// signaled.
|
|
88
|
+
///
|
|
89
|
+
/// # Arguments
|
|
90
|
+
///
|
|
91
|
+
/// * `ruby` - Reference to the Ruby VM
|
|
92
|
+
///
|
|
93
|
+
/// # Returns
|
|
94
|
+
///
|
|
95
|
+
/// Nothing on success.
|
|
96
|
+
fn on_cancel(ruby: &Ruby, this: &Self) -> Result<(), Error> {
|
|
97
|
+
let inner = this.inner.clone();
|
|
98
|
+
this.bridge.wait_for(
|
|
99
|
+
ruby,
|
|
100
|
+
async move { inner.on_cancel().await },
|
|
101
|
+
Span::current(),
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Schedule a timer to fire at the specified time.
|
|
106
|
+
///
|
|
107
|
+
/// # Arguments
|
|
108
|
+
///
|
|
109
|
+
/// * `ruby` - Reference to the Ruby VM
|
|
110
|
+
/// * `ruby_time` - Ruby Time object specifying when the timer should fire
|
|
111
|
+
///
|
|
112
|
+
/// # Returns
|
|
113
|
+
///
|
|
114
|
+
/// Nothing on success.
|
|
115
|
+
///
|
|
116
|
+
/// # Errors
|
|
117
|
+
///
|
|
118
|
+
/// Returns an error if the time is invalid or if scheduling fails.
|
|
119
|
+
fn schedule(ruby: &Ruby, this: &Self, ruby_time: Value) -> Result<(), Error> {
|
|
120
|
+
let compact_time = time_to_compact_datetime(ruby, ruby_time)?;
|
|
121
|
+
let inner = this.inner.clone();
|
|
122
|
+
|
|
123
|
+
// Extract OpenTelemetry context from Ruby for distributed tracing
|
|
124
|
+
let context = extract_opentelemetry_context(ruby, &this.propagator)?;
|
|
125
|
+
|
|
126
|
+
// Create span for tracing and set parent context
|
|
127
|
+
let span = info_span!("schedule", time = %compact_time);
|
|
128
|
+
if let Err(err) = span.set_parent(context) {
|
|
129
|
+
debug!("failed to set parent span: {err:#}");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.bridge
|
|
133
|
+
.wait_for(
|
|
134
|
+
ruby,
|
|
135
|
+
async move { inner.schedule(compact_time, TimerType::Application).await },
|
|
136
|
+
span,
|
|
137
|
+
)?
|
|
138
|
+
.map_err(|error| {
|
|
139
|
+
Error::new(
|
|
140
|
+
ruby.exception_runtime_error(),
|
|
141
|
+
format!("Failed to schedule timer: {error:#}"),
|
|
142
|
+
)
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Clear all scheduled timers and schedule a new one at the specified time.
|
|
147
|
+
///
|
|
148
|
+
/// # Arguments
|
|
149
|
+
///
|
|
150
|
+
/// * `ruby` - Reference to the Ruby VM
|
|
151
|
+
/// * `ruby_time` - Ruby Time object specifying when the new timer should
|
|
152
|
+
/// fire
|
|
153
|
+
///
|
|
154
|
+
/// # Returns
|
|
155
|
+
///
|
|
156
|
+
/// Nothing on success.
|
|
157
|
+
///
|
|
158
|
+
/// # Errors
|
|
159
|
+
///
|
|
160
|
+
/// Returns an error if the time is invalid or if scheduling fails.
|
|
161
|
+
fn clear_and_schedule(ruby: &Ruby, this: &Self, ruby_time: Value) -> Result<(), Error> {
|
|
162
|
+
let compact_time = time_to_compact_datetime(ruby, ruby_time)?;
|
|
163
|
+
let inner = this.inner.clone();
|
|
164
|
+
|
|
165
|
+
// Extract OpenTelemetry context from Ruby for distributed tracing
|
|
166
|
+
let context = extract_opentelemetry_context(ruby, &this.propagator)?;
|
|
167
|
+
|
|
168
|
+
// Create span for tracing and set parent context
|
|
169
|
+
let span = info_span!("clear_and_schedule", time = %compact_time);
|
|
170
|
+
if let Err(err) = span.set_parent(context) {
|
|
171
|
+
debug!("failed to set parent span: {err:#}");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
this.bridge
|
|
175
|
+
.wait_for(
|
|
176
|
+
ruby,
|
|
177
|
+
async move {
|
|
178
|
+
inner
|
|
179
|
+
.clear_and_schedule(compact_time, TimerType::Application)
|
|
180
|
+
.await
|
|
181
|
+
},
|
|
182
|
+
span,
|
|
183
|
+
)?
|
|
184
|
+
.map_err(|error| {
|
|
185
|
+
Error::new(
|
|
186
|
+
ruby.exception_runtime_error(),
|
|
187
|
+
format!("Failed to clear and schedule timer: {error:#}"),
|
|
188
|
+
)
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// Unschedule a timer at the specified time.
|
|
193
|
+
///
|
|
194
|
+
/// # Arguments
|
|
195
|
+
///
|
|
196
|
+
/// * `ruby` - Reference to the Ruby VM
|
|
197
|
+
/// * `ruby_time` - Ruby Time object specifying which timer to unschedule
|
|
198
|
+
///
|
|
199
|
+
/// # Returns
|
|
200
|
+
///
|
|
201
|
+
/// Nothing on success.
|
|
202
|
+
///
|
|
203
|
+
/// # Errors
|
|
204
|
+
///
|
|
205
|
+
/// Returns an error if the time is invalid or if unscheduling fails.
|
|
206
|
+
fn unschedule(ruby: &Ruby, this: &Self, ruby_time: Value) -> Result<(), Error> {
|
|
207
|
+
let compact_time = time_to_compact_datetime(ruby, ruby_time)?;
|
|
208
|
+
let inner = this.inner.clone();
|
|
209
|
+
|
|
210
|
+
// Extract OpenTelemetry context from Ruby for distributed tracing
|
|
211
|
+
let context = extract_opentelemetry_context(ruby, &this.propagator)?;
|
|
212
|
+
|
|
213
|
+
// Create span for tracing and set parent context
|
|
214
|
+
let span = info_span!("unschedule", time = %compact_time);
|
|
215
|
+
if let Err(err) = span.set_parent(context) {
|
|
216
|
+
debug!("failed to set parent span: {err:#}");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
this.bridge
|
|
220
|
+
.wait_for(
|
|
221
|
+
ruby,
|
|
222
|
+
async move { inner.unschedule(compact_time, TimerType::Application).await },
|
|
223
|
+
span,
|
|
224
|
+
)?
|
|
225
|
+
.map_err(|error| {
|
|
226
|
+
Error::new(
|
|
227
|
+
ruby.exception_runtime_error(),
|
|
228
|
+
format!("Failed to unschedule timer: {error:#}"),
|
|
229
|
+
)
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// Clear all scheduled timers.
|
|
234
|
+
///
|
|
235
|
+
/// # Arguments
|
|
236
|
+
///
|
|
237
|
+
/// * `ruby` - Reference to the Ruby VM
|
|
238
|
+
///
|
|
239
|
+
/// # Returns
|
|
240
|
+
///
|
|
241
|
+
/// Nothing on success.
|
|
242
|
+
///
|
|
243
|
+
/// # Errors
|
|
244
|
+
///
|
|
245
|
+
/// Returns an error if clearing fails.
|
|
246
|
+
fn clear_scheduled(ruby: &Ruby, this: &Self) -> Result<(), Error> {
|
|
247
|
+
let inner = this.inner.clone();
|
|
248
|
+
|
|
249
|
+
// Extract OpenTelemetry context from Ruby for distributed tracing
|
|
250
|
+
let context = extract_opentelemetry_context(ruby, &this.propagator)?;
|
|
251
|
+
|
|
252
|
+
// Create span for tracing and set parent context
|
|
253
|
+
let span = info_span!("clear_scheduled");
|
|
254
|
+
if let Err(err) = span.set_parent(context) {
|
|
255
|
+
debug!("failed to set parent span: {err:#}");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
this.bridge
|
|
259
|
+
.wait_for(
|
|
260
|
+
ruby,
|
|
261
|
+
async move { inner.clear_scheduled(TimerType::Application).await },
|
|
262
|
+
span,
|
|
263
|
+
)?
|
|
264
|
+
.map_err(|error| {
|
|
265
|
+
Error::new(
|
|
266
|
+
ruby.exception_runtime_error(),
|
|
267
|
+
format!("Failed to clear scheduled timers: {error:#}"),
|
|
268
|
+
)
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/// Get all scheduled timer times.
|
|
273
|
+
///
|
|
274
|
+
/// # Arguments
|
|
275
|
+
///
|
|
276
|
+
/// * `ruby` - Reference to the Ruby VM
|
|
277
|
+
///
|
|
278
|
+
/// # Returns
|
|
279
|
+
///
|
|
280
|
+
/// Array of Ruby Time objects representing all scheduled timer times.
|
|
281
|
+
///
|
|
282
|
+
/// # Errors
|
|
283
|
+
///
|
|
284
|
+
/// Returns an error if retrieving scheduled times fails or if
|
|
285
|
+
/// converting times to Ruby objects fails.
|
|
286
|
+
fn scheduled(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
287
|
+
let inner = this.inner.clone();
|
|
288
|
+
|
|
289
|
+
// Extract OpenTelemetry context from Ruby for distributed tracing
|
|
290
|
+
let context = extract_opentelemetry_context(ruby, &this.propagator)?;
|
|
291
|
+
|
|
292
|
+
// Create span for tracing and set parent context
|
|
293
|
+
let span = info_span!("scheduled");
|
|
294
|
+
if let Err(err) = span.set_parent(context) {
|
|
295
|
+
debug!("failed to set parent span: {err:#}");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let scheduled_times = this
|
|
299
|
+
.bridge
|
|
300
|
+
.wait_for(
|
|
301
|
+
ruby,
|
|
302
|
+
async move { inner.scheduled(TimerType::Application).await },
|
|
303
|
+
span,
|
|
304
|
+
)?
|
|
305
|
+
.map_err(|e| {
|
|
306
|
+
Error::new(
|
|
307
|
+
ruby.exception_runtime_error(),
|
|
308
|
+
format!("Failed to get scheduled times: {e}"),
|
|
309
|
+
)
|
|
310
|
+
})?;
|
|
311
|
+
|
|
312
|
+
// Convert CompactDateTime objects to Ruby Time objects using idiomatic iterator
|
|
313
|
+
// pattern
|
|
314
|
+
let ruby_array = ruby.ary_try_from_iter(
|
|
315
|
+
scheduled_times
|
|
316
|
+
.into_iter()
|
|
317
|
+
.map(|compact_time| compact_datetime_to_time(ruby, compact_time)),
|
|
318
|
+
)?;
|
|
319
|
+
|
|
320
|
+
Ok(ruby_array.as_value())
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/// Initializes the Context class in Ruby.
|
|
325
|
+
///
|
|
326
|
+
/// Registers the `Prosody::Context` class in the Ruby runtime, making it
|
|
327
|
+
/// available to Ruby code with all timer scheduling methods.
|
|
328
|
+
///
|
|
329
|
+
/// # Arguments
|
|
330
|
+
///
|
|
331
|
+
/// * `ruby` - Reference to the Ruby VM
|
|
332
|
+
///
|
|
333
|
+
/// # Errors
|
|
334
|
+
///
|
|
335
|
+
/// Returns a Magnus error if the class definition fails
|
|
336
|
+
pub fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
337
|
+
let module = ruby.get_inner(&ROOT_MOD);
|
|
338
|
+
let class = module.define_class(id!(ruby, "Context"), ruby.class_object())?;
|
|
339
|
+
|
|
340
|
+
// Cancellation methods
|
|
341
|
+
class.define_method(
|
|
342
|
+
id!(ruby, "should_cancel?"),
|
|
343
|
+
method!(Context::should_cancel, 0),
|
|
344
|
+
)?;
|
|
345
|
+
class.define_method(id!(ruby, "on_cancel"), method!(Context::on_cancel, 0))?;
|
|
346
|
+
|
|
347
|
+
// Timer scheduling methods
|
|
348
|
+
class.define_method(id!(ruby, "schedule"), method!(Context::schedule, 1))?;
|
|
349
|
+
class.define_method(
|
|
350
|
+
id!(ruby, "clear_and_schedule"),
|
|
351
|
+
method!(Context::clear_and_schedule, 1),
|
|
352
|
+
)?;
|
|
353
|
+
class.define_method(id!(ruby, "unschedule"), method!(Context::unschedule, 1))?;
|
|
354
|
+
class.define_method(
|
|
355
|
+
id!(ruby, "clear_scheduled"),
|
|
356
|
+
method!(Context::clear_scheduled, 0),
|
|
357
|
+
)?;
|
|
358
|
+
class.define_method(id!(ruby, "scheduled"), method!(Context::scheduled, 0))?;
|
|
359
|
+
|
|
360
|
+
Ok(())
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/// Converts a Ruby Time object to `CompactDateTime`.
|
|
364
|
+
///
|
|
365
|
+
/// `CompactDateTime` stores only epoch seconds (u32) with second-level
|
|
366
|
+
/// precision. This function extracts nanoseconds from Ruby Time to implement
|
|
367
|
+
/// proper rounding.
|
|
368
|
+
///
|
|
369
|
+
/// # Arguments
|
|
370
|
+
///
|
|
371
|
+
/// * `_ruby` - Reference to the Ruby VM (ensures Ruby thread safety)
|
|
372
|
+
/// * `ruby_time` - Ruby Time object to convert
|
|
373
|
+
///
|
|
374
|
+
/// # Returns
|
|
375
|
+
///
|
|
376
|
+
/// `CompactDateTime` representing the same time, rounded to nearest second.
|
|
377
|
+
///
|
|
378
|
+
/// # Errors
|
|
379
|
+
///
|
|
380
|
+
/// Returns an error if the time is outside the `CompactDateTime` range
|
|
381
|
+
/// (1970-2106) or if the Ruby Time object is invalid.
|
|
382
|
+
fn time_to_compact_datetime(ruby: &Ruby, ruby_time: Value) -> Result<CompactDateTime, Error> {
|
|
383
|
+
// Extract epoch seconds and nanoseconds from Ruby Time
|
|
384
|
+
let epoch_seconds: i64 = ruby_time.funcall(id!(ruby, "to_i"), ())?;
|
|
385
|
+
let nanos: u32 = ruby_time.funcall(id!(ruby, "nsec"), ())?;
|
|
386
|
+
|
|
387
|
+
// Validate CompactDateTime range (1970-2106)
|
|
388
|
+
let seconds_u32 = u32::try_from(epoch_seconds).map_err(|_| {
|
|
389
|
+
Error::new(
|
|
390
|
+
ruby.exception_arg_error(),
|
|
391
|
+
format!("Time {epoch_seconds} is outside CompactDateTime range (1970-2106)"),
|
|
392
|
+
)
|
|
393
|
+
})?;
|
|
394
|
+
|
|
395
|
+
// Apply CompactDateTime's rounding logic
|
|
396
|
+
let final_seconds = if nanos >= NANOSECOND_ROUNDING_THRESHOLD {
|
|
397
|
+
seconds_u32.checked_add(1).ok_or_else(|| {
|
|
398
|
+
Error::new(
|
|
399
|
+
ruby.exception_arg_error(),
|
|
400
|
+
"Time overflow during rounding to nearest second",
|
|
401
|
+
)
|
|
402
|
+
})?
|
|
403
|
+
} else {
|
|
404
|
+
seconds_u32
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
Ok(CompactDateTime::from(final_seconds))
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/// Converts a `CompactDateTime` to a Ruby Time object.
|
|
411
|
+
///
|
|
412
|
+
/// `CompactDateTime` only stores epoch seconds, so the resulting Ruby Time
|
|
413
|
+
/// will have zero nanoseconds. This is the most efficient conversion.
|
|
414
|
+
///
|
|
415
|
+
/// # Arguments
|
|
416
|
+
///
|
|
417
|
+
/// * `ruby` - Reference to the Ruby VM
|
|
418
|
+
/// * `compact_time` - `CompactDateTime` to convert
|
|
419
|
+
///
|
|
420
|
+
/// # Returns
|
|
421
|
+
///
|
|
422
|
+
/// Ruby Time object representing the same time with zero nanoseconds.
|
|
423
|
+
///
|
|
424
|
+
/// # Errors
|
|
425
|
+
///
|
|
426
|
+
/// Returns an error if the Ruby Time class cannot be accessed or if
|
|
427
|
+
/// creating the Time object fails.
|
|
428
|
+
fn compact_datetime_to_time(ruby: &Ruby, compact_time: CompactDateTime) -> Result<Value, Error> {
|
|
429
|
+
// Direct access to epoch seconds - CompactDateTime has no sub-second precision
|
|
430
|
+
let epoch_seconds = i64::from(compact_time.epoch_seconds());
|
|
431
|
+
|
|
432
|
+
// Create Ruby Time with zero nanoseconds (CompactDateTime precision limit)
|
|
433
|
+
ruby.module_kernel()
|
|
434
|
+
.const_get::<_, RClass>(id!(ruby, "Time"))?
|
|
435
|
+
.funcall(id!(ruby, "at"), (epoch_seconds,))
|
|
436
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
//! Defines the Ruby-facing wrapper for Kafka consumer messages.
|
|
2
|
+
//!
|
|
3
|
+
//! This module implements a Ruby-accessible wrapper around the Prosody consumer
|
|
4
|
+
//! message type, exposing message properties and content as Ruby objects.
|
|
5
|
+
|
|
6
|
+
use crate::{ROOT_MOD, id};
|
|
7
|
+
use educe::Educe;
|
|
8
|
+
use magnus::value::ReprValue;
|
|
9
|
+
use magnus::{Error, Module, RClass, Ruby, Value, method};
|
|
10
|
+
use prosody::consumer::Keyed;
|
|
11
|
+
use prosody::consumer::message::ConsumerMessage;
|
|
12
|
+
use serde_magnus::serialize;
|
|
13
|
+
|
|
14
|
+
/// Ruby-accessible wrapper for Kafka consumer messages.
|
|
15
|
+
///
|
|
16
|
+
/// Provides Ruby bindings to access Kafka message data including topic,
|
|
17
|
+
/// partition, offset, key, timestamp, and payload.
|
|
18
|
+
#[derive(Educe, Clone)]
|
|
19
|
+
#[educe(Debug)]
|
|
20
|
+
#[magnus::wrap(class = "Prosody::Message", frozen_shareable)]
|
|
21
|
+
pub struct Message {
|
|
22
|
+
/// The wrapped Prosody consumer message
|
|
23
|
+
#[educe(Debug(ignore))]
|
|
24
|
+
inner: ConsumerMessage,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
impl Message {
|
|
28
|
+
/// Returns the topic name this message was published to.
|
|
29
|
+
///
|
|
30
|
+
/// # Returns
|
|
31
|
+
///
|
|
32
|
+
/// A string slice containing the topic name.
|
|
33
|
+
fn topic(&self) -> &'static str {
|
|
34
|
+
self.inner.topic().as_ref()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Returns the Kafka partition number for this message.
|
|
38
|
+
///
|
|
39
|
+
/// # Returns
|
|
40
|
+
///
|
|
41
|
+
/// The partition number as an i32.
|
|
42
|
+
fn partition(&self) -> i32 {
|
|
43
|
+
self.inner.partition()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Returns the Kafka offset for this message within its partition.
|
|
47
|
+
///
|
|
48
|
+
/// # Returns
|
|
49
|
+
///
|
|
50
|
+
/// The message offset as an i64.
|
|
51
|
+
fn offset(&self) -> i64 {
|
|
52
|
+
self.inner.offset()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Returns the message key.
|
|
56
|
+
///
|
|
57
|
+
/// # Returns
|
|
58
|
+
///
|
|
59
|
+
/// A string slice containing the message key.
|
|
60
|
+
fn key(&self) -> &str {
|
|
61
|
+
self.inner.key()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Converts the message timestamp to a Ruby Time object.
|
|
65
|
+
///
|
|
66
|
+
/// # Arguments
|
|
67
|
+
///
|
|
68
|
+
/// * `ruby` - Reference to the Ruby VM
|
|
69
|
+
/// * `this` - Reference to the Message instance
|
|
70
|
+
///
|
|
71
|
+
/// # Returns
|
|
72
|
+
///
|
|
73
|
+
/// A Ruby Time object representing the message timestamp.
|
|
74
|
+
///
|
|
75
|
+
/// # Errors
|
|
76
|
+
///
|
|
77
|
+
/// Returns an error if the Ruby Time class cannot be accessed or if
|
|
78
|
+
/// creating the Time object fails.
|
|
79
|
+
fn timestamp(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
80
|
+
let epoch_micros = this.inner.timestamp().timestamp_micros();
|
|
81
|
+
ruby.module_kernel()
|
|
82
|
+
.const_get::<_, RClass>(id!(ruby, "Time"))?
|
|
83
|
+
.funcall(
|
|
84
|
+
id!(ruby, "at"),
|
|
85
|
+
(epoch_micros, ruby.to_symbol("microsecond")),
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/// Deserializes the message payload to a Ruby value.
|
|
90
|
+
///
|
|
91
|
+
/// # Arguments
|
|
92
|
+
///
|
|
93
|
+
/// * `_ruby` - Reference to the Ruby VM (unused but required by Magnus)
|
|
94
|
+
/// * `this` - Reference to the Message instance
|
|
95
|
+
///
|
|
96
|
+
/// # Returns
|
|
97
|
+
///
|
|
98
|
+
/// A Ruby value containing the deserialized message payload.
|
|
99
|
+
///
|
|
100
|
+
/// # Errors
|
|
101
|
+
///
|
|
102
|
+
/// Returns an error if payload deserialization fails.
|
|
103
|
+
fn payload(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
104
|
+
serialize(ruby, this.inner.payload())
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
impl From<ConsumerMessage> for Message {
|
|
109
|
+
/// Creates a new Message wrapper from a Prosody `ConsumerMessage`.
|
|
110
|
+
///
|
|
111
|
+
/// # Arguments
|
|
112
|
+
///
|
|
113
|
+
/// * `value` - The Prosody `ConsumerMessage` to wrap
|
|
114
|
+
fn from(value: ConsumerMessage) -> Self {
|
|
115
|
+
Self { inner: value }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// Initializes the `Prosody::Message` Ruby class and defines its methods.
|
|
120
|
+
///
|
|
121
|
+
/// # Arguments
|
|
122
|
+
///
|
|
123
|
+
/// * `ruby` - Reference to the Ruby VM
|
|
124
|
+
///
|
|
125
|
+
/// # Returns
|
|
126
|
+
///
|
|
127
|
+
/// OK on successful initialization.
|
|
128
|
+
///
|
|
129
|
+
/// # Errors
|
|
130
|
+
///
|
|
131
|
+
/// Returns an error if class or method definition fails.
|
|
132
|
+
pub fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
133
|
+
let module = ruby.get_inner(&ROOT_MOD);
|
|
134
|
+
let class = module.define_class(id!(ruby, "Message"), ruby.class_object())?;
|
|
135
|
+
|
|
136
|
+
class.define_method(id!(ruby, "topic"), method!(Message::topic, 0))?;
|
|
137
|
+
class.define_method(id!(ruby, "partition"), method!(Message::partition, 0))?;
|
|
138
|
+
class.define_method(id!(ruby, "offset"), method!(Message::offset, 0))?;
|
|
139
|
+
class.define_method(id!(ruby, "key"), method!(Message::key, 0))?;
|
|
140
|
+
class.define_method(id!(ruby, "timestamp"), method!(Message::timestamp, 0))?;
|
|
141
|
+
class.define_method(id!(ruby, "payload"), method!(Message::payload, 0))?;
|
|
142
|
+
|
|
143
|
+
Ok(())
|
|
144
|
+
}
|