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,166 @@
|
|
|
1
|
+
//! Provides functionality to schedule and execute Rust functions within the
|
|
2
|
+
//! Ruby runtime.
|
|
3
|
+
//!
|
|
4
|
+
//! This module contains the implementation of `RubyProcessor`, which interfaces
|
|
5
|
+
//! with a Ruby `AsyncTaskProcessor` class to manage task execution,
|
|
6
|
+
//! cancellation, and lifecycle management.
|
|
7
|
+
|
|
8
|
+
use crate::bridge::Bridge;
|
|
9
|
+
use crate::scheduler::cancellation::CancellationToken;
|
|
10
|
+
use crate::scheduler::result::ResultSender;
|
|
11
|
+
use crate::util::ThreadSafeValue;
|
|
12
|
+
use crate::{ROOT_MOD, id};
|
|
13
|
+
use educe::Educe;
|
|
14
|
+
use magnus::value::ReprValue;
|
|
15
|
+
use magnus::{Class, Error, Module, RClass, Ruby, Value};
|
|
16
|
+
use std::collections::HashMap;
|
|
17
|
+
use std::sync::Arc;
|
|
18
|
+
use std::sync::atomic::AtomicBool;
|
|
19
|
+
use std::sync::atomic::Ordering::Relaxed;
|
|
20
|
+
|
|
21
|
+
/// A thread-safe wrapper around a Ruby `AsyncTaskProcessor` instance.
|
|
22
|
+
///
|
|
23
|
+
/// This struct provides an interface for submitting Rust functions to be
|
|
24
|
+
/// executed in the Ruby runtime, managing task lifecycle, and handling task
|
|
25
|
+
/// cleanup upon shutdown.
|
|
26
|
+
#[derive(Clone, Educe)]
|
|
27
|
+
#[educe(Debug)]
|
|
28
|
+
pub struct RubyProcessor {
|
|
29
|
+
/// Thread-safe reference to the Ruby `AsyncTaskProcessor` instance
|
|
30
|
+
processor: Arc<ThreadSafeValue>,
|
|
31
|
+
|
|
32
|
+
/// Bridge for communicating between Rust and Ruby threads
|
|
33
|
+
bridge: Bridge,
|
|
34
|
+
|
|
35
|
+
/// Flag indicating whether the processor is in shutdown state
|
|
36
|
+
is_shutdown: Arc<AtomicBool>,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
impl RubyProcessor {
|
|
40
|
+
/// Creates a new `RubyProcessor` instance.
|
|
41
|
+
///
|
|
42
|
+
/// Instantiates a Ruby `AsyncTaskProcessor` class and starts it, making it
|
|
43
|
+
/// ready to accept tasks.
|
|
44
|
+
///
|
|
45
|
+
/// # Arguments
|
|
46
|
+
///
|
|
47
|
+
/// * `ruby` - Reference to the Ruby VM
|
|
48
|
+
/// * `bridge` - Bridge for communicating between Rust and Ruby threads
|
|
49
|
+
///
|
|
50
|
+
/// # Returns
|
|
51
|
+
///
|
|
52
|
+
/// A new `RubyProcessor` instance
|
|
53
|
+
///
|
|
54
|
+
/// # Errors
|
|
55
|
+
///
|
|
56
|
+
/// Returns a Magnus error if:
|
|
57
|
+
/// - The `AsyncTaskProcessor` Ruby class cannot be found
|
|
58
|
+
/// - Instantiation of the processor fails
|
|
59
|
+
/// - Starting the processor fails
|
|
60
|
+
pub fn new(ruby: &Ruby, bridge: Bridge) -> Result<Self, Error> {
|
|
61
|
+
let module = ruby.get_inner(&ROOT_MOD);
|
|
62
|
+
let logger: Value = module.funcall(id!(ruby, "logger"), ())?;
|
|
63
|
+
|
|
64
|
+
let class: RClass = module.const_get(id!(ruby, "AsyncTaskProcessor"))?;
|
|
65
|
+
let instance: Value = class.new_instance((logger,))?;
|
|
66
|
+
let _: Value = instance.funcall(id!(ruby, "start"), ())?;
|
|
67
|
+
Ok(Self {
|
|
68
|
+
processor: Arc::new(ThreadSafeValue::new(instance, bridge.clone())),
|
|
69
|
+
bridge,
|
|
70
|
+
is_shutdown: Arc::default(),
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Submits a function to be executed in the Ruby runtime.
|
|
75
|
+
///
|
|
76
|
+
/// This method packages a Rust function along with metadata and callback
|
|
77
|
+
/// mechanisms to be executed by the Ruby processor. It handles
|
|
78
|
+
/// propagation of tracing context and ensures proper result handling.
|
|
79
|
+
///
|
|
80
|
+
/// # Arguments
|
|
81
|
+
///
|
|
82
|
+
/// * `ruby` - Reference to the Ruby VM
|
|
83
|
+
/// * `task_id` - Unique identifier for the task
|
|
84
|
+
/// * `carrier` - OpenTelemetry context carrier for trace propagation
|
|
85
|
+
/// * `result_receiver` - Channel for reporting task execution results
|
|
86
|
+
/// * `function` - The Rust function to execute in Ruby's context
|
|
87
|
+
///
|
|
88
|
+
/// # Returns
|
|
89
|
+
///
|
|
90
|
+
/// A `CancellationToken` that can be used to cancel the task if needed
|
|
91
|
+
///
|
|
92
|
+
/// # Errors
|
|
93
|
+
///
|
|
94
|
+
/// Returns a Magnus error if:
|
|
95
|
+
/// - The processor is shutting down
|
|
96
|
+
/// - The function submission to Ruby fails
|
|
97
|
+
pub fn submit<F>(
|
|
98
|
+
&self,
|
|
99
|
+
ruby: &Ruby,
|
|
100
|
+
task_id: &str,
|
|
101
|
+
carrier: HashMap<String, String>,
|
|
102
|
+
event_context: HashMap<String, String>,
|
|
103
|
+
result_receiver: ResultSender,
|
|
104
|
+
function: F,
|
|
105
|
+
) -> Result<CancellationToken, Error>
|
|
106
|
+
where
|
|
107
|
+
F: FnOnce(&Ruby) -> Result<(), Error> + Send + 'static,
|
|
108
|
+
{
|
|
109
|
+
if self.is_shutdown.load(Relaxed) {
|
|
110
|
+
return Err(Error::new(
|
|
111
|
+
ruby.exception_runtime_error(),
|
|
112
|
+
String::from("Processor is shutting down"),
|
|
113
|
+
));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Convert the result sender into a Ruby callback
|
|
117
|
+
let callback = result_receiver.into_proc(ruby);
|
|
118
|
+
|
|
119
|
+
// Wrap the function in a Ruby block that can be executed later
|
|
120
|
+
let mut maybe_function = Some(function);
|
|
121
|
+
let block = ruby.proc_from_fn(move |ruby, _, _| {
|
|
122
|
+
if let Some(function) = maybe_function.take() {
|
|
123
|
+
function(ruby)
|
|
124
|
+
} else {
|
|
125
|
+
Ok(())
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Convert the tracing context carrier and event context to Ruby hashes
|
|
130
|
+
let carrier = ruby.hash_from_iter(carrier);
|
|
131
|
+
let event_context = ruby.hash_from_iter(event_context);
|
|
132
|
+
|
|
133
|
+
// Call the Ruby method: def submit(task_id, carrier, event_context, callback,
|
|
134
|
+
// &task_block)
|
|
135
|
+
let token = CancellationToken::new(
|
|
136
|
+
self.processor.get(ruby).funcall_with_block(
|
|
137
|
+
id!(ruby, "submit"),
|
|
138
|
+
(task_id, carrier, event_context, callback),
|
|
139
|
+
block,
|
|
140
|
+
)?,
|
|
141
|
+
self.bridge.clone(),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
Ok(token)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// Stops the processor, preventing it from accepting new tasks.
|
|
148
|
+
///
|
|
149
|
+
/// This method is idempotent - calling it multiple times will only
|
|
150
|
+
/// stop the processor once.
|
|
151
|
+
///
|
|
152
|
+
/// # Arguments
|
|
153
|
+
///
|
|
154
|
+
/// * `ruby` - Reference to the Ruby VM
|
|
155
|
+
///
|
|
156
|
+
/// # Errors
|
|
157
|
+
///
|
|
158
|
+
/// Returns a Magnus error if the stop operation fails in Ruby
|
|
159
|
+
pub fn stop(&self, ruby: &Ruby) -> Result<(), Error> {
|
|
160
|
+
if !self.is_shutdown.fetch_or(true, Relaxed) {
|
|
161
|
+
let _: Value = self.processor.get(ruby).funcall(id!(ruby, "stop"), ())?;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
Ok(())
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
//! # Result Communication
|
|
2
|
+
//!
|
|
3
|
+
//! Provides asynchronous result communication between Ruby and Rust.
|
|
4
|
+
//!
|
|
5
|
+
//! This module implements a channel-based system for Ruby task results to be
|
|
6
|
+
//! communicated back to Rust. It supports different error categories
|
|
7
|
+
//! (transient vs permanent) and ensures one-time result delivery through
|
|
8
|
+
//! atomic guarantees.
|
|
9
|
+
|
|
10
|
+
use crate::id;
|
|
11
|
+
use atomic_take::AtomicTake;
|
|
12
|
+
use educe::Educe;
|
|
13
|
+
use magnus::block::Proc;
|
|
14
|
+
use magnus::value::ReprValue;
|
|
15
|
+
use magnus::{Error, Ruby, TryConvert, Value, kwargs};
|
|
16
|
+
use prosody::error::{ClassifyError, ErrorCategory};
|
|
17
|
+
use thiserror::Error;
|
|
18
|
+
use tokio::sync::oneshot;
|
|
19
|
+
use tracing::debug;
|
|
20
|
+
|
|
21
|
+
/// Creates a paired sender and receiver for task result communication.
|
|
22
|
+
///
|
|
23
|
+
/// Creates a one-shot channel wrapped with `AtomicTake` to ensure
|
|
24
|
+
/// the result is only sent once, preventing duplicate delivery.
|
|
25
|
+
///
|
|
26
|
+
/// # Returns
|
|
27
|
+
///
|
|
28
|
+
/// A tuple containing a connected `ResultSender` and `ResultReceiver` pair.
|
|
29
|
+
pub fn result_channel() -> (ResultSender, ResultReceiver) {
|
|
30
|
+
let (result_tx, result_rx) = oneshot::channel();
|
|
31
|
+
let result_tx = AtomicTake::new(result_tx);
|
|
32
|
+
(ResultSender { result_tx }, ResultReceiver { result_rx })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Sends task results from Ruby back to Rust.
|
|
36
|
+
///
|
|
37
|
+
/// Wraps a one-shot sender in an `AtomicTake` to guarantee that the result
|
|
38
|
+
/// can only be sent once, preventing accidental duplicate sends.
|
|
39
|
+
#[derive(Educe)]
|
|
40
|
+
#[educe(Debug)]
|
|
41
|
+
pub struct ResultSender {
|
|
42
|
+
#[educe(Debug(ignore))]
|
|
43
|
+
result_tx: AtomicTake<oneshot::Sender<Result<(), ProcessingError>>>,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Receives task results in Rust from Ruby.
|
|
47
|
+
///
|
|
48
|
+
/// Wraps a one-shot receiver that allows asynchronously waiting
|
|
49
|
+
/// for a task to complete or fail.
|
|
50
|
+
#[derive(Educe)]
|
|
51
|
+
#[educe(Debug)]
|
|
52
|
+
pub struct ResultReceiver {
|
|
53
|
+
#[educe(Debug(ignore))]
|
|
54
|
+
result_rx: oneshot::Receiver<Result<(), ProcessingError>>,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
impl ResultSender {
|
|
58
|
+
/// Sends a result from Ruby to Rust.
|
|
59
|
+
///
|
|
60
|
+
/// Takes a Ruby result value, interprets it as a success or failure,
|
|
61
|
+
/// and sends it through the channel. For failures, it extracts meaningful
|
|
62
|
+
/// error messages from the Ruby exception and categorizes them as
|
|
63
|
+
/// permanent or transient.
|
|
64
|
+
///
|
|
65
|
+
/// # Arguments
|
|
66
|
+
///
|
|
67
|
+
/// * `ruby` - Reference to the Ruby VM
|
|
68
|
+
/// * `is_success` - Boolean indicating if the operation succeeded
|
|
69
|
+
/// * `result` - For successes, the return value; for failures, the Ruby
|
|
70
|
+
/// exception
|
|
71
|
+
///
|
|
72
|
+
/// # Returns
|
|
73
|
+
///
|
|
74
|
+
/// `true` if the result was sent successfully, `false` if the result had
|
|
75
|
+
/// already been sent or the receiver was dropped.
|
|
76
|
+
pub fn send(&self, ruby: &Ruby, is_success: bool, result: Value) -> bool {
|
|
77
|
+
let Some(result_tx) = self.result_tx.take() else {
|
|
78
|
+
debug!("result was already sent");
|
|
79
|
+
return false;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if is_success {
|
|
83
|
+
if result_tx.send(Ok(())).is_err() {
|
|
84
|
+
debug!("discarding result; receiver went away");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// For error results, determine if the error is permanent by calling
|
|
91
|
+
// the `permanent?` method on the Ruby exception
|
|
92
|
+
let is_permanent = result.funcall(id!(ruby, "permanent?"), ()).unwrap_or(false);
|
|
93
|
+
|
|
94
|
+
// Extract a detailed error message from the Ruby exception
|
|
95
|
+
let error_string: String = result
|
|
96
|
+
.funcall(id!(ruby, "full_message"), (kwargs!("highlight" => false),))
|
|
97
|
+
.or_else(|_| result.funcall(id!(ruby, "inspect"), ()))
|
|
98
|
+
.unwrap_or_else(|_| result.to_string());
|
|
99
|
+
|
|
100
|
+
let error = if is_permanent {
|
|
101
|
+
ProcessingError::Permanent(error_string)
|
|
102
|
+
} else {
|
|
103
|
+
ProcessingError::Transient(error_string)
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if result_tx.send(Err(error)).is_err() {
|
|
107
|
+
debug!("discarding result; receiver went away");
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
true
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Converts this sender into a Ruby Proc for callback-style integration.
|
|
115
|
+
///
|
|
116
|
+
/// Creates a Ruby Proc that, when called with a success status and result
|
|
117
|
+
/// value, will send that result through this sender back to Rust.
|
|
118
|
+
///
|
|
119
|
+
/// # Arguments
|
|
120
|
+
///
|
|
121
|
+
/// * `ruby` - Reference to the Ruby VM
|
|
122
|
+
///
|
|
123
|
+
/// # Returns
|
|
124
|
+
///
|
|
125
|
+
/// A Ruby `Proc` that can be passed to Ruby code as a callback
|
|
126
|
+
pub fn into_proc(self, ruby: &Ruby) -> Proc {
|
|
127
|
+
ruby.proc_from_fn(move |ruby, args, _block| match args {
|
|
128
|
+
[is_success, result, ..] => {
|
|
129
|
+
let was_sent = self.send(ruby, bool::try_convert(*is_success)?, *result);
|
|
130
|
+
Ok(was_sent)
|
|
131
|
+
}
|
|
132
|
+
_ => Err(Error::new(
|
|
133
|
+
ruby.exception_arg_error(),
|
|
134
|
+
format!("Expected two arguments but received {}", args.len()),
|
|
135
|
+
)),
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
impl ResultReceiver {
|
|
141
|
+
/// Asynchronously waits for a result from Ruby.
|
|
142
|
+
///
|
|
143
|
+
/// Awaits the task completion in Ruby and returns its result.
|
|
144
|
+
/// This consumes the receiver, ensuring the result can only be awaited
|
|
145
|
+
/// once.
|
|
146
|
+
///
|
|
147
|
+
/// # Returns
|
|
148
|
+
///
|
|
149
|
+
/// `Ok(())` if the task completed successfully
|
|
150
|
+
///
|
|
151
|
+
/// # Errors
|
|
152
|
+
///
|
|
153
|
+
/// Returns a `ProcessingError` if:
|
|
154
|
+
/// - The task failed (with either a permanent or transient error)
|
|
155
|
+
/// - The channel was closed unexpectedly (e.g., the Ruby VM terminated)
|
|
156
|
+
pub async fn receive(self) -> Result<(), ProcessingError> {
|
|
157
|
+
self.result_rx.await.map_err(|_| ProcessingError::Closed)?
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// Errors that can occur during task processing.
|
|
162
|
+
///
|
|
163
|
+
/// Represents error scenarios when processing tasks between Ruby and Rust,
|
|
164
|
+
/// with categorization that informs handling strategies (e.g., retry policies).
|
|
165
|
+
#[derive(Clone, Debug, Error)]
|
|
166
|
+
pub enum ProcessingError {
|
|
167
|
+
/// A temporary error that may succeed on retry.
|
|
168
|
+
#[error("transient error: {0}")]
|
|
169
|
+
Transient(String),
|
|
170
|
+
|
|
171
|
+
/// A permanent error that will not succeed on retry.
|
|
172
|
+
#[error("permanent error: {0}")]
|
|
173
|
+
Permanent(String),
|
|
174
|
+
|
|
175
|
+
/// The result channel was closed before receiving a result.
|
|
176
|
+
///
|
|
177
|
+
/// This typically happens when the Ruby VM terminates while a task is in
|
|
178
|
+
/// progress.
|
|
179
|
+
#[error("result channel has been closed")]
|
|
180
|
+
Closed,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
impl ClassifyError for ProcessingError {
|
|
184
|
+
/// Classifies errors to determine retry behavior.
|
|
185
|
+
///
|
|
186
|
+
/// Maps error types to their appropriate retry categories:
|
|
187
|
+
/// - Transient errors are retryable
|
|
188
|
+
/// - Permanent errors should not be retried
|
|
189
|
+
/// - Channel closure is treated as a transient error, allowing for retries
|
|
190
|
+
/// when communication is restored
|
|
191
|
+
fn classify_error(&self) -> ErrorCategory {
|
|
192
|
+
match self {
|
|
193
|
+
ProcessingError::Transient(_) | ProcessingError::Closed => ErrorCategory::Transient,
|
|
194
|
+
ProcessingError::Permanent(_) => ErrorCategory::Permanent,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//! OpenTelemetry tracing utilities for Ruby-Rust integration.
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides shared utilities for extracting and propagating
|
|
4
|
+
//! OpenTelemetry context across the Ruby-Rust boundary for distributed tracing.
|
|
5
|
+
use crate::id;
|
|
6
|
+
use magnus::value::ReprValue;
|
|
7
|
+
use magnus::{Error, Module, RModule, Ruby, Value};
|
|
8
|
+
use opentelemetry::Context;
|
|
9
|
+
use opentelemetry::propagation::{TextMapCompositePropagator, TextMapPropagator};
|
|
10
|
+
use std::collections::HashMap;
|
|
11
|
+
|
|
12
|
+
/// Extracts OpenTelemetry context from Ruby's current tracing environment.
|
|
13
|
+
///
|
|
14
|
+
/// This function extracts the current OpenTelemetry context from Ruby and
|
|
15
|
+
/// converts it to a Rust context that can be used for distributed tracing.
|
|
16
|
+
/// The context is extracted using Ruby's OpenTelemetry propagation mechanism
|
|
17
|
+
/// and then parsed using the provided propagator.
|
|
18
|
+
///
|
|
19
|
+
/// # Arguments
|
|
20
|
+
///
|
|
21
|
+
/// * `ruby` - Reference to the Ruby VM
|
|
22
|
+
/// * `propagator` - The OpenTelemetry propagator to use for context extraction
|
|
23
|
+
///
|
|
24
|
+
/// # Returns
|
|
25
|
+
///
|
|
26
|
+
/// The extracted OpenTelemetry context that can be used as a parent for new
|
|
27
|
+
/// spans.
|
|
28
|
+
///
|
|
29
|
+
/// # Errors
|
|
30
|
+
///
|
|
31
|
+
/// Returns an error if:
|
|
32
|
+
/// - OpenTelemetry module is not available in Ruby
|
|
33
|
+
/// - Context extraction or propagation fails
|
|
34
|
+
/// - Ruby-to-Rust type conversion fails
|
|
35
|
+
///
|
|
36
|
+
/// # Example
|
|
37
|
+
///
|
|
38
|
+
/// ```rust
|
|
39
|
+
/// let context = extract_opentelemetry_context(ruby, &propagator)?;
|
|
40
|
+
/// let span = info_span!("operation");
|
|
41
|
+
/// span.set_parent(context);
|
|
42
|
+
/// ```
|
|
43
|
+
pub fn extract_opentelemetry_context(
|
|
44
|
+
ruby: &Ruby,
|
|
45
|
+
propagator: &TextMapCompositePropagator,
|
|
46
|
+
) -> Result<Context, Error> {
|
|
47
|
+
let carrier = ruby.hash_new();
|
|
48
|
+
let otel_class: RModule = ruby.class_module().const_get(id!(ruby, "OpenTelemetry"))?;
|
|
49
|
+
let propagator_obj: Value = otel_class.funcall(id!(ruby, "propagation"), ())?;
|
|
50
|
+
let _: Value = propagator_obj.funcall(id!(ruby, "inject"), (carrier,))?;
|
|
51
|
+
|
|
52
|
+
let carrier: HashMap<String, String> = carrier.to_hash_map()?;
|
|
53
|
+
let context = propagator.extract(&carrier);
|
|
54
|
+
|
|
55
|
+
Ok(context)
|
|
56
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
//! # Utilities for Ruby/Rust interoperability
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides utilities for safe and efficient interaction between
|
|
4
|
+
//! Rust and Ruby, particularly focusing on thread-safety and efficient symbol
|
|
5
|
+
//! handling.
|
|
6
|
+
|
|
7
|
+
use crate::bridge::Bridge;
|
|
8
|
+
use crate::logging::Logger;
|
|
9
|
+
use crate::{BRIDGE, RUNTIME, TRACING_INIT};
|
|
10
|
+
use magnus::value::BoxValue;
|
|
11
|
+
use magnus::{Ruby, Value};
|
|
12
|
+
use prosody::tracing::initialize_tracing;
|
|
13
|
+
use std::mem::{ManuallyDrop, forget};
|
|
14
|
+
use tokio::runtime::{EnterGuard, Handle};
|
|
15
|
+
use tracing::warn;
|
|
16
|
+
|
|
17
|
+
/// Creates a static Ruby identifier (symbol) for efficient reuse.
|
|
18
|
+
///
|
|
19
|
+
/// This macro creates a lazily-initialized static Ruby identifier from a string
|
|
20
|
+
/// literal. Using this macro for frequently accessed Ruby method names or
|
|
21
|
+
/// symbols avoids repeatedly converting strings to Ruby symbols at runtime.
|
|
22
|
+
///
|
|
23
|
+
/// This macro requires a `ruby: &Ruby` parameter to enforce that it can only
|
|
24
|
+
/// be used within a Ruby thread context, ensuring thread safety.
|
|
25
|
+
///
|
|
26
|
+
/// # Examples
|
|
27
|
+
///
|
|
28
|
+
/// ```
|
|
29
|
+
/// let method_name = id!(ruby, "to_s");
|
|
30
|
+
/// // Use method_name with Ruby function calls
|
|
31
|
+
/// ```
|
|
32
|
+
#[macro_export]
|
|
33
|
+
macro_rules! id {
|
|
34
|
+
($ruby:expr, $str:expr) => {{
|
|
35
|
+
static VAL: magnus::value::LazyId = magnus::value::LazyId::new($str);
|
|
36
|
+
let _ruby: &magnus::Ruby = $ruby; // Enforce that ruby is &Ruby type
|
|
37
|
+
*VAL
|
|
38
|
+
}};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// A thread-safe wrapper around a Ruby value.
|
|
42
|
+
///
|
|
43
|
+
/// Provides a way to safely share Ruby values between threads by wrapping
|
|
44
|
+
/// them in a type that implements both `Send` and `Sync`. This type enforces
|
|
45
|
+
/// that the underlying Ruby value is only accessed within a Ruby thread
|
|
46
|
+
/// context.
|
|
47
|
+
///
|
|
48
|
+
/// # Drop safety
|
|
49
|
+
///
|
|
50
|
+
/// [`BoxValue`] must be dropped on a Ruby thread because its `Drop` impl calls
|
|
51
|
+
/// `rb_gc_unregister_address`. This type handles that automatically: if dropped
|
|
52
|
+
/// on a Ruby thread, the value is cleaned up normally. If dropped on a
|
|
53
|
+
/// non-Ruby thread, the value is sent through the bridge for deferred cleanup
|
|
54
|
+
/// on the Ruby thread. If the bridge is unavailable (shutdown), the value is
|
|
55
|
+
/// leaked with a warning rather than risking a segfault.
|
|
56
|
+
#[derive(Debug)]
|
|
57
|
+
pub struct ThreadSafeValue {
|
|
58
|
+
value: ManuallyDrop<RubyDrop>,
|
|
59
|
+
bridge: Bridge,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// SAFETY: The underlying value can only be accessed from a Ruby thread
|
|
63
|
+
// (enforced by requiring `&Ruby` in `get`). Drop is safe across threads: it
|
|
64
|
+
// sends cleanup through the bridge when not on a Ruby thread, falling back
|
|
65
|
+
// to a leak if the bridge is unavailable.
|
|
66
|
+
unsafe impl Send for ThreadSafeValue {}
|
|
67
|
+
|
|
68
|
+
// SAFETY: `get` requires `&Ruby`, ensuring the value is only read on a Ruby
|
|
69
|
+
// thread. The inner `BoxValue` is never mutated after construction.
|
|
70
|
+
unsafe impl Sync for ThreadSafeValue {}
|
|
71
|
+
|
|
72
|
+
impl ThreadSafeValue {
|
|
73
|
+
/// Creates a new thread-safe wrapper around a Ruby value.
|
|
74
|
+
///
|
|
75
|
+
/// # Arguments
|
|
76
|
+
///
|
|
77
|
+
/// * `value` - The Ruby value to wrap
|
|
78
|
+
/// * `bridge` - The bridge used to defer cleanup onto the Ruby thread
|
|
79
|
+
pub fn new(value: Value, bridge: Bridge) -> Self {
|
|
80
|
+
Self {
|
|
81
|
+
value: ManuallyDrop::new(RubyDrop::new(value)),
|
|
82
|
+
bridge,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Gets a reference to the wrapped Ruby value.
|
|
87
|
+
///
|
|
88
|
+
/// This method ensures that access to the Ruby value only happens
|
|
89
|
+
/// within a Ruby thread context by requiring a `Ruby` reference.
|
|
90
|
+
///
|
|
91
|
+
/// # Arguments
|
|
92
|
+
///
|
|
93
|
+
/// * `ruby` - A reference to the Ruby VM
|
|
94
|
+
pub fn get(&self, ruby: &Ruby) -> &Value {
|
|
95
|
+
self.value.get(ruby)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
impl Drop for ThreadSafeValue {
|
|
100
|
+
fn drop(&mut self) {
|
|
101
|
+
// SAFETY: `drop` is called exactly once per value, so this
|
|
102
|
+
// `ManuallyDrop::take` cannot double-free.
|
|
103
|
+
let inner = unsafe { ManuallyDrop::take(&mut self.value) };
|
|
104
|
+
|
|
105
|
+
// On a Ruby thread: safe to drop directly.
|
|
106
|
+
if RubyDrop::can_drop() {
|
|
107
|
+
drop(inner);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// On a non-Ruby thread: send to the bridge for cleanup on the Ruby
|
|
112
|
+
// thread. When the closure runs, `inner` is dropped on the Ruby thread
|
|
113
|
+
// and `RubyDrop::Drop` takes the normal cleanup path.
|
|
114
|
+
//
|
|
115
|
+
// If the send fails (bridge shut down), the `SendError` owns the
|
|
116
|
+
// closure which owns `inner`. When `SendError` drops here on the
|
|
117
|
+
// non-Ruby thread, `RubyDrop::Drop` takes the leak path and emits
|
|
118
|
+
// its own warning — no need to warn here too.
|
|
119
|
+
let _ = self.bridge.send(Box::new(move |_ruby| drop(inner)));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Wraps a [`BoxValue`] and ensures it is dropped on a Ruby thread.
|
|
124
|
+
///
|
|
125
|
+
/// `BoxValue::drop` calls `rb_gc_unregister_address`, which must only run on
|
|
126
|
+
/// a Ruby thread. `RubyDrop` handles this safely: on a Ruby thread it drops
|
|
127
|
+
/// normally; on any other thread it leaks with a warning rather than
|
|
128
|
+
/// segfaulting. It has no knowledge of channels or bridges, so there is no
|
|
129
|
+
/// risk of infinite recursion in `Drop`.
|
|
130
|
+
#[derive(Debug)]
|
|
131
|
+
struct RubyDrop(ManuallyDrop<BoxValue<Value>>);
|
|
132
|
+
|
|
133
|
+
// SAFETY: Either dropped on a Ruby thread (normal cleanup) or leaked on a
|
|
134
|
+
// non-Ruby thread (forget + warn). Never calls into Ruby from the wrong thread.
|
|
135
|
+
unsafe impl Send for RubyDrop {}
|
|
136
|
+
|
|
137
|
+
impl RubyDrop {
|
|
138
|
+
fn new(value: Value) -> Self {
|
|
139
|
+
Self(ManuallyDrop::new(BoxValue::new(value)))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/// Gets a reference to the wrapped Ruby value.
|
|
143
|
+
///
|
|
144
|
+
/// This method ensures that access to the Ruby value only happens
|
|
145
|
+
/// within a Ruby thread context by requiring a `Ruby` reference.
|
|
146
|
+
///
|
|
147
|
+
/// # Arguments
|
|
148
|
+
///
|
|
149
|
+
/// * `_ruby` - A reference to the Ruby VM
|
|
150
|
+
fn get(&self, _ruby: &Ruby) -> &Value {
|
|
151
|
+
&self.0
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// Returns `true` if the current thread is a Ruby thread and it is safe
|
|
155
|
+
/// to call `rb_gc_unregister_address` (i.e. drop the inner [`BoxValue`]).
|
|
156
|
+
fn can_drop() -> bool {
|
|
157
|
+
Ruby::get().is_ok()
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
impl Drop for RubyDrop {
|
|
162
|
+
fn drop(&mut self) {
|
|
163
|
+
// SAFETY: `drop` is called exactly once per value.
|
|
164
|
+
let inner = unsafe { ManuallyDrop::take(&mut self.0) };
|
|
165
|
+
|
|
166
|
+
if Ruby::get().is_ok() {
|
|
167
|
+
drop(inner);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
warn!(
|
|
172
|
+
"leaked a Ruby value because it was dropped on a non-Ruby thread; this is safe but \
|
|
173
|
+
indicates a value outlived its expected scope"
|
|
174
|
+
);
|
|
175
|
+
forget(inner);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// Ensures a Tokio runtime context exists, entering one if necessary.
|
|
180
|
+
///
|
|
181
|
+
/// Only creates a runtime guard when not already in a runtime context, avoiding
|
|
182
|
+
/// `EnterGuard` ordering violations that panic.
|
|
183
|
+
///
|
|
184
|
+
/// Lazily initializes the bridge and tracing subsystems on first call. This
|
|
185
|
+
/// deferred initialization is critical for fork safety—each process gets its
|
|
186
|
+
/// own bridge channels and tracing state rather than inheriting stale handles
|
|
187
|
+
/// from the parent.
|
|
188
|
+
///
|
|
189
|
+
/// # Returns
|
|
190
|
+
///
|
|
191
|
+
/// `Some(EnterGuard)` if we entered a new runtime (hold the guard), or `None`
|
|
192
|
+
/// if already in a runtime context.
|
|
193
|
+
///
|
|
194
|
+
/// # Examples
|
|
195
|
+
///
|
|
196
|
+
/// ```rust
|
|
197
|
+
/// let _guard = ensure_runtime_context(ruby);
|
|
198
|
+
/// // Safe to perform async operations
|
|
199
|
+
/// ```
|
|
200
|
+
pub fn ensure_runtime_context(ruby: &Ruby) -> Option<EnterGuard<'static>> {
|
|
201
|
+
let guard = Handle::try_current().is_err().then(|| RUNTIME.enter());
|
|
202
|
+
|
|
203
|
+
// Set up the bridge for Ruby-Rust communication
|
|
204
|
+
let bridge = BRIDGE.get_or_init(|| Bridge::new(ruby));
|
|
205
|
+
|
|
206
|
+
// Initialize tracing for observability
|
|
207
|
+
#[allow(clippy::print_stderr, reason = "logger has not been initialized yet")]
|
|
208
|
+
TRACING_INIT.get_or_init(|| {
|
|
209
|
+
let maybe_logger = Logger::new(ruby, bridge.clone())
|
|
210
|
+
.inspect_err(|error| eprintln!("failed to create logger: {error:#}"))
|
|
211
|
+
.ok();
|
|
212
|
+
|
|
213
|
+
if let Err(error) = initialize_tracing(maybe_logger) {
|
|
214
|
+
eprintln!("failed to initialize tracing: {error:#}");
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
guard
|
|
219
|
+
}
|