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
data/Rakefile ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rb_sys/cargo/metadata"
5
+ require "rb_sys/extensiontask"
6
+
7
+ task build: :compile
8
+
9
+ GEMSPEC = Gem::Specification.load("prosody.gemspec")
10
+
11
+ begin
12
+ RbSys::ExtensionTask.new("prosody", GEMSPEC) do |ext|
13
+ ext.lib_dir = "lib/prosody"
14
+ end
15
+ rescue RbSys::CargoMetadataError
16
+ # This is expected for the source gem, which can't be installed directly
17
+ warn "Source gem cannot be installed directly, must be a supported platform"
18
+ end
19
+
20
+ require "rspec/core/rake_task"
21
+
22
+ RSpec::Core::RakeTask.new(:spec)
23
+
24
+ require "standard/rake"
25
+
26
+ task default: %i[compile spec standard]
@@ -0,0 +1,38 @@
1
+ [package]
2
+ edition = "2024"
3
+ license = "MIT"
4
+ name = "prosody"
5
+ publish = false
6
+ version = "0.1.0"
7
+
8
+ [lib]
9
+ crate-type = ["cdylib"]
10
+
11
+ [dependencies]
12
+ atomic-take.workspace = true
13
+ bumpalo = { workspace = true, features = ["collections"] }
14
+ educe.workspace = true
15
+ futures.workspace = true
16
+ magnus = { workspace = true, default-features = false }
17
+ opentelemetry.workspace = true
18
+ prosody = { workspace = true }
19
+ rb-sys.workspace = true
20
+ serde = { workspace = true, features = ["derive"] }
21
+ serde-untagged.workspace = true
22
+ serde_magnus.workspace = true
23
+ thiserror.workspace = true
24
+ tokio = { workspace = true, features = ["macros", "rt-multi-thread", "sync"] }
25
+ tokio-stream.workspace = true
26
+ tracing.workspace = true
27
+ tracing-opentelemetry.workspace = true
28
+ tracing-subscriber.workspace = true
29
+
30
+
31
+ [target.'cfg(not(target_os = "windows"))'.dependencies]
32
+ tikv-jemallocator = { workspace = true, features = [
33
+ "disable_initial_exec_tls",
34
+ ] }
35
+
36
+
37
+ [lints]
38
+ workspace = true
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("prosody/prosody")
@@ -0,0 +1,171 @@
1
+ //! # Admin Client Module
2
+ //!
3
+ //! Provides administrative capabilities for Kafka through the Prosody library.
4
+ //! This module implements Ruby bindings for creating and deleting Kafka topics.
5
+
6
+ use crate::bridge::Bridge;
7
+ use crate::util::ensure_runtime_context;
8
+ use crate::{ROOT_MOD, id};
9
+ use magnus::value::ReprValue;
10
+ use magnus::{Error, Module, Object, Ruby, Value, function, method};
11
+ use prosody::admin::{AdminConfiguration, ProsodyAdminClient, TopicConfiguration};
12
+ use std::sync::Arc;
13
+ use tracing::Span;
14
+
15
+ /// Ruby wrapper for the Prosody admin client.
16
+ ///
17
+ /// This struct provides administrative operations for Kafka topics, such as
18
+ /// creating and deleting topics. It wraps the Rust `ProsodyAdminClient` and
19
+ /// uses the bridge mechanism to handle asynchronous operations from Ruby.
20
+ #[magnus::wrap(class = "Prosody::AdminClient")]
21
+ pub struct AdminClient {
22
+ /// The underlying Prosody admin client
23
+ client: Arc<ProsodyAdminClient>,
24
+ /// Bridge for executing asynchronous operations from Ruby
25
+ bridge: Bridge,
26
+ /// PID at construction time, used to detect post-fork usage
27
+ pid: u32,
28
+ }
29
+
30
+ impl AdminClient {
31
+ /// Creates a new admin client with the specified bootstrap servers.
32
+ ///
33
+ /// # Arguments
34
+ ///
35
+ /// * `ruby` - Reference to the Ruby VM
36
+ /// * `bootstrap_servers` - List of Kafka bootstrap server addresses
37
+ ///
38
+ /// # Errors
39
+ ///
40
+ /// Returns a `Magnus::Error` if:
41
+ /// - The client cannot be created with the provided bootstrap servers
42
+ /// - The bridge is not initialized
43
+ #[allow(clippy::needless_pass_by_value)]
44
+ pub fn new(ruby: &Ruby, bootstrap_servers: Vec<String>) -> Result<Self, Error> {
45
+ let _guard = ensure_runtime_context(ruby);
46
+ let admin_config = AdminConfiguration::new(bootstrap_servers)
47
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
48
+
49
+ let client = Arc::new(
50
+ ProsodyAdminClient::new(&admin_config)
51
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?,
52
+ );
53
+
54
+ let bridge = crate::BRIDGE
55
+ .get()
56
+ .ok_or(Error::new(
57
+ ruby.exception_runtime_error(),
58
+ "Bridge not initialized",
59
+ ))?
60
+ .clone();
61
+
62
+ Ok(Self {
63
+ client,
64
+ bridge,
65
+ pid: std::process::id(),
66
+ })
67
+ }
68
+
69
+ fn check_fork(ruby: &Ruby, this: &Self) -> Result<(), Error> {
70
+ if std::process::id() != this.pid {
71
+ return Err(Error::new(
72
+ ruby.exception_runtime_error(),
73
+ "Prosody::AdminClient cannot be used after fork. Create a new client in the child \
74
+ process.",
75
+ ));
76
+ }
77
+ Ok(())
78
+ }
79
+
80
+ /// Creates a new Kafka topic.
81
+ ///
82
+ /// # Arguments
83
+ ///
84
+ /// * `ruby` - Reference to the Ruby VM
85
+ /// * `this` - The admin client instance
86
+ /// * `name` - Name of the topic to create
87
+ /// * `partition_count` - Number of partitions for the topic
88
+ /// * `replication_factor` - Replication factor for the topic
89
+ ///
90
+ /// # Errors
91
+ ///
92
+ /// Returns a `Magnus::Error` if:
93
+ /// - The topic creation fails
94
+ /// - There's an issue with the asynchronous execution
95
+ pub fn create_topic(
96
+ ruby: &Ruby,
97
+ this: &Self,
98
+ name: String,
99
+ partition_count: u16,
100
+ replication_factor: u16,
101
+ ) -> Result<(), Error> {
102
+ Self::check_fork(ruby, this)?;
103
+ let topic_config = TopicConfiguration::builder()
104
+ .name(name)
105
+ .partition_count(partition_count)
106
+ .replication_factor(replication_factor)
107
+ .build()
108
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))?;
109
+
110
+ let client = this.client.clone();
111
+ let future = async move { client.create_topic(&topic_config).await };
112
+
113
+ this.bridge
114
+ .wait_for(ruby, future, Span::current())?
115
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))
116
+ }
117
+
118
+ /// Deletes a Kafka topic.
119
+ ///
120
+ /// # Arguments
121
+ ///
122
+ /// * `ruby` - Reference to the Ruby VM
123
+ /// * `this` - The admin client instance
124
+ /// * `name` - Name of the topic to delete
125
+ ///
126
+ /// # Errors
127
+ ///
128
+ /// Returns a `Magnus::Error` if:
129
+ /// - The topic deletion fails
130
+ /// - There's an issue with the asynchronous execution
131
+ pub fn delete_topic(ruby: &Ruby, this: &Self, name: String) -> Result<(), Error> {
132
+ Self::check_fork(ruby, this)?;
133
+ let client = this.client.clone();
134
+ let future = async move { client.delete_topic(&name).await };
135
+
136
+ this.bridge
137
+ .wait_for(ruby, future, Span::current())?
138
+ .map_err(|error| Error::new(ruby.exception_runtime_error(), error.to_string()))
139
+ }
140
+ }
141
+
142
+ /// Initializes the admin module by registering the `Prosody::AdminClient`
143
+ /// class.
144
+ ///
145
+ /// # Arguments
146
+ ///
147
+ /// * `ruby` - Reference to the Ruby VM
148
+ ///
149
+ /// # Errors
150
+ ///
151
+ /// Returns a `Magnus::Error` if class or method definition fails
152
+ pub fn init(ruby: &Ruby) -> Result<(), Error> {
153
+ let module = ruby.get_inner(&ROOT_MOD);
154
+ let class_id = id!(ruby, "AdminClient");
155
+ let class = module.define_class(class_id, ruby.class_object())?;
156
+
157
+ class.define_singleton_method("new", function!(AdminClient::new, 1))?;
158
+ class.define_method(
159
+ id!(ruby, "create_topic"),
160
+ method!(AdminClient::create_topic, 3),
161
+ )?;
162
+ class.define_method(
163
+ id!(ruby, "delete_topic"),
164
+ method!(AdminClient::delete_topic, 1),
165
+ )?;
166
+
167
+ // Make the admin client class private
168
+ let _: Value = module.funcall(id!(ruby, "private_constant"), (class_id,))?;
169
+
170
+ Ok(())
171
+ }
@@ -0,0 +1,60 @@
1
+ //! This module provides functionality to bridge between Rust and Ruby, allowing
2
+ //! asynchronous operations in Rust to communicate with the Ruby runtime.
3
+ //!
4
+ //! It includes mechanisms for executing Ruby code from Rust threads and for
5
+ //! managing asynchronous callback communication between the two languages.
6
+
7
+ use crate::bridge::Bridge;
8
+ use crate::id;
9
+ use crate::util::ThreadSafeValue;
10
+ use magnus::value::ReprValue;
11
+ use magnus::{Error, IntoValue, Ruby, Value};
12
+
13
+ /// Handles asynchronous callbacks from Rust to Ruby using a thread-safe queue.
14
+ ///
15
+ /// This struct provides a way to send values back to Ruby from asynchronous
16
+ /// Rust operations, ensuring thread safety by wrapping the Ruby queue in a
17
+ /// thread-safe container.
18
+ pub struct AsyncCallback {
19
+ /// A thread-safe wrapper around a Ruby Queue object
20
+ queue: ThreadSafeValue,
21
+ }
22
+
23
+ impl AsyncCallback {
24
+ /// Creates a new `AsyncCallback` from a Ruby Queue object.
25
+ ///
26
+ /// # Arguments
27
+ ///
28
+ /// * `queue` - A Ruby Queue object that will receive values from Rust
29
+ /// * `bridge` - The bridge used to defer cleanup of the wrapped queue
30
+ /// value onto the Ruby thread when the callback is dropped
31
+ pub fn from_queue(queue: Value, bridge: Bridge) -> Self {
32
+ Self {
33
+ queue: ThreadSafeValue::new(queue, bridge),
34
+ }
35
+ }
36
+
37
+ /// Completes the callback by pushing a value to the associated Ruby queue.
38
+ ///
39
+ /// This consumes the callback, preventing it from being used multiple
40
+ /// times.
41
+ ///
42
+ /// # Arguments
43
+ ///
44
+ /// * `ruby` - A reference to the Ruby VM
45
+ /// * `value` - The value to push to the queue, which will be converted to a
46
+ /// Ruby value
47
+ ///
48
+ /// # Errors
49
+ ///
50
+ /// Returns an error if the Ruby function call fails
51
+ pub fn complete<V>(self, ruby: &Ruby, value: V) -> Result<(), Error>
52
+ where
53
+ V: IntoValue,
54
+ {
55
+ self.queue
56
+ .get(ruby)
57
+ .funcall(id!(ruby, "push"), (value.into_value_with(ruby),))
58
+ .map(|_: Value| ())
59
+ }
60
+ }
@@ -0,0 +1,332 @@
1
+ //! # Bridge Module
2
+ //!
3
+ //! Facilitates communication between Ruby and Rust, particularly for handling
4
+ //! asynchronous operations. This module creates a dedicated Ruby thread that
5
+ //! processes functions sent from Rust, allowing async Rust code to interact
6
+ //! safely with the Ruby runtime.
7
+
8
+ use crate::bridge::callback::AsyncCallback;
9
+ use crate::gvl::{GvlError, without_gvl};
10
+ use crate::{ROOT_MOD, RUNTIME, id};
11
+ use atomic_take::AtomicTake;
12
+ use educe::Educe;
13
+ use futures::executor::block_on;
14
+ use magnus::value::{Lazy, ReprValue};
15
+ use magnus::{Error, Module, RClass, Ruby, Value};
16
+ use std::any::Any;
17
+ use thiserror::Error;
18
+ use tokio::select;
19
+ use tokio::sync::mpsc::error::SendError;
20
+ use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel};
21
+ use tokio::sync::oneshot;
22
+ use tracing::{Instrument, Span, debug, error, warn};
23
+
24
+ /// Helper module for handling callbacks from async Rust code to Ruby.
25
+ mod callback;
26
+
27
+ /// Maximum number of commands to process in a single poll operation.
28
+ const POLL_BATCH_SIZE: usize = 16;
29
+
30
+ /// Lazily initialized reference to Ruby's Thread class.
31
+ #[allow(clippy::expect_used)]
32
+ pub static THREAD_CLASS: Lazy<RClass> = Lazy::new(|ruby| {
33
+ ruby.class_object()
34
+ .const_get(id!(ruby, "Thread"))
35
+ .expect("Failed to load Thread class")
36
+ });
37
+
38
+ /// Lazily initialized reference to Ruby's `Thread::Queue` class.
39
+ #[allow(clippy::expect_used)]
40
+ pub static QUEUE_CLASS: Lazy<RClass> = Lazy::new(|ruby| {
41
+ ruby.get_inner(&THREAD_CLASS)
42
+ .const_get(id!(ruby, "Queue"))
43
+ .expect("Failed to load Queue class")
44
+ });
45
+
46
+ /// Type alias for a boxed closure that can be executed in a Ruby context.
47
+ pub(crate) type RubyFunction = Box<dyn FnOnce(&Ruby) + Send>;
48
+
49
+ /// A wrapper for results returned from async operations to Ruby.
50
+ ///
51
+ /// Uses `AtomicTake` to ensure the value can only be extracted once.
52
+ #[derive(Debug)]
53
+ #[magnus::wrap(class = "Prosody::DynamicResult")]
54
+ struct DynamicResult(AtomicTake<Box<dyn Any + Send>>);
55
+
56
+ /// Bridge between Ruby and Rust for async operations.
57
+ ///
58
+ /// Creates a dedicated Ruby thread that processes functions sent from Rust,
59
+ /// allowing async Rust code to interact safely with the Ruby runtime.
60
+ #[derive(Educe, Clone)]
61
+ #[educe(Debug)]
62
+ pub struct Bridge {
63
+ /// Channel sender for submitting functions to be executed in the Ruby
64
+ /// context.
65
+ #[educe(Debug(ignore))]
66
+ tx: UnboundedSender<RubyFunction>,
67
+ }
68
+
69
+ impl Bridge {
70
+ /// Creates a new Bridge.
71
+ ///
72
+ /// The internal command channel is unbounded. In typical request/response
73
+ /// flows, callers wait for Ruby to process commands, which tends to bound
74
+ /// queue growth in practice. However, fire-and-forget uses (e.g. from
75
+ /// `Drop` implementations on non-Ruby threads) can enqueue without
76
+ /// blocking, so growth is unbounded if the Ruby bridge thread is stalled.
77
+ ///
78
+ /// # Arguments
79
+ ///
80
+ /// * `ruby` - Reference to the Ruby VM.
81
+ ///
82
+ /// # Returns
83
+ ///
84
+ /// A new `Bridge` instance.
85
+ pub fn new(ruby: &Ruby) -> Self {
86
+ let (tx, mut rx) = unbounded_channel();
87
+
88
+ // Create a dedicated Ruby thread to process functions
89
+ ruby.thread_create_from_fn(move |ruby| {
90
+ loop {
91
+ let Err(error) = poll(&mut rx, ruby) else {
92
+ continue;
93
+ };
94
+
95
+ if matches!(error, BridgeError::Shutdown) {
96
+ debug!("shutting down Ruby bridge");
97
+ break;
98
+ }
99
+
100
+ error!("error during Ruby bridge poll: {error:#}");
101
+ }
102
+ });
103
+
104
+ Self { tx }
105
+ }
106
+
107
+ /// Runs a function in the Ruby context and returns its result
108
+ /// asynchronously.
109
+ ///
110
+ /// # Arguments
111
+ ///
112
+ /// * `function` - Closure to run in the Ruby context.
113
+ ///
114
+ /// # Returns
115
+ ///
116
+ /// The result of the function wrapped in a `Result`.
117
+ ///
118
+ /// # Errors
119
+ ///
120
+ /// Returns `BridgeError::Shutdown` if the bridge has been shut down.
121
+ pub async fn run<F, T>(&self, function: F) -> Result<T, BridgeError>
122
+ where
123
+ F: FnOnce(&Ruby) -> T + Send + 'static,
124
+ T: Send + 'static,
125
+ {
126
+ let (result_tx, result_rx) = oneshot::channel();
127
+
128
+ // Wrap the function to capture its result
129
+ let function = |ruby: &Ruby| {
130
+ let result = function(ruby);
131
+ if result_tx.send(result).is_err() {
132
+ error!("failed to send Ruby bridge result");
133
+ }
134
+ };
135
+
136
+ self.tx
137
+ .send(Box::new(function))
138
+ .map_err(|_| BridgeError::Shutdown)?;
139
+
140
+ result_rx.await.map_err(|_| BridgeError::Shutdown)
141
+ }
142
+
143
+ /// Waits for a future to complete and returns its result to Ruby.
144
+ ///
145
+ /// This method bridges asynchronous Rust code with synchronous Ruby code
146
+ /// by:
147
+ /// 1. Creating a Ruby Queue to receive the result
148
+ /// 2. Spawning a Rust task to await the future
149
+ /// 3. Yielding until the result is pushed to the Queue
150
+ ///
151
+ /// # Arguments
152
+ ///
153
+ /// * `ruby` - Reference to the Ruby VM.
154
+ /// * `future` - Future to await.
155
+ ///
156
+ /// # Returns
157
+ ///
158
+ /// The output of the future.
159
+ ///
160
+ /// # Errors
161
+ ///
162
+ /// Returns a Magnus error if there's an issue with the Ruby interaction or
163
+ /// type conversion.
164
+ pub fn wait_for<Fut>(&self, ruby: &Ruby, future: Fut, span: Span) -> Result<Fut::Output, Error>
165
+ where
166
+ Fut: Future + Send + 'static,
167
+ Fut::Output: Send,
168
+ {
169
+ let cloned_span = span.clone();
170
+ let _enter = cloned_span.enter();
171
+
172
+ // Create a Ruby Queue to receive the result
173
+ let queue: Value = ruby.get_inner(&QUEUE_CLASS).funcall(id!(ruby, "new"), ())?;
174
+ let callback = AsyncCallback::from_queue(queue, self.clone());
175
+ let tx = self.tx.clone();
176
+
177
+ // Spawn a task to await the future and send the result back to Ruby
178
+ RUNTIME.spawn(
179
+ async move {
180
+ let result = future.await;
181
+ let function = move |ruby: &Ruby| {
182
+ let value = DynamicResult(AtomicTake::new(Box::new(result)));
183
+ if let Err(error) = callback.complete(ruby, value) {
184
+ error!("failed to complete Ruby callback: {error:#}");
185
+ }
186
+ };
187
+
188
+ if let Err(error) = tx.send(Box::new(function)) {
189
+ error!("failed to send callback to Ruby: {error:#}");
190
+ }
191
+ }
192
+ .instrument(span),
193
+ );
194
+
195
+ // Yield until the result is available, then extract and return it
196
+ queue
197
+ .funcall::<_, _, &DynamicResult>(id!(ruby, "pop"), ())?
198
+ .0
199
+ .take()
200
+ .ok_or_else(|| Error::new(ruby.exception_runtime_error(), "result already taken"))?
201
+ .downcast::<Fut::Output>()
202
+ .map_err(|_| Error::new(ruby.exception_runtime_error(), "failed to downcast result"))
203
+ .map(|output| *output)
204
+ }
205
+
206
+ /// Synchronously enqueues a function for execution on the Ruby thread.
207
+ ///
208
+ /// Unlike [`run`](Self::run), this method does not await a result and can
209
+ /// be called from synchronous contexts such as `Drop` implementations.
210
+ /// Returns `Err` only if the bridge receiver has been dropped (shutdown).
211
+ pub(crate) fn send(&self, function: RubyFunction) -> Result<(), SendError<RubyFunction>> {
212
+ self.tx.send(function)
213
+ }
214
+ }
215
+
216
+ /// Polls for and executes functions in the Ruby context.
217
+ ///
218
+ /// This function processes commands from the receiver channel, allowing them to
219
+ /// be executed in the Ruby context. Since the GVL is expensive to acquire, this
220
+ /// executes multiple commands in a single poll operation to reduce the
221
+ /// overhead.
222
+ ///
223
+ /// # Arguments
224
+ ///
225
+ /// * `rx` - Receiver for commands to execute.
226
+ /// * `ruby` - Reference to the Ruby VM.
227
+ ///
228
+ /// # Returns
229
+ ///
230
+ /// `Ok(())` if commands were processed successfully.
231
+ ///
232
+ /// # Errors
233
+ ///
234
+ /// Returns a `BridgeError` if there was an issue with polling or executing
235
+ /// commands.
236
+ fn poll(rx: &mut UnboundedReceiver<RubyFunction>, ruby: &Ruby) -> Result<(), BridgeError> {
237
+ // Set up cancellation channel
238
+ let (cancel_tx, cancel_rx) = oneshot::channel();
239
+ let mut maybe_cancel_tx = Some(cancel_tx);
240
+
241
+ // Function to be executed without the GVL (Global VM Lock)
242
+ let poll_fn = || {
243
+ // Wait for either a command or cancellation
244
+ let maybe_command = block_on(async {
245
+ select! {
246
+ _ = cancel_rx => Err(BridgeError::Cancelled),
247
+ result = rx.recv() => Ok(result),
248
+ }
249
+ })?;
250
+
251
+ let first_command = maybe_command.ok_or(BridgeError::Shutdown)?;
252
+
253
+ // Batch up to POLL_BATCH_SIZE commands
254
+ let mut commands = Vec::with_capacity(POLL_BATCH_SIZE);
255
+ commands.push(first_command);
256
+
257
+ while commands.len() < POLL_BATCH_SIZE {
258
+ if let Ok(command) = rx.try_recv() {
259
+ commands.push(command);
260
+ } else {
261
+ break;
262
+ }
263
+ }
264
+
265
+ Result::<_, BridgeError>::Ok(commands)
266
+ };
267
+
268
+ // Function to cancel polling if needed
269
+ let cancel_fn = || {
270
+ let Some(cancel_tx) = maybe_cancel_tx.take() else {
271
+ return;
272
+ };
273
+
274
+ if cancel_tx.send(()).is_err() {
275
+ warn!("Failed to cancel poll operation");
276
+ }
277
+ };
278
+
279
+ // Execute poll function without the GVL
280
+ let commands = without_gvl(poll_fn, cancel_fn)??;
281
+
282
+ // Execute each command in the Ruby context
283
+ for command in commands {
284
+ command(ruby);
285
+ }
286
+
287
+ Ok(())
288
+ }
289
+
290
+ /// Errors that can occur during bridge operations.
291
+ #[derive(Debug, Error)]
292
+ pub enum BridgeError {
293
+ /// An error occurred while polling with the GVL released.
294
+ #[error("an error occurred while polling: {0:#}")]
295
+ Gvl(#[from] GvlError),
296
+
297
+ /// Failed to create an async task.
298
+ #[error("failed to create async task: {0:#}")]
299
+ TaskCreate(String),
300
+
301
+ /// An async task was dropped before completion.
302
+ #[error("async task was dropped before completion")]
303
+ TaskDropped,
304
+
305
+ /// A command was cancelled by the Ruby runtime.
306
+ #[error("command was cancelled by Ruby runtime")]
307
+ Cancelled,
308
+
309
+ /// The Ruby bridge was shut down.
310
+ #[error("Ruby bridge was shutdown")]
311
+ Shutdown,
312
+ }
313
+
314
+ /// Initializes the bridge module in Ruby.
315
+ ///
316
+ /// # Arguments
317
+ ///
318
+ /// * `ruby` - Reference to the Ruby VM.
319
+ ///
320
+ /// # Returns
321
+ ///
322
+ /// `Ok(())` if initialization was successful.
323
+ ///
324
+ /// # Errors
325
+ ///
326
+ /// Returns a Magnus error if there's an issue with class definition.
327
+ pub fn init(ruby: &Ruby) -> Result<(), Error> {
328
+ let module = ruby.get_inner(&ROOT_MOD);
329
+ module.define_class("DynamicResult", ruby.class_object())?;
330
+
331
+ Ok(())
332
+ }