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