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,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
+ }