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