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.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.cargo/config.toml +2 -0
  3. data/.release-please-manifest.json +3 -0
  4. data/.rspec +3 -0
  5. data/.ruby-version +1 -0
  6. data/.standard.yml +9 -0
  7. data/.taplo.toml +6 -0
  8. data/ARCHITECTURE.md +591 -0
  9. data/CHANGELOG.md +92 -0
  10. data/Cargo.lock +3513 -0
  11. data/Cargo.toml +77 -0
  12. data/LICENSE +21 -0
  13. data/Makefile +36 -0
  14. data/README.md +946 -0
  15. data/Rakefile +26 -0
  16. data/ext/prosody/Cargo.toml +38 -0
  17. data/ext/prosody/extconf.rb +6 -0
  18. data/ext/prosody/src/admin.rs +171 -0
  19. data/ext/prosody/src/bridge/callback.rs +60 -0
  20. data/ext/prosody/src/bridge/mod.rs +332 -0
  21. data/ext/prosody/src/client/config.rs +819 -0
  22. data/ext/prosody/src/client/mod.rs +379 -0
  23. data/ext/prosody/src/gvl.rs +149 -0
  24. data/ext/prosody/src/handler/context.rs +436 -0
  25. data/ext/prosody/src/handler/message.rs +144 -0
  26. data/ext/prosody/src/handler/mod.rs +338 -0
  27. data/ext/prosody/src/handler/trigger.rs +93 -0
  28. data/ext/prosody/src/lib.rs +82 -0
  29. data/ext/prosody/src/logging.rs +353 -0
  30. data/ext/prosody/src/scheduler/cancellation.rs +67 -0
  31. data/ext/prosody/src/scheduler/handle.rs +50 -0
  32. data/ext/prosody/src/scheduler/mod.rs +169 -0
  33. data/ext/prosody/src/scheduler/processor.rs +166 -0
  34. data/ext/prosody/src/scheduler/result.rs +197 -0
  35. data/ext/prosody/src/tracing_util.rs +56 -0
  36. data/ext/prosody/src/util.rs +219 -0
  37. data/lib/prosody/configuration.rb +333 -0
  38. data/lib/prosody/handler.rb +177 -0
  39. data/lib/prosody/native_stubs.rb +417 -0
  40. data/lib/prosody/processor.rb +321 -0
  41. data/lib/prosody/sentry.rb +36 -0
  42. data/lib/prosody/version.rb +10 -0
  43. data/lib/prosody.rb +42 -0
  44. data/release-please-config.json +10 -0
  45. data/sig/configuration.rbs +252 -0
  46. data/sig/handler.rbs +79 -0
  47. data/sig/processor.rbs +100 -0
  48. data/sig/prosody.rbs +171 -0
  49. data/sig/version.rbs +9 -0
  50. metadata +193 -0
@@ -0,0 +1,338 @@
1
+ //! # Message Handler Module
2
+ //!
3
+ //! Provides functionality to handle Kafka messages in Ruby by bridging between
4
+ //! the Rust-based Prosody Kafka client and Ruby application code.
5
+ //!
6
+ //! This module implements a handler for Kafka consumer messages that:
7
+ //! 1. Receives messages from Kafka via the Prosody consumer
8
+ //! 2. Converts them to Ruby objects
9
+ //! 3. Schedules execution in the Ruby runtime
10
+ //! 4. Properly handles error cases and shutdown scenarios
11
+
12
+ use crate::bridge::{Bridge, BridgeError};
13
+ use crate::handler::context::Context;
14
+ use crate::handler::message::Message;
15
+ use crate::handler::trigger::Timer;
16
+ use crate::id;
17
+ use crate::scheduler::result::ProcessingError;
18
+ use crate::scheduler::{Scheduler, SchedulerError};
19
+ use crate::util::ThreadSafeValue;
20
+ use futures::pin_mut;
21
+ use magnus::value::ReprValue;
22
+ use magnus::{Error, Ruby, Value};
23
+ use opentelemetry::propagation::TextMapCompositePropagator;
24
+ use prosody::consumer::event_context::EventContext;
25
+ use prosody::consumer::message::ConsumerMessage;
26
+ use prosody::consumer::middleware::FallibleHandler;
27
+ use prosody::consumer::{DemandType, Keyed};
28
+ use prosody::error::{ClassifyError, ErrorCategory};
29
+ use prosody::propagator::new_propagator;
30
+ use prosody::timers::{TimerType, Trigger as ProsodyTrigger};
31
+ use std::collections::HashMap;
32
+ use std::sync::Arc;
33
+ use thiserror::Error;
34
+ use tokio::select;
35
+ use tracing::{Instrument, info_span};
36
+
37
+ mod context;
38
+ mod message;
39
+ mod trigger;
40
+
41
+ /// A handler that bridges between Kafka messages and Ruby message processing
42
+ /// code.
43
+ ///
44
+ /// This struct manages the execution of Ruby message handling code when Kafka
45
+ /// messages are received. It uses a scheduler to run the Ruby handler code
46
+ /// in the Ruby runtime environment, ensuring proper thread safety and error
47
+ /// handling.
48
+ #[derive(Clone, Debug)]
49
+ pub struct RubyHandler {
50
+ /// Bridge for communicating between Rust and Ruby
51
+ bridge: Bridge,
52
+
53
+ /// Scheduler for running Ruby code in the Ruby runtime
54
+ scheduler: Arc<Scheduler>,
55
+
56
+ /// Thread-safe reference to the Ruby handler instance
57
+ handler: Arc<ThreadSafeValue>,
58
+
59
+ /// OpenTelemetry propagator shared across contexts
60
+ propagator: Arc<TextMapCompositePropagator>,
61
+ }
62
+
63
+ impl RubyHandler {
64
+ /// Creates a new handler that will dispatch Kafka messages to a Ruby
65
+ /// handler instance.
66
+ ///
67
+ /// # Arguments
68
+ ///
69
+ /// * `bridge` - Bridge for communicating between Rust and Ruby threads
70
+ /// * `ruby` - Reference to the Ruby VM
71
+ /// * `handler_instance` - Ruby object that will handle Kafka messages
72
+ ///
73
+ /// # Errors
74
+ ///
75
+ /// Returns a `Magnus::Error` if creating the scheduler fails
76
+ pub fn new(bridge: Bridge, ruby: &Ruby, handler_instance: Value) -> Result<Self, Error> {
77
+ Ok(Self {
78
+ bridge: bridge.clone(),
79
+ handler: Arc::new(ThreadSafeValue::new(handler_instance, bridge.clone())),
80
+ scheduler: Arc::new(Scheduler::new(ruby, bridge)?),
81
+ propagator: Arc::new(new_propagator()),
82
+ })
83
+ }
84
+ }
85
+
86
+ impl FallibleHandler for RubyHandler {
87
+ type Error = RubyHandlerError;
88
+
89
+ /// Processes a Kafka message by dispatching it to the Ruby handler.
90
+ ///
91
+ /// This method:
92
+ /// 1. Creates a unique task ID from the message metadata
93
+ /// 2. Schedules the handler execution in the Ruby runtime
94
+ /// 3. Waits for the result or responds to shutdown signals
95
+ /// 4. Handles cancellation if a shutdown is requested
96
+ ///
97
+ /// # Arguments
98
+ ///
99
+ /// * `context` - Kafka message context containing metadata and control
100
+ /// signals
101
+ /// * `message` - The Kafka message to be processed
102
+ ///
103
+ /// # Errors
104
+ ///
105
+ /// Returns a `RubyHandlerError` if any of the following fail:
106
+ /// - Scheduling the task
107
+ /// - Communication with the Ruby runtime
108
+ /// - The Ruby handler throws an exception
109
+ async fn on_message<C>(
110
+ &self,
111
+ context: C,
112
+ message: ConsumerMessage,
113
+ _demand_type: DemandType,
114
+ ) -> Result<(), Self::Error>
115
+ where
116
+ C: EventContext,
117
+ {
118
+ // Create a new span for the on_message operation as a child of the message's
119
+ // span
120
+ let span = info_span!(
121
+ parent: message.span(),
122
+ "on_message",
123
+ topic = %message.topic(),
124
+ partition = message.partition(),
125
+ offset = message.offset(),
126
+ key = %message.key()
127
+ );
128
+
129
+ // Get a future that completes when cancellation is signaled
130
+ let cloned_context = context.clone();
131
+ let cancel_future = cloned_context.on_cancel();
132
+
133
+ // Clone the handler reference for use in the closure
134
+ let handler = self.handler.clone();
135
+
136
+ // Create a unique task ID for this message
137
+ let task_id = format!(
138
+ "{}/{}:{}",
139
+ message.topic(),
140
+ message.partition(),
141
+ message.offset()
142
+ );
143
+
144
+ let event_context = HashMap::from([
145
+ ("event_type".into(), "message".into()),
146
+ ("topic".into(), message.topic().to_string()),
147
+ ("partition".into(), message.partition().to_string()),
148
+ ("key".into(), message.key().to_string()),
149
+ ("offset".into(), message.offset().to_string()),
150
+ ]);
151
+
152
+ // Convert the Kafka message and context to Ruby-compatible types
153
+ let context = Context::new(
154
+ context.boxed(),
155
+ self.bridge.clone(),
156
+ self.propagator.clone(),
157
+ );
158
+ let message: Message = message.into();
159
+
160
+ // Execute the entire message handling operation within the span
161
+ let cloned_span = span.clone();
162
+ async move {
163
+ // Schedule the task to run in Ruby
164
+ let task_handle = self
165
+ .scheduler
166
+ .schedule(task_id, &cloned_span, event_context, move |ruby| {
167
+ let _: Value = handler
168
+ .get(ruby)
169
+ .funcall(id!(ruby, "on_message"), (context, message))?;
170
+
171
+ Ok(())
172
+ })
173
+ .await?;
174
+
175
+ // Get the future that will complete when the task is done
176
+ let result_future = task_handle.result.receive();
177
+ pin_mut!(result_future);
178
+
179
+ // Wait for either task completion or shutdown signal
180
+ select! {
181
+ result = &mut result_future => {
182
+ result?;
183
+ }
184
+ () = cancel_future => {
185
+ // If cancellation requested, cancel the task and wait for it to complete
186
+ task_handle.cancellation_token.cancel(&self.bridge).await?;
187
+ result_future.await?;
188
+ }
189
+ }
190
+
191
+ Ok(())
192
+ }
193
+ .instrument(span)
194
+ .await
195
+ }
196
+
197
+ async fn on_timer<C>(
198
+ &self,
199
+ context: C,
200
+ trigger: ProsodyTrigger,
201
+ _demand_type: DemandType,
202
+ ) -> Result<(), Self::Error>
203
+ where
204
+ C: EventContext,
205
+ {
206
+ // Only process application timers; internal timers are handled by middleware
207
+ if trigger.timer_type != TimerType::Application {
208
+ return Ok(());
209
+ }
210
+
211
+ // Create a new span for the on_timer operation as a child of the trigger's span
212
+ let span = info_span!(
213
+ parent: trigger.span(),
214
+ "on_timer",
215
+ key = %trigger.key,
216
+ time = %trigger.time
217
+ );
218
+
219
+ // Get a future that completes when cancellation is signaled
220
+ let cloned_context = context.clone();
221
+ let cancel_future = cloned_context.on_cancel();
222
+
223
+ // Clone the handler reference for use in the closure
224
+ let handler = self.handler.clone();
225
+
226
+ // Create a unique task ID for this timer
227
+ let task_id = format!("timer/{}/{}", trigger.key, trigger.time);
228
+
229
+ let event_context = HashMap::from([
230
+ ("event_type".into(), "timer".into()),
231
+ ("key".into(), trigger.key.to_string()),
232
+ ("time".into(), trigger.time.to_string()),
233
+ ]);
234
+
235
+ // Convert the timer trigger and context to Ruby-compatible types
236
+ let context = Context::new(
237
+ context.boxed(),
238
+ self.bridge.clone(),
239
+ self.propagator.clone(),
240
+ );
241
+ let timer: Timer = trigger.into();
242
+
243
+ // Execute the entire timer handling operation within the span
244
+ let cloned_span = span.clone();
245
+ async move {
246
+ // Schedule the task to run in Ruby
247
+ let task_handle = self
248
+ .scheduler
249
+ .schedule(task_id, &cloned_span, event_context, move |ruby| {
250
+ let _: Value = handler
251
+ .get(ruby)
252
+ .funcall(id!(ruby, "on_timer"), (context, timer))?;
253
+
254
+ Ok(())
255
+ })
256
+ .await?;
257
+
258
+ // Get the future that will complete when the task is done
259
+ let result_future = task_handle.result.receive();
260
+ pin_mut!(result_future);
261
+
262
+ // Wait for either task completion or shutdown signal
263
+ select! {
264
+ result = &mut result_future => {
265
+ result?;
266
+ }
267
+ () = cancel_future => {
268
+ // If cancellation requested, cancel the task and wait for it to complete
269
+ task_handle.cancellation_token.cancel(&self.bridge).await?;
270
+ result_future.await?;
271
+ }
272
+ }
273
+
274
+ Ok(())
275
+ }
276
+ .instrument(span)
277
+ .await
278
+ }
279
+
280
+ /// Shuts down the handler.
281
+ ///
282
+ /// This is a no-op for the Ruby handler since resources are managed
283
+ /// by the Ruby runtime through garbage collection.
284
+ async fn shutdown(self) {
285
+ // No cleanup required - Ruby handles resource cleanup via GC
286
+ }
287
+ }
288
+
289
+ impl ClassifyError for RubyHandlerError {
290
+ /// Categorizes errors for proper retry handling in the Kafka consumer.
291
+ ///
292
+ /// Maps error types to their appropriate categories:
293
+ /// - Scheduler and Bridge errors are considered transient (retryable)
294
+ /// - Processing errors are categorized according to their own
295
+ /// classification
296
+ fn classify_error(&self) -> ErrorCategory {
297
+ match self {
298
+ RubyHandlerError::Scheduler(_) | RubyHandlerError::Bridge(_) => {
299
+ ErrorCategory::Transient
300
+ }
301
+ RubyHandlerError::Processing(error) => error.classify_error(),
302
+ }
303
+ }
304
+ }
305
+
306
+ /// Errors that can occur when handling Kafka messages in Ruby.
307
+ #[derive(Debug, Error)]
308
+ pub enum RubyHandlerError {
309
+ /// Error from the task scheduler
310
+ #[error(transparent)]
311
+ Scheduler(#[from] SchedulerError),
312
+
313
+ /// Error communicating with the Ruby runtime
314
+ #[error(transparent)]
315
+ Bridge(#[from] BridgeError),
316
+
317
+ /// Error from the Ruby handler code
318
+ #[error(transparent)]
319
+ Processing(#[from] ProcessingError),
320
+ }
321
+
322
+ /// Initializes the handler module by registering Ruby classes for message
323
+ /// context and content.
324
+ ///
325
+ /// # Arguments
326
+ ///
327
+ /// * `ruby` - Reference to the Ruby VM
328
+ ///
329
+ /// # Errors
330
+ ///
331
+ /// Returns a `Magnus::Error` if class initialization fails
332
+ pub fn init(ruby: &Ruby) -> Result<(), Error> {
333
+ context::init(ruby)?;
334
+ message::init(ruby)?;
335
+ trigger::init(ruby)?;
336
+
337
+ Ok(())
338
+ }
@@ -0,0 +1,93 @@
1
+ //! Defines the Ruby-facing wrapper for timer triggers.
2
+ //!
3
+ //! This module implements a Ruby-accessible wrapper around the Prosody timer
4
+ //! trigger type, exposing trigger properties 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::timers::Trigger;
11
+
12
+ /// Ruby-accessible wrapper for timer triggers.
13
+ ///
14
+ /// Provides Ruby bindings to access timer trigger data including key,
15
+ /// execution time, and tracing span information.
16
+ #[derive(Educe, Clone)]
17
+ #[educe(Debug)]
18
+ #[magnus::wrap(class = "Prosody::Timer", frozen_shareable)]
19
+ pub struct Timer {
20
+ /// The wrapped Prosody timer trigger
21
+ #[educe(Debug(ignore))]
22
+ inner: Trigger,
23
+ }
24
+
25
+ impl Timer {
26
+ /// Returns the entity key identifying what this timer belongs to.
27
+ ///
28
+ /// # Returns
29
+ ///
30
+ /// A string slice containing the entity key.
31
+ fn key(&self) -> &str {
32
+ self.inner.key.as_ref()
33
+ }
34
+
35
+ /// Converts the trigger execution time to a Ruby Time object.
36
+ ///
37
+ /// # Arguments
38
+ ///
39
+ /// * `ruby` - Reference to the Ruby VM
40
+ /// * `this` - Reference to the Trigger instance
41
+ ///
42
+ /// # Returns
43
+ ///
44
+ /// A Ruby Time object representing when this timer should execute.
45
+ ///
46
+ /// # Errors
47
+ ///
48
+ /// Returns an error if the Ruby Time class cannot be accessed or if
49
+ /// creating the Time object fails.
50
+ fn time(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
51
+ // Direct access to epoch seconds - CompactDateTime has no sub-second precision
52
+ let epoch_seconds = i64::from(this.inner.time.epoch_seconds());
53
+
54
+ // Create Ruby Time with zero nanoseconds (CompactDateTime precision limit)
55
+ ruby.module_kernel()
56
+ .const_get::<_, RClass>(id!(ruby, "Time"))?
57
+ .funcall(id!(ruby, "at"), (epoch_seconds,))
58
+ }
59
+ }
60
+
61
+ impl From<Trigger> for Timer {
62
+ /// Creates a new Timer wrapper from a Prosody `Trigger`.
63
+ ///
64
+ /// # Arguments
65
+ ///
66
+ /// * `value` - The Prosody `Trigger` to wrap
67
+ fn from(value: Trigger) -> Self {
68
+ Self { inner: value }
69
+ }
70
+ }
71
+
72
+ /// Initializes the `Prosody::Timer` Ruby class and defines its methods.
73
+ ///
74
+ /// # Arguments
75
+ ///
76
+ /// * `ruby` - Reference to the Ruby VM
77
+ ///
78
+ /// # Returns
79
+ ///
80
+ /// OK on successful initialization.
81
+ ///
82
+ /// # Errors
83
+ ///
84
+ /// Returns an error if class or method definition fails.
85
+ pub fn init(ruby: &Ruby) -> Result<(), Error> {
86
+ let module = ruby.get_inner(&ROOT_MOD);
87
+ let class = module.define_class(id!(ruby, "Timer"), ruby.class_object())?;
88
+
89
+ class.define_method(id!(ruby, "key"), method!(Timer::key, 0))?;
90
+ class.define_method(id!(ruby, "time"), method!(Timer::time, 0))?;
91
+
92
+ Ok(())
93
+ }
@@ -0,0 +1,82 @@
1
+ //! # Prosody Ruby Extension
2
+ //!
3
+ //! This crate provides Ruby bindings for the Prosody event processing library.
4
+ //! It bridges the Rust implementation of Prosody with Ruby, allowing Ruby
5
+ //! applications to use Prosody for event processing and messaging.
6
+ //!
7
+ //! The extension handles asynchronous communication between Rust and Ruby,
8
+ //! provides client functionality for interacting with message brokers,
9
+ //! manages message handling, and includes logging and scheduling capabilities.
10
+
11
+ // Temporarily removing allows to see what lints we have
12
+
13
+ #![allow(clippy::multiple_crate_versions, missing_docs)]
14
+
15
+ use crate::bridge::Bridge;
16
+ use magnus::value::Lazy;
17
+ use magnus::{Error, RModule, Ruby};
18
+ use std::sync::{LazyLock, OnceLock};
19
+ #[cfg(not(target_os = "windows"))]
20
+ use tikv_jemallocator::Jemalloc;
21
+ use tokio::runtime::Runtime;
22
+
23
+ mod admin;
24
+ mod bridge;
25
+ mod client;
26
+ mod gvl;
27
+ mod handler;
28
+ mod logging;
29
+ mod scheduler;
30
+ mod tracing_util;
31
+ mod util;
32
+
33
+ #[cfg(not(target_os = "windows"))]
34
+ #[global_allocator]
35
+ static GLOBAL: Jemalloc = Jemalloc;
36
+
37
+ /// Global instance of the Ruby-Rust communication bridge.
38
+ /// Initialized during extension startup and used throughout the library.
39
+ pub static BRIDGE: OnceLock<Bridge> = OnceLock::new();
40
+
41
+ /// Ensures tracing initialization occurs exactly once.
42
+ pub static TRACING_INIT: OnceLock<()> = OnceLock::new();
43
+
44
+ /// Global Tokio runtime for asynchronous operations.
45
+ ///
46
+ /// This runtime powers all async operations in the extension, including
47
+ /// message processing, scheduling, and communication with Ruby.
48
+ #[allow(clippy::expect_used)]
49
+ static RUNTIME: LazyLock<Runtime> =
50
+ LazyLock::new(|| Runtime::new().expect("Failed to create Tokio runtime"));
51
+
52
+ /// Reference to the root Ruby module for this extension.
53
+ ///
54
+ /// This is lazily initialized during extension startup and provides
55
+ /// access to the `Prosody` module in Ruby.
56
+ #[allow(clippy::expect_used)]
57
+ pub static ROOT_MOD: Lazy<RModule> = Lazy::new(|ruby| {
58
+ ruby.define_module("Prosody")
59
+ .expect("Failed to define Prosody module")
60
+ });
61
+
62
+ /// Initializes the Prosody Ruby extension.
63
+ ///
64
+ /// This function initializes the various components of the extension.
65
+ ///
66
+ /// # Arguments
67
+ ///
68
+ /// * `ruby` - Reference to the Ruby VM instance
69
+ ///
70
+ /// # Errors
71
+ ///
72
+ /// Returns a Magnus error if any initialization step fails, such as
73
+ /// defining Ruby classes or configuring components.
74
+ #[magnus::init]
75
+ fn init(ruby: &Ruby) -> Result<(), Error> {
76
+ admin::init(ruby)?;
77
+ bridge::init(ruby)?;
78
+ handler::init(ruby)?;
79
+ client::init(ruby)?;
80
+
81
+ Ok(())
82
+ }