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,353 @@
1
+ //! # Logging Bridge
2
+ //!
3
+ //! This module provides a bridge between Rust's tracing infrastructure and
4
+ //! Ruby's logging system. It captures tracing events from Rust code and
5
+ //! forwards them to a Ruby logger, ensuring logs from the native extension are
6
+ //! properly integrated into the Ruby application's logging.
7
+ //!
8
+ //! It uses bump allocation for efficient string formatting and concurrent
9
+ //! processing for high-throughput logging scenarios.
10
+
11
+ #![allow(clippy::print_stderr)]
12
+
13
+ use crate::ROOT_MOD;
14
+ use crate::bridge::Bridge;
15
+ use crate::id;
16
+ use crate::util::ThreadSafeValue;
17
+ use bumpalo::Bump;
18
+ use bumpalo::collections::string::String as BumpString;
19
+ use educe::Educe;
20
+ use futures::StreamExt;
21
+ use magnus::value::ReprValue;
22
+ use magnus::{Ruby, Value};
23
+ use std::cell::RefCell;
24
+ use std::error::Error;
25
+ use std::fmt;
26
+ use std::fmt::{Debug, Display, Write};
27
+ use std::sync::Arc;
28
+ use tokio::spawn;
29
+ use tokio::sync::mpsc::{UnboundedSender, unbounded_channel};
30
+ use tokio_stream::wrappers::UnboundedReceiverStream;
31
+ use tracing::field::{Field, Visit};
32
+ use tracing::{Event, Level, Metadata, Subscriber};
33
+ use tracing_subscriber::Layer;
34
+ use tracing_subscriber::layer::Context;
35
+
36
+ /// Maximum number of log requests to process concurrently.
37
+ ///
38
+ /// Setting this to `None` allows unlimited concurrency, while `Some(n)` limits
39
+ /// to n concurrent log requests.
40
+ const CONCURRENT_LOG_REQUESTS: Option<usize> = Some(16);
41
+
42
+ /// A tracing layer that forwards log events to Ruby's logging system.
43
+ ///
44
+ /// This struct captures tracing events from Rust and sends them to a Ruby
45
+ /// Logger instance, ensuring consistent logging across language boundaries.
46
+ #[derive(Clone, Educe)]
47
+ #[educe(Debug)]
48
+ pub struct Logger {
49
+ /// Channel sender for log events
50
+ #[educe(Debug(ignore))]
51
+ tx: UnboundedSender<(Level, String)>,
52
+ }
53
+
54
+ impl Logger {
55
+ /// Creates a new logger that forwards events to Ruby's logging system.
56
+ ///
57
+ /// This function:
58
+ /// 1. Creates a channel for passing log events
59
+ /// 2. Reads the user-configured logger from `Prosody.logger`
60
+ /// 3. Spawns a tokio task to handle log events asynchronously
61
+ ///
62
+ /// # Arguments
63
+ ///
64
+ /// * `ruby` - Reference to the Ruby VM
65
+ /// * `bridge` - Bridge for communication between Rust and Ruby
66
+ ///
67
+ /// # Returns
68
+ ///
69
+ /// A new `Logger` instance or an error if Ruby logger creation fails.
70
+ ///
71
+ /// # Errors
72
+ ///
73
+ /// Returns a `magnus::Error` if:
74
+ /// - The `Prosody` module cannot be accessed
75
+ /// - Calling `Prosody.logger` fails
76
+ pub fn new(ruby: &Ruby, bridge: Bridge) -> Result<Self, magnus::Error> {
77
+ let (tx, rx) = unbounded_channel::<(Level, String)>();
78
+
79
+ let module = ruby.get_inner(&ROOT_MOD);
80
+ let logger: Value = module.funcall(id!(ruby, "logger"), ())?;
81
+ let logger = Arc::new(ThreadSafeValue::new(logger, bridge.clone()));
82
+
83
+ spawn(async move {
84
+ UnboundedReceiverStream::new(rx)
85
+ .for_each_concurrent(CONCURRENT_LOG_REQUESTS, move |evt| {
86
+ log_event(bridge.clone(), logger.clone(), evt)
87
+ })
88
+ .await;
89
+ });
90
+
91
+ Ok(Self { tx })
92
+ }
93
+ }
94
+
95
+ /// Sends a log event to Ruby.
96
+ ///
97
+ /// This function maps Rust tracing levels to Ruby Logger methods and forwards
98
+ /// the message to the Ruby logger.
99
+ ///
100
+ /// # Arguments
101
+ ///
102
+ /// * `bridge` - Bridge for communication between Rust and Ruby
103
+ /// * `logger` - Thread-safe reference to a Ruby Logger instance
104
+ /// * `(level, msg)` - The log level and formatted message to send
105
+ async fn log_event(bridge: Bridge, logger: Arc<ThreadSafeValue>, (level, msg): (Level, String)) {
106
+ if let Err(error) = bridge
107
+ .run(move |ruby| {
108
+ let method = match level {
109
+ Level::ERROR => id!(ruby, "error"),
110
+ Level::WARN => id!(ruby, "warn"),
111
+ Level::INFO => id!(ruby, "info"),
112
+ Level::DEBUG | Level::TRACE => id!(ruby, "debug"),
113
+ };
114
+
115
+ if let Err(error) = logger.get(ruby).funcall::<_, _, Value>(method, (msg,)) {
116
+ eprintln!("failed to log to Ruby: {error:#}");
117
+ }
118
+ })
119
+ .await
120
+ {
121
+ eprintln!("failed to log to Ruby: {error:#}");
122
+ }
123
+ }
124
+
125
+ impl<S: Subscriber> Layer<S> for Logger {
126
+ /// Processes a tracing event and forwards it to Ruby.
127
+ ///
128
+ /// This method captures the event, formats it using a `Visitor`, and sends
129
+ /// it to the Ruby logger.
130
+ ///
131
+ /// # Arguments
132
+ ///
133
+ /// * `event` - The tracing event to process
134
+ /// * `_ctx` - The subscriber context (unused)
135
+ fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
136
+ let metadata = event.metadata();
137
+ if !metadata.is_event() {
138
+ return;
139
+ }
140
+
141
+ thread_local! {
142
+ static LOG_BUMP: RefCell<Bump> = RefCell::new(Bump::with_capacity(1024));
143
+ }
144
+
145
+ LOG_BUMP.with(|cell| {
146
+ let mut bump = cell.borrow_mut();
147
+ bump.reset();
148
+
149
+ let mut visitor = Visitor::new(&bump, metadata);
150
+ event.record(&mut visitor);
151
+
152
+ if let Err(error) = self.tx.send((*metadata.level(), visitor.to_string())) {
153
+ eprintln!("failed to send log message: {error:#}; message: {visitor}");
154
+ }
155
+ });
156
+ }
157
+ }
158
+
159
+ /// A visitor that accumulates recorded field values into a bump-allocated
160
+ /// string.
161
+ ///
162
+ /// This struct processes tracing event fields and formats them into a
163
+ /// human-readable log message using bump allocation for efficient memory
164
+ /// management.
165
+ pub struct Visitor<'a> {
166
+ /// Reference to the bump allocator
167
+ bump: &'a Bump,
168
+
169
+ /// Accumulated metadata as a string
170
+ metadata: BumpString<'a>,
171
+
172
+ /// Optional message field, if present
173
+ maybe_message: Option<BumpString<'a>>,
174
+ }
175
+
176
+ impl<'a> Visitor<'a> {
177
+ /// Creates a new visitor for processing event fields.
178
+ ///
179
+ /// Initializes the visitor with metadata from the event, including module
180
+ /// path and source line number.
181
+ ///
182
+ /// # Arguments
183
+ ///
184
+ /// * `bump` - Reference to a bump allocator
185
+ /// * `md` - Metadata from the tracing event
186
+ pub fn new(bump: &'a Bump, md: &'static Metadata<'static>) -> Self {
187
+ let mut visitor = Self {
188
+ bump,
189
+ metadata: BumpString::with_capacity_in(128, bump),
190
+ maybe_message: None,
191
+ };
192
+
193
+ if let Some(module) = md.module_path() {
194
+ let v = bump.alloc_str(module);
195
+ visitor.push_kv("module", v);
196
+ }
197
+
198
+ if let Some(line) = md.line() {
199
+ let v = bumpalo::format!(in bump, "{}", line);
200
+ visitor.push_kv("line", &v);
201
+ }
202
+
203
+ visitor
204
+ }
205
+
206
+ /// Adds a key-value pair to the metadata string.
207
+ ///
208
+ /// # Arguments
209
+ ///
210
+ /// * `key` - The field name
211
+ /// * `value` - The field value as a string
212
+ fn push_kv(&mut self, key: &str, value: &str) {
213
+ if self.metadata.is_empty() {
214
+ self.metadata.push_str(key);
215
+ self.metadata.push('=');
216
+ self.metadata.push_str(value);
217
+ } else {
218
+ self.metadata.push(' ');
219
+ self.metadata.push_str(key);
220
+ self.metadata.push('=');
221
+ self.metadata.push_str(value);
222
+ }
223
+ }
224
+ }
225
+
226
+ impl Visit for Visitor<'_> {
227
+ /// Records a f64 value.
228
+ ///
229
+ /// # Arguments
230
+ ///
231
+ /// * `f` - Field descriptor
232
+ /// * `v` - Field value
233
+ fn record_f64(&mut self, f: &Field, v: f64) {
234
+ let s = bumpalo::format!(in self.bump, "{}", v);
235
+ self.push_kv(f.name(), &s);
236
+ }
237
+
238
+ /// Records an i64 value.
239
+ ///
240
+ /// # Arguments
241
+ ///
242
+ /// * `f` - Field descriptor
243
+ /// * `v` - Field value
244
+ fn record_i64(&mut self, f: &Field, v: i64) {
245
+ let s = bumpalo::format!(in self.bump, "{}", v);
246
+ self.push_kv(f.name(), &s);
247
+ }
248
+
249
+ /// Records a u64 value.
250
+ ///
251
+ /// # Arguments
252
+ ///
253
+ /// * `f` - Field descriptor
254
+ /// * `v` - Field value
255
+ fn record_u64(&mut self, f: &Field, v: u64) {
256
+ let s = bumpalo::format!(in self.bump, "{}", v);
257
+ self.push_kv(f.name(), &s);
258
+ }
259
+
260
+ /// Records an i128 value.
261
+ ///
262
+ /// # Arguments
263
+ ///
264
+ /// * `f` - Field descriptor
265
+ /// * `v` - Field value
266
+ fn record_i128(&mut self, f: &Field, v: i128) {
267
+ let s = bumpalo::format!(in self.bump, "{}", v);
268
+ self.push_kv(f.name(), &s);
269
+ }
270
+
271
+ /// Records a u128 value.
272
+ ///
273
+ /// # Arguments
274
+ ///
275
+ /// * `f` - Field descriptor
276
+ /// * `v` - Field value
277
+ fn record_u128(&mut self, f: &Field, v: u128) {
278
+ let s = bumpalo::format!(in self.bump, "{}", v);
279
+ self.push_kv(f.name(), &s);
280
+ }
281
+
282
+ /// Records a boolean value.
283
+ ///
284
+ /// # Arguments
285
+ ///
286
+ /// * `f` - Field descriptor
287
+ /// * `v` - Field value
288
+ fn record_bool(&mut self, f: &Field, v: bool) {
289
+ let s = bumpalo::format!(in self.bump, "{}", v);
290
+ self.push_kv(f.name(), &s);
291
+ }
292
+
293
+ /// Records a string value.
294
+ ///
295
+ /// # Arguments
296
+ ///
297
+ /// * `f` - Field descriptor
298
+ /// * `v` - Field value
299
+ fn record_str(&mut self, f: &Field, v: &str) {
300
+ self.push_kv(f.name(), v);
301
+ }
302
+
303
+ /// Records an error value.
304
+ ///
305
+ /// # Arguments
306
+ ///
307
+ /// * `f` - Field descriptor
308
+ /// * `err` - Error value
309
+ fn record_error(&mut self, f: &Field, err: &(dyn Error + 'static)) {
310
+ let s = bumpalo::format!(in self.bump, "{:#}", err);
311
+ self.push_kv(f.name(), &s);
312
+ }
313
+
314
+ /// Records a debug-formatted value.
315
+ ///
316
+ /// Special handling is applied to the "message" field, which is stored
317
+ /// separately from other metadata.
318
+ ///
319
+ /// # Arguments
320
+ ///
321
+ /// * `f` - Field descriptor
322
+ /// * `dbg` - Value to format with Debug
323
+ fn record_debug(&mut self, f: &Field, dbg: &dyn Debug) {
324
+ let m = bumpalo::format!(in self.bump, "{:?}", dbg);
325
+ if f.name() == "message" {
326
+ self.maybe_message = Some(m);
327
+ } else {
328
+ self.push_kv(f.name(), &m);
329
+ }
330
+ }
331
+ }
332
+
333
+ impl Display for Visitor<'_> {
334
+ /// Formats the log message.
335
+ ///
336
+ /// If a "message" field was recorded, it's placed at the beginning of the
337
+ /// formatted string, followed by the metadata. Otherwise, only metadata is
338
+ /// included.
339
+ ///
340
+ /// # Arguments
341
+ ///
342
+ /// * `f` - Formatter to write to
343
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344
+ match &self.maybe_message {
345
+ None => f.write_str(&self.metadata),
346
+ Some(message) => {
347
+ f.write_str(message)?;
348
+ f.write_char(' ')?;
349
+ f.write_str(&self.metadata)
350
+ }
351
+ }
352
+ }
353
+ }
@@ -0,0 +1,67 @@
1
+ //! Provides functionality for canceling scheduled Ruby tasks from Rust.
2
+ //!
3
+ //! This module defines a token-based cancellation mechanism that allows
4
+ //! asynchronous Rust code to safely cancel operations running in the Ruby VM.
5
+
6
+ use crate::bridge::Bridge;
7
+ use crate::id;
8
+ use crate::scheduler::SchedulerError;
9
+ use crate::util::ThreadSafeValue;
10
+ use magnus::value::ReprValue;
11
+ use magnus::{Ruby, Value};
12
+
13
+ /// Token that can be used to cancel a scheduled Ruby task.
14
+ ///
15
+ /// This struct wraps a Ruby cancellation token object and provides a safe
16
+ /// interface for canceling the associated task across the Rust/Ruby boundary.
17
+ #[derive(Debug)]
18
+ pub struct CancellationToken {
19
+ /// The Ruby cancellation token, wrapped in a thread-safe container
20
+ token: ThreadSafeValue,
21
+ }
22
+
23
+ impl CancellationToken {
24
+ /// Creates a new cancellation token from a Ruby value.
25
+ ///
26
+ /// # Arguments
27
+ ///
28
+ /// * `token` - A Ruby value that responds to the `cancel` method
29
+ /// * `bridge` - The bridge used to defer cleanup of the wrapped token
30
+ /// value onto the Ruby thread when the token is dropped
31
+ pub fn new(token: Value, bridge: Bridge) -> Self {
32
+ Self {
33
+ token: ThreadSafeValue::new(token, bridge),
34
+ }
35
+ }
36
+
37
+ /// Cancels the associated Ruby task.
38
+ ///
39
+ /// This method consumes the token, preventing it from being used to cancel
40
+ /// the task more than once.
41
+ ///
42
+ /// # Arguments
43
+ ///
44
+ /// * `bridge` - The bridge used to execute Ruby code from Rust
45
+ ///
46
+ /// # Returns
47
+ ///
48
+ /// `Ok(())` if the task was successfully canceled.
49
+ ///
50
+ /// # Errors
51
+ ///
52
+ /// Returns a `SchedulerError::Cancel` if:
53
+ /// - The Ruby `cancel` method call fails
54
+ /// - The bridge fails to execute the Ruby code
55
+ pub async fn cancel(self, bridge: &Bridge) -> Result<(), SchedulerError> {
56
+ bridge
57
+ .run(move |ruby: &Ruby| {
58
+ self.token
59
+ .get(ruby)
60
+ .funcall::<_, _, Value>(id!(ruby, "cancel"), ())
61
+ .map_err(|error| SchedulerError::Cancel(error.to_string()))?;
62
+
63
+ Ok(())
64
+ })
65
+ .await?
66
+ }
67
+ }
@@ -0,0 +1,50 @@
1
+ //! # Task Handle
2
+ //!
3
+ //! This module provides the `TaskHandle` type, which represents a handle to an
4
+ //! asynchronous task scheduled for execution in the Ruby runtime.
5
+ //!
6
+ //! A `TaskHandle` encapsulates:
7
+ //! 1. A `ResultReceiver` for awaiting the completion of the task
8
+ //! 2. A `CancellationToken` for requesting cancellation of the task
9
+
10
+ use crate::scheduler::cancellation::CancellationToken;
11
+ use crate::scheduler::result::ResultReceiver;
12
+
13
+ /// A handle to an asynchronous task scheduled for execution in the Ruby
14
+ /// runtime.
15
+ ///
16
+ /// This struct combines two key components for managing asynchronous tasks:
17
+ /// - A `ResultReceiver` that allows waiting for the task's completion and
18
+ /// retrieving its result
19
+ /// - A `CancellationToken` that enables requesting cancellation of the
20
+ /// in-progress task
21
+ ///
22
+ /// The handle is typically returned from the `Scheduler::schedule` method and
23
+ /// should be retained as long as the task is active to maintain the ability
24
+ /// to await its result or request cancellation.
25
+ #[derive(Debug)]
26
+ pub struct TaskHandle {
27
+ /// Receiver for the task's result, used to await task completion
28
+ pub result: ResultReceiver,
29
+
30
+ /// Token used to request cancellation of the task
31
+ pub cancellation_token: CancellationToken,
32
+ }
33
+
34
+ impl TaskHandle {
35
+ /// Creates a new `TaskHandle` with the provided result receiver and
36
+ /// cancellation token.
37
+ ///
38
+ /// # Arguments
39
+ ///
40
+ /// * `result` - The receiver that will provide the task's result upon
41
+ /// completion
42
+ /// * `cancellation_token` - The token that can be used to request
43
+ /// cancellation of the task
44
+ pub fn new(result: ResultReceiver, cancellation_token: CancellationToken) -> TaskHandle {
45
+ Self {
46
+ result,
47
+ cancellation_token,
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,169 @@
1
+ //! # Scheduler
2
+ //!
3
+ //! The scheduler module provides an asynchronous task scheduling mechanism for
4
+ //! executing Ruby code from Rust. It handles the complexities of bridging
5
+ //! between the Rust async world and the Ruby synchronous environment, ensuring
6
+ //! proper propagation of tracing context.
7
+ //!
8
+ //! This module manages task submission, execution, cancellation, and result
9
+ //! handling, while preserving proper OpenTelemetry context across language
10
+ //! boundaries.
11
+
12
+ use crate::RUNTIME;
13
+ use crate::bridge::{Bridge, BridgeError};
14
+ use crate::scheduler::handle::TaskHandle;
15
+ use crate::scheduler::processor::RubyProcessor;
16
+ use crate::scheduler::result::result_channel;
17
+ use magnus::{Error, Ruby};
18
+ use opentelemetry::propagation::{TextMapCompositePropagator, TextMapPropagator};
19
+ use prosody::propagator::new_propagator;
20
+ use std::collections::HashMap;
21
+ use std::convert::identity;
22
+ use thiserror::Error;
23
+ use tracing::{Span, error, instrument};
24
+ use tracing_opentelemetry::OpenTelemetrySpanExt;
25
+
26
+ mod cancellation;
27
+ pub mod handle;
28
+ mod processor;
29
+ pub mod result;
30
+
31
+ /// Manages the scheduling of Rust functions for execution in Ruby, with proper
32
+ /// context propagation.
33
+ ///
34
+ /// The `Scheduler` is responsible for:
35
+ /// - Submitting tasks to a Ruby processor for execution
36
+ /// - Propagating OpenTelemetry context between Rust and Ruby
37
+ /// - Managing task lifecycle and result handling
38
+ /// - Ensuring proper shutdown when dropped
39
+ #[derive(Debug)]
40
+ pub struct Scheduler {
41
+ /// Communication bridge to the Ruby runtime
42
+ bridge: Bridge,
43
+
44
+ /// Ruby-side task processor
45
+ processor: RubyProcessor,
46
+
47
+ /// Propagator for OpenTelemetry context
48
+ propagator: TextMapCompositePropagator,
49
+ }
50
+
51
+ impl Scheduler {
52
+ /// Creates a new `Scheduler` instance.
53
+ ///
54
+ /// # Arguments
55
+ ///
56
+ /// * `ruby` - A reference to the Ruby VM
57
+ /// * `bridge` - A bridge for communication with the Ruby runtime
58
+ ///
59
+ /// # Errors
60
+ ///
61
+ /// Returns a `Magnus::Error` if the Ruby processor cannot be created.
62
+ pub fn new(ruby: &Ruby, bridge: Bridge) -> Result<Self, Error> {
63
+ Ok(Self {
64
+ bridge: bridge.clone(),
65
+ processor: RubyProcessor::new(ruby, bridge)?,
66
+ propagator: new_propagator(),
67
+ })
68
+ }
69
+
70
+ /// Schedules a function to be executed in the Ruby runtime.
71
+ ///
72
+ /// This method:
73
+ /// 1. Injects the current tracing context into a carrier
74
+ /// 2. Creates a channel for receiving the task result
75
+ /// 3. Submits the task to the Ruby processor
76
+ /// 4. Returns a handle for tracking the task
77
+ ///
78
+ /// # Arguments
79
+ ///
80
+ /// * `task_id` - A unique identifier for the task
81
+ /// * `span` - The tracing span to propagate to Ruby
82
+ /// * `function` - The function to execute in Ruby
83
+ ///
84
+ /// # Errors
85
+ ///
86
+ /// Returns a `SchedulerError` if:
87
+ /// - The bridge fails to run the submission function
88
+ /// - The processor fails to submit the task
89
+ ///
90
+ /// # Returns
91
+ ///
92
+ /// A `TaskHandle` for tracking the status and result of the scheduled task.
93
+ #[instrument(level = "debug", skip(self, span, event_context, function), err)]
94
+ pub async fn schedule<F>(
95
+ &self,
96
+ task_id: String,
97
+ span: &Span,
98
+ event_context: HashMap<String, String>,
99
+ function: F,
100
+ ) -> Result<TaskHandle, SchedulerError>
101
+ where
102
+ F: FnOnce(&Ruby) -> Result<(), Error> + Send + 'static,
103
+ {
104
+ let mut carrier: HashMap<String, String> = HashMap::with_capacity(2);
105
+ self.propagator
106
+ .inject_context(&span.context(), &mut carrier);
107
+
108
+ let (result_tx, result_rx) = result_channel();
109
+ let cloned_instance = self.processor.clone();
110
+
111
+ let token = self
112
+ .bridge
113
+ .run(move |ruby: &Ruby| {
114
+ cloned_instance
115
+ .submit(ruby, &task_id, carrier, event_context, result_tx, function)
116
+ .map_err(|error| SchedulerError::Submit(error.to_string()))
117
+ })
118
+ .await??;
119
+
120
+ Ok(TaskHandle::new(result_rx, token))
121
+ }
122
+ }
123
+
124
+ impl Drop for Scheduler {
125
+ /// Ensures the Ruby processor is properly stopped when the scheduler is
126
+ /// dropped.
127
+ ///
128
+ /// This spawns an async task to gracefully shut down the processor,
129
+ /// logging any errors that occur during shutdown.
130
+ fn drop(&mut self) {
131
+ let bridge = self.bridge.clone();
132
+ let processor = self.processor.clone();
133
+
134
+ RUNTIME.spawn(async move {
135
+ if let Err(error) = bridge
136
+ .run(move |ruby| {
137
+ processor
138
+ .stop(ruby)
139
+ .map_err(|error| SchedulerError::Shutdown(error.to_string()))
140
+ })
141
+ .await
142
+ .map_err(SchedulerError::from)
143
+ .and_then(identity)
144
+ {
145
+ error!("failed to shutdown processor: {error:#}");
146
+ }
147
+ });
148
+ }
149
+ }
150
+
151
+ /// Errors that can occur during scheduler operations.
152
+ #[derive(Debug, Error)]
153
+ pub enum SchedulerError {
154
+ /// Failed to submit a task to the Ruby processor.
155
+ #[error("failed to submit task: {0}")]
156
+ Submit(String),
157
+
158
+ /// An error occurred in the bridge while communicating with Ruby.
159
+ #[error(transparent)]
160
+ Bridge(#[from] BridgeError),
161
+
162
+ /// Failed to cancel a running task.
163
+ #[error("failed to cancel task: {0}")]
164
+ Cancel(String),
165
+
166
+ /// Failed to properly shut down the Ruby processor.
167
+ #[error("failed to shutdown processor: {0}")]
168
+ Shutdown(String),
169
+ }