rubyx-py 0.1.0 → 0.2.0
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 +4 -4
- data/README.md +38 -24
- data/docs/assets/logo.png +0 -0
- data/ext/rubyx/src/context.rs +45 -14
- data/ext/rubyx/src/eval.rs +14 -60
- data/ext/rubyx/src/future.rs +534 -14
- data/ext/rubyx/src/gvl.rs +70 -0
- data/ext/rubyx/src/lib.rs +82 -32
- data/ext/rubyx/src/nonblocking_stream.rs +5 -78
- data/ext/rubyx/src/rubyx_object.rs +91 -3
- data/ext/rubyx/src/stream.rs +77 -1
- data/lib/generators/rubyx/templates/example.py +5 -0
- data/lib/generators/rubyx/templates/pyproject.toml +5 -0
- data/lib/rubyx/version.rb +1 -1
- metadata +5 -1
data/ext/rubyx/src/future.rs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
use crate::gvl::{rb_thread_call_without_gvl, rb_thread_check_ints, recv_loop, ubf_cancel};
|
|
1
2
|
use crate::python_api::PythonApi;
|
|
2
3
|
use crate::python_ffi::PyObject;
|
|
3
4
|
use crate::ruby_helpers::runtime_error;
|
|
@@ -6,8 +7,23 @@ use crate::stream::SendableValue;
|
|
|
6
7
|
use crossbeam_channel::{bounded, Receiver};
|
|
7
8
|
use magnus::{Error, Value};
|
|
8
9
|
use std::cell::RefCell;
|
|
10
|
+
use std::ffi::c_void;
|
|
11
|
+
use std::sync::atomic::AtomicBool;
|
|
12
|
+
use std::sync::Arc;
|
|
9
13
|
use std::thread::{self, JoinHandle};
|
|
10
14
|
|
|
15
|
+
struct FutureRecvArgs {
|
|
16
|
+
receiver: Receiver<Result<SendableValue, String>>,
|
|
17
|
+
result: Option<Result<Result<SendableValue, String>, crossbeam_channel::RecvError>>,
|
|
18
|
+
cancel: Arc<AtomicBool>,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
unsafe extern "C" fn future_recv_cb(args: *mut c_void) -> *mut c_void {
|
|
22
|
+
let args = &mut *(args as *mut FutureRecvArgs);
|
|
23
|
+
args.result = recv_loop(&args.receiver, &args.cancel);
|
|
24
|
+
std::ptr::null_mut()
|
|
25
|
+
}
|
|
26
|
+
|
|
11
27
|
/// A future representing an async Python operation running on a background thread.
|
|
12
28
|
///
|
|
13
29
|
/// The Python coroutine is executed via `asyncio.run()` on a dedicated thread.
|
|
@@ -49,22 +65,10 @@ impl RubyxFuture {
|
|
|
49
65
|
}
|
|
50
66
|
}
|
|
51
67
|
|
|
52
|
-
/// Block until the
|
|
68
|
+
/// Block until the future is ready and releasing the GVL so other Ruby threads can run.
|
|
53
69
|
/// Can only be called once — subsequent calls return an error.
|
|
54
70
|
pub fn value(&self) -> Result<Value, Error> {
|
|
55
|
-
|
|
56
|
-
if let Some(handle) = self.handle.borrow_mut().take() {
|
|
57
|
-
let _ = handle.join();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
match self.receiver.try_recv() {
|
|
61
|
-
Ok(Ok(sendable)) => sendable.try_into(),
|
|
62
|
-
Ok(Err(err)) => Err(Error::new(runtime_error(), err)),
|
|
63
|
-
Err(_) => Err(Error::new(
|
|
64
|
-
runtime_error(),
|
|
65
|
-
"Future already consumed or worker failed",
|
|
66
|
-
)),
|
|
67
|
-
}
|
|
71
|
+
value_nonblocking(self)
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
pub fn is_ready(&self) -> bool {
|
|
@@ -80,6 +84,49 @@ impl Drop for RubyxFuture {
|
|
|
80
84
|
}
|
|
81
85
|
}
|
|
82
86
|
|
|
87
|
+
/// Block until the future is ready, releasing the GVL so other Ruby threads can run.
|
|
88
|
+
pub(crate) fn value_nonblocking(future: &RubyxFuture) -> Result<Value, Error> {
|
|
89
|
+
if let Ok(result) = future.receiver.try_recv() {
|
|
90
|
+
if let Some(handle) = future.handle.borrow_mut().take() {
|
|
91
|
+
let _ = handle.join();
|
|
92
|
+
}
|
|
93
|
+
return match result {
|
|
94
|
+
Ok(sendable) => sendable.try_into(),
|
|
95
|
+
Err(err) => Err(Error::new(runtime_error(), err)),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let cancel = Arc::new(AtomicBool::new(false));
|
|
100
|
+
let mut args = FutureRecvArgs {
|
|
101
|
+
receiver: future.receiver.clone(),
|
|
102
|
+
result: None,
|
|
103
|
+
cancel: cancel.clone(),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
unsafe {
|
|
107
|
+
rb_thread_call_without_gvl(
|
|
108
|
+
future_recv_cb,
|
|
109
|
+
&mut args as *mut FutureRecvArgs as *mut c_void,
|
|
110
|
+
Some(ubf_cancel),
|
|
111
|
+
Arc::as_ptr(&cancel) as *mut c_void,
|
|
112
|
+
);
|
|
113
|
+
rb_thread_check_ints();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if let Some(handle) = future.handle.borrow_mut().take() {
|
|
117
|
+
let _ = handle.join();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
match args.result {
|
|
121
|
+
Some(Ok(Ok(sendable))) => sendable.try_into(),
|
|
122
|
+
Some(Ok(Err(err))) => Err(Error::new(runtime_error(), err)),
|
|
123
|
+
_ => Err(Error::new(
|
|
124
|
+
runtime_error(),
|
|
125
|
+
"Future cancelled or worker failed",
|
|
126
|
+
)),
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
83
130
|
/// Run asyncio.run(coroutine) and convert the result to a SendableValue
|
|
84
131
|
/// (thread-safe). Runs on the worker thread with the GIL held.
|
|
85
132
|
fn run_asyncio_sendable(
|
|
@@ -124,3 +171,476 @@ fn run_asyncio_sendable(
|
|
|
124
171
|
api.decref(result);
|
|
125
172
|
sendable
|
|
126
173
|
}
|
|
174
|
+
|
|
175
|
+
#[cfg(test)]
|
|
176
|
+
mod tests {
|
|
177
|
+
use super::*;
|
|
178
|
+
use crate::stream::SendableValue;
|
|
179
|
+
use crate::test_helpers::with_ruby_python;
|
|
180
|
+
use crossbeam_channel::{bounded, unbounded};
|
|
181
|
+
use magnus::value::ReprValue;
|
|
182
|
+
use magnus::TryConvert;
|
|
183
|
+
use serial_test::serial;
|
|
184
|
+
use std::sync::atomic::Ordering;
|
|
185
|
+
use std::time::{Duration, Instant};
|
|
186
|
+
|
|
187
|
+
// ========== future_recv_cb (pure Rust, no Ruby needed) ==========
|
|
188
|
+
|
|
189
|
+
#[test]
|
|
190
|
+
fn test_future_recv_cb_delivers_ok_value() {
|
|
191
|
+
let (tx, rx) = unbounded();
|
|
192
|
+
tx.send(Ok(SendableValue::Integer(42))).unwrap();
|
|
193
|
+
|
|
194
|
+
let cancel = Arc::new(AtomicBool::new(false));
|
|
195
|
+
let mut args = FutureRecvArgs {
|
|
196
|
+
receiver: rx,
|
|
197
|
+
result: None,
|
|
198
|
+
cancel,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
unsafe {
|
|
202
|
+
future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
match args.result.unwrap().unwrap() {
|
|
206
|
+
Ok(SendableValue::Integer(n)) => assert_eq!(n, 42),
|
|
207
|
+
other => panic!("expected Ok(Integer(42)), got {other:?}"),
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
#[test]
|
|
212
|
+
fn test_future_recv_cb_delivers_err_value() {
|
|
213
|
+
let (tx, rx) = unbounded();
|
|
214
|
+
tx.send(Err("python exploded".to_string())).unwrap();
|
|
215
|
+
|
|
216
|
+
let cancel = Arc::new(AtomicBool::new(false));
|
|
217
|
+
let mut args = FutureRecvArgs {
|
|
218
|
+
receiver: rx,
|
|
219
|
+
result: None,
|
|
220
|
+
cancel,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
unsafe {
|
|
224
|
+
future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
match args.result.unwrap().unwrap() {
|
|
228
|
+
Err(msg) => assert_eq!(msg, "python exploded"),
|
|
229
|
+
other => panic!("expected Err, got {other:?}"),
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#[test]
|
|
234
|
+
fn test_future_recv_cb_handles_disconnect() {
|
|
235
|
+
let (tx, rx) = bounded::<Result<SendableValue, String>>(1);
|
|
236
|
+
drop(tx);
|
|
237
|
+
|
|
238
|
+
let cancel = Arc::new(AtomicBool::new(false));
|
|
239
|
+
let mut args = FutureRecvArgs {
|
|
240
|
+
receiver: rx,
|
|
241
|
+
result: None,
|
|
242
|
+
cancel,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
unsafe {
|
|
246
|
+
future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
assert!(args.result.unwrap().is_err());
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
#[test]
|
|
253
|
+
fn test_future_recv_cb_respects_cancel_flag() {
|
|
254
|
+
let (_tx, rx) = bounded::<Result<SendableValue, String>>(1);
|
|
255
|
+
|
|
256
|
+
let cancel = Arc::new(AtomicBool::new(false));
|
|
257
|
+
let cancel_clone = cancel.clone();
|
|
258
|
+
|
|
259
|
+
let mut args = FutureRecvArgs {
|
|
260
|
+
receiver: rx,
|
|
261
|
+
result: None,
|
|
262
|
+
cancel,
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// Set cancel from another thread after a short delay
|
|
266
|
+
std::thread::spawn(move || {
|
|
267
|
+
std::thread::sleep(Duration::from_millis(30));
|
|
268
|
+
cancel_clone.store(true, Ordering::Relaxed);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
let start = Instant::now();
|
|
272
|
+
unsafe {
|
|
273
|
+
future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
|
|
274
|
+
}
|
|
275
|
+
let elapsed = start.elapsed();
|
|
276
|
+
|
|
277
|
+
assert!(args.result.is_none(), "expected None on cancel");
|
|
278
|
+
assert!(
|
|
279
|
+
elapsed < Duration::from_millis(200),
|
|
280
|
+
"cancel took {elapsed:?}, expected < 200ms"
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
#[test]
|
|
285
|
+
fn test_future_recv_cb_cancel_flag_already_set() {
|
|
286
|
+
let (_tx, rx) = bounded::<Result<SendableValue, String>>(1);
|
|
287
|
+
|
|
288
|
+
let cancel = Arc::new(AtomicBool::new(true)); // pre-set
|
|
289
|
+
let mut args = FutureRecvArgs {
|
|
290
|
+
receiver: rx,
|
|
291
|
+
result: None,
|
|
292
|
+
cancel,
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
let start = Instant::now();
|
|
296
|
+
unsafe {
|
|
297
|
+
future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
|
|
298
|
+
}
|
|
299
|
+
let elapsed = start.elapsed();
|
|
300
|
+
|
|
301
|
+
assert!(args.result.is_none(), "expected None on pre-set cancel");
|
|
302
|
+
assert!(
|
|
303
|
+
elapsed < Duration::from_millis(10),
|
|
304
|
+
"pre-set cancel took {elapsed:?}, expected near-instant"
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
#[test]
|
|
309
|
+
fn test_future_recv_cb_with_delayed_producer() {
|
|
310
|
+
let (tx, rx) = bounded::<Result<SendableValue, String>>(1);
|
|
311
|
+
|
|
312
|
+
let cancel = Arc::new(AtomicBool::new(false));
|
|
313
|
+
let mut args = FutureRecvArgs {
|
|
314
|
+
receiver: rx,
|
|
315
|
+
result: None,
|
|
316
|
+
cancel,
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// Send value after a delay (simulates slow Python computation)
|
|
320
|
+
std::thread::spawn(move || {
|
|
321
|
+
std::thread::sleep(Duration::from_millis(100));
|
|
322
|
+
tx.send(Ok(SendableValue::Str("delayed".to_string())))
|
|
323
|
+
.unwrap();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
let start = Instant::now();
|
|
327
|
+
unsafe {
|
|
328
|
+
future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
|
|
329
|
+
}
|
|
330
|
+
let elapsed = start.elapsed();
|
|
331
|
+
|
|
332
|
+
match args.result.unwrap().unwrap() {
|
|
333
|
+
Ok(SendableValue::Str(s)) => assert_eq!(s, "delayed"),
|
|
334
|
+
other => panic!("expected Ok(Str), got {other:?}"),
|
|
335
|
+
}
|
|
336
|
+
assert!(
|
|
337
|
+
elapsed >= Duration::from_millis(80),
|
|
338
|
+
"should have waited for producer, elapsed: {elapsed:?}"
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
#[test]
|
|
343
|
+
fn test_future_recv_cb_all_sendable_types() {
|
|
344
|
+
let (tx, rx) = unbounded();
|
|
345
|
+
tx.send(Ok(SendableValue::Nil)).unwrap();
|
|
346
|
+
tx.send(Ok(SendableValue::Integer(99))).unwrap();
|
|
347
|
+
tx.send(Ok(SendableValue::Float(3.14))).unwrap();
|
|
348
|
+
tx.send(Ok(SendableValue::Str("hello".to_string())))
|
|
349
|
+
.unwrap();
|
|
350
|
+
tx.send(Ok(SendableValue::Bool(true))).unwrap();
|
|
351
|
+
|
|
352
|
+
let cancel = Arc::new(AtomicBool::new(false));
|
|
353
|
+
|
|
354
|
+
// Nil
|
|
355
|
+
let mut args = FutureRecvArgs {
|
|
356
|
+
receiver: rx.clone(),
|
|
357
|
+
result: None,
|
|
358
|
+
cancel: cancel.clone(),
|
|
359
|
+
};
|
|
360
|
+
unsafe {
|
|
361
|
+
future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
|
|
362
|
+
}
|
|
363
|
+
assert!(matches!(
|
|
364
|
+
args.result.unwrap().unwrap(),
|
|
365
|
+
Ok(SendableValue::Nil)
|
|
366
|
+
));
|
|
367
|
+
|
|
368
|
+
// Integer
|
|
369
|
+
let mut args = FutureRecvArgs {
|
|
370
|
+
receiver: rx.clone(),
|
|
371
|
+
result: None,
|
|
372
|
+
cancel: cancel.clone(),
|
|
373
|
+
};
|
|
374
|
+
unsafe {
|
|
375
|
+
future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
|
|
376
|
+
}
|
|
377
|
+
match args.result.unwrap().unwrap() {
|
|
378
|
+
Ok(SendableValue::Integer(n)) => assert_eq!(n, 99),
|
|
379
|
+
other => panic!("expected Integer(99), got {other:?}"),
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Float
|
|
383
|
+
let mut args = FutureRecvArgs {
|
|
384
|
+
receiver: rx.clone(),
|
|
385
|
+
result: None,
|
|
386
|
+
cancel: cancel.clone(),
|
|
387
|
+
};
|
|
388
|
+
unsafe {
|
|
389
|
+
future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
|
|
390
|
+
}
|
|
391
|
+
match args.result.unwrap().unwrap() {
|
|
392
|
+
Ok(SendableValue::Float(f)) => assert!((f - 3.14).abs() < f64::EPSILON),
|
|
393
|
+
other => panic!("expected Float(3.14), got {other:?}"),
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Str
|
|
397
|
+
let mut args = FutureRecvArgs {
|
|
398
|
+
receiver: rx.clone(),
|
|
399
|
+
result: None,
|
|
400
|
+
cancel: cancel.clone(),
|
|
401
|
+
};
|
|
402
|
+
unsafe {
|
|
403
|
+
future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
|
|
404
|
+
}
|
|
405
|
+
match args.result.unwrap().unwrap() {
|
|
406
|
+
Ok(SendableValue::Str(s)) => assert_eq!(s, "hello"),
|
|
407
|
+
other => panic!("expected Str(\"hello\"), got {other:?}"),
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Bool
|
|
411
|
+
let mut args = FutureRecvArgs {
|
|
412
|
+
receiver: rx.clone(),
|
|
413
|
+
result: None,
|
|
414
|
+
cancel: cancel.clone(),
|
|
415
|
+
};
|
|
416
|
+
unsafe {
|
|
417
|
+
future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
|
|
418
|
+
}
|
|
419
|
+
match args.result.unwrap().unwrap() {
|
|
420
|
+
Ok(SendableValue::Bool(b)) => assert!(b),
|
|
421
|
+
other => panic!("expected Bool(true), got {other:?}"),
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ========== RubyxFuture::value (needs Ruby for SendableValue → Value) ==========
|
|
426
|
+
|
|
427
|
+
/// Helper: create a RubyxFuture backed by a channel (no Python needed)
|
|
428
|
+
fn make_future(rx: Receiver<Result<SendableValue, String>>) -> RubyxFuture {
|
|
429
|
+
RubyxFuture {
|
|
430
|
+
receiver: rx,
|
|
431
|
+
handle: RefCell::new(None),
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/// Helper: create a RubyxFuture with a JoinHandle
|
|
436
|
+
fn make_future_with_handle(
|
|
437
|
+
rx: Receiver<Result<SendableValue, String>>,
|
|
438
|
+
handle: JoinHandle<()>,
|
|
439
|
+
) -> RubyxFuture {
|
|
440
|
+
RubyxFuture {
|
|
441
|
+
receiver: rx,
|
|
442
|
+
handle: RefCell::new(Some(handle)),
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/// Test helper: read from future without going through value()/value_nonblocking().
|
|
447
|
+
/// Avoids rb_thread_call_without_gvl which deadlocks in the embedded Ruby test harness.
|
|
448
|
+
fn test_recv(future: &RubyxFuture) -> Result<Value, Error> {
|
|
449
|
+
if let Some(handle) = future.handle.borrow_mut().take() {
|
|
450
|
+
let _ = handle.join();
|
|
451
|
+
}
|
|
452
|
+
match future.receiver.try_recv() {
|
|
453
|
+
Ok(Ok(sendable)) => sendable.try_into(),
|
|
454
|
+
Ok(Err(err)) => Err(Error::new(runtime_error(), err)),
|
|
455
|
+
Err(_) => Err(Error::new(
|
|
456
|
+
runtime_error(),
|
|
457
|
+
"Future already consumed or worker failed",
|
|
458
|
+
)),
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
#[test]
|
|
463
|
+
#[serial]
|
|
464
|
+
fn test_value_fast_path_already_ready() {
|
|
465
|
+
with_ruby_python(|_ruby, _api| {
|
|
466
|
+
let (tx, rx) = bounded(1);
|
|
467
|
+
tx.send(Ok(SendableValue::Integer(7))).unwrap();
|
|
468
|
+
|
|
469
|
+
let future = make_future(rx);
|
|
470
|
+
let val = test_recv(&future).unwrap();
|
|
471
|
+
assert_eq!(i64::try_convert(val).unwrap(), 7);
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
#[test]
|
|
476
|
+
#[serial]
|
|
477
|
+
fn test_value_fast_path_error() {
|
|
478
|
+
with_ruby_python(|_ruby, _api| {
|
|
479
|
+
let (tx, rx) = bounded(1);
|
|
480
|
+
tx.send(Err("boom".to_string())).unwrap();
|
|
481
|
+
|
|
482
|
+
let future = make_future(rx);
|
|
483
|
+
let err = test_recv(&future).unwrap_err();
|
|
484
|
+
assert!(err.to_string().contains("boom"));
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
#[test]
|
|
489
|
+
#[serial]
|
|
490
|
+
fn test_value_slow_path_waits_for_producer() {
|
|
491
|
+
with_ruby_python(|_ruby, _api| {
|
|
492
|
+
let (tx, rx) = bounded(1);
|
|
493
|
+
|
|
494
|
+
let handle = std::thread::spawn(move || {
|
|
495
|
+
std::thread::sleep(Duration::from_millis(100));
|
|
496
|
+
tx.send(Ok(SendableValue::Str("from worker".to_string())))
|
|
497
|
+
.unwrap();
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
let future = make_future_with_handle(rx, handle);
|
|
501
|
+
|
|
502
|
+
let start = Instant::now();
|
|
503
|
+
let val = test_recv(&future).unwrap();
|
|
504
|
+
let elapsed = start.elapsed();
|
|
505
|
+
|
|
506
|
+
let s: String = TryConvert::try_convert(val).unwrap();
|
|
507
|
+
assert_eq!(s, "from worker");
|
|
508
|
+
assert!(
|
|
509
|
+
elapsed >= Duration::from_millis(80),
|
|
510
|
+
"should have waited, elapsed: {elapsed:?}"
|
|
511
|
+
);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
#[test]
|
|
516
|
+
#[serial]
|
|
517
|
+
fn test_value_returns_all_sendable_types() {
|
|
518
|
+
with_ruby_python(|_ruby, _api| {
|
|
519
|
+
// Integer
|
|
520
|
+
let (tx, rx) = bounded(1);
|
|
521
|
+
tx.send(Ok(SendableValue::Integer(42))).unwrap();
|
|
522
|
+
let val = test_recv(&make_future(rx)).unwrap();
|
|
523
|
+
assert_eq!(i64::try_convert(val).unwrap(), 42);
|
|
524
|
+
|
|
525
|
+
// Float
|
|
526
|
+
let (tx, rx) = bounded(1);
|
|
527
|
+
tx.send(Ok(SendableValue::Float(2.5))).unwrap();
|
|
528
|
+
let val = test_recv(&make_future(rx)).unwrap();
|
|
529
|
+
assert_eq!(f64::try_convert(val).unwrap(), 2.5);
|
|
530
|
+
|
|
531
|
+
// String
|
|
532
|
+
let (tx, rx) = bounded(1);
|
|
533
|
+
tx.send(Ok(SendableValue::Str("hello".to_string())))
|
|
534
|
+
.unwrap();
|
|
535
|
+
let val = test_recv(&make_future(rx)).unwrap();
|
|
536
|
+
let s: String = TryConvert::try_convert(val).unwrap();
|
|
537
|
+
assert_eq!(s, "hello");
|
|
538
|
+
|
|
539
|
+
// Bool
|
|
540
|
+
let (tx, rx) = bounded(1);
|
|
541
|
+
tx.send(Ok(SendableValue::Bool(true))).unwrap();
|
|
542
|
+
let val = test_recv(&make_future(rx)).unwrap();
|
|
543
|
+
assert_eq!(bool::try_convert(val).unwrap(), true);
|
|
544
|
+
|
|
545
|
+
// Nil
|
|
546
|
+
let (tx, rx) = bounded(1);
|
|
547
|
+
tx.send(Ok(SendableValue::Nil)).unwrap();
|
|
548
|
+
let val = test_recv(&make_future(rx)).unwrap();
|
|
549
|
+
assert!(val.is_nil());
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
#[test]
|
|
554
|
+
#[serial]
|
|
555
|
+
fn test_value_consumed_twice_returns_error() {
|
|
556
|
+
with_ruby_python(|_ruby, _api| {
|
|
557
|
+
let (tx, rx) = bounded(1);
|
|
558
|
+
tx.send(Ok(SendableValue::Integer(1))).unwrap();
|
|
559
|
+
|
|
560
|
+
let future = make_future(rx);
|
|
561
|
+
let _ = test_recv(&future).unwrap();
|
|
562
|
+
let err = test_recv(&future).unwrap_err();
|
|
563
|
+
assert!(err.to_string().contains("consumed") || err.to_string().contains("failed"));
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
#[test]
|
|
568
|
+
fn test_is_ready_before_and_after_send() {
|
|
569
|
+
let (tx, rx) = bounded(1);
|
|
570
|
+
let future = make_future(rx);
|
|
571
|
+
|
|
572
|
+
assert!(!future.is_ready());
|
|
573
|
+
tx.send(Ok(SendableValue::Integer(1))).unwrap();
|
|
574
|
+
assert!(future.is_ready());
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
#[test]
|
|
578
|
+
fn test_drop_joins_handle() {
|
|
579
|
+
let (tx, rx) = bounded(1);
|
|
580
|
+
let flag = Arc::new(AtomicBool::new(false));
|
|
581
|
+
let flag_clone = flag.clone();
|
|
582
|
+
|
|
583
|
+
let handle = std::thread::spawn(move || {
|
|
584
|
+
std::thread::sleep(Duration::from_millis(50));
|
|
585
|
+
flag_clone.store(true, Ordering::Relaxed);
|
|
586
|
+
let _ = tx.send(Ok(SendableValue::Nil));
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
let future = make_future_with_handle(rx, handle);
|
|
590
|
+
drop(future); // should join the handle
|
|
591
|
+
|
|
592
|
+
assert!(
|
|
593
|
+
flag.load(Ordering::Relaxed),
|
|
594
|
+
"worker should have completed before drop returned"
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ========== PyObjectRef: future.value() returns RubyxObject ==========
|
|
599
|
+
|
|
600
|
+
#[test]
|
|
601
|
+
#[serial]
|
|
602
|
+
fn test_value_returns_rubyx_object_for_py_object_ref() {
|
|
603
|
+
with_ruby_python(|_ruby, api| {
|
|
604
|
+
let os = api.import_module("os").expect("os should import");
|
|
605
|
+
api.incref(os); // incref for PyObjectRef
|
|
606
|
+
|
|
607
|
+
let (tx, rx) = bounded(1);
|
|
608
|
+
tx.send(Ok(SendableValue::PyObjectRef(os as usize)))
|
|
609
|
+
.unwrap();
|
|
610
|
+
|
|
611
|
+
let future = make_future(rx);
|
|
612
|
+
let val = test_recv(&future).unwrap();
|
|
613
|
+
|
|
614
|
+
// Should be a RubyxObject, not a primitive
|
|
615
|
+
assert!(!val.is_nil());
|
|
616
|
+
assert!(i64::try_convert(val).is_err(), "should not be Integer");
|
|
617
|
+
assert!(String::try_convert(val).is_err(), "should not be String");
|
|
618
|
+
|
|
619
|
+
api.decref(os);
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
#[test]
|
|
624
|
+
fn test_future_recv_cb_delivers_py_object_ref() {
|
|
625
|
+
let (tx, rx) = unbounded();
|
|
626
|
+
// Use a fake address — we're just testing the callback delivers it through
|
|
627
|
+
let fake_addr = 0xDEADBEEFusize;
|
|
628
|
+
tx.send(Ok(SendableValue::PyObjectRef(fake_addr))).unwrap();
|
|
629
|
+
|
|
630
|
+
let cancel = Arc::new(AtomicBool::new(false));
|
|
631
|
+
let mut args = FutureRecvArgs {
|
|
632
|
+
receiver: rx,
|
|
633
|
+
result: None,
|
|
634
|
+
cancel,
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
unsafe {
|
|
638
|
+
future_recv_cb(&mut args as *mut FutureRecvArgs as *mut c_void);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
match args.result.unwrap().unwrap() {
|
|
642
|
+
Ok(SendableValue::PyObjectRef(addr)) => assert_eq!(addr, fake_addr),
|
|
643
|
+
other => panic!("expected PyObjectRef(0xDEADBEEF), got {other:?}"),
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
use crossbeam_channel::Receiver;
|
|
2
|
+
use std::ffi::c_void;
|
|
3
|
+
use std::sync::atomic::AtomicBool;
|
|
4
|
+
|
|
5
|
+
extern "C" {
|
|
6
|
+
/// Release the GVL, call `func(data1)`, then reacquire the GVL.
|
|
7
|
+
///
|
|
8
|
+
/// While `func` runs, other Ruby threads can execute. Inside `func`,
|
|
9
|
+
/// you must NOT call any Ruby C API or access any Ruby VALUE.
|
|
10
|
+
///
|
|
11
|
+
/// If `ubf` is provided, Ruby may call it from another thread to
|
|
12
|
+
/// interrupt `func` (e.g., on Thread#kill or signal delivery).
|
|
13
|
+
///
|
|
14
|
+
/// # Safety
|
|
15
|
+
///
|
|
16
|
+
/// - `func` must not touch Ruby objects (GVL is not held).
|
|
17
|
+
/// - `data1` must remain valid for the duration of `func`.
|
|
18
|
+
/// - `data2` must remain valid for the duration of `ubf` (if provided).
|
|
19
|
+
pub(crate) fn rb_thread_call_without_gvl(
|
|
20
|
+
func: unsafe extern "C" fn(*mut c_void) -> *mut c_void,
|
|
21
|
+
data1: *mut c_void,
|
|
22
|
+
ubf: Option<unsafe extern "C" fn(*mut c_void)>,
|
|
23
|
+
data2: *mut c_void,
|
|
24
|
+
) -> *mut c_void;
|
|
25
|
+
|
|
26
|
+
/// Check for pending Ruby interrupts (Thread#kill, signals, etc.).
|
|
27
|
+
///
|
|
28
|
+
/// Must be called WITH the GVL held. If an interrupt is pending,
|
|
29
|
+
/// this raises a Ruby exception (longjmp). Call this immediately
|
|
30
|
+
/// after `rb_thread_call_without_gvl` returns to deliver any
|
|
31
|
+
/// interrupts that arrived while the GVL was released.
|
|
32
|
+
pub(crate) fn rb_thread_check_ints();
|
|
33
|
+
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
pub(crate) unsafe extern "C" fn ubf_cancel(data: *mut c_void) {
|
|
37
|
+
let cancel = &*(data as *const AtomicBool);
|
|
38
|
+
cancel.store(true, std::sync::atomic::Ordering::Relaxed);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Generic receive loop for use inside `rb_thread_call_without_gvl` callbacks.
|
|
42
|
+
///
|
|
43
|
+
/// Polls the channel with a 50ms timeout between cancel-flag checks.
|
|
44
|
+
/// Returns `None` if cancelled, `Some(Ok(item))` on success, or
|
|
45
|
+
/// `Some(Err(_))` if the sender disconnected.
|
|
46
|
+
pub(crate) fn recv_loop<T>(
|
|
47
|
+
receiver: &Receiver<T>,
|
|
48
|
+
cancel: &AtomicBool,
|
|
49
|
+
) -> Option<Result<T, crossbeam_channel::RecvError>> {
|
|
50
|
+
loop {
|
|
51
|
+
match receiver.try_recv() {
|
|
52
|
+
Ok(item) => return Some(Ok(item)),
|
|
53
|
+
Err(crossbeam_channel::TryRecvError::Empty) => {
|
|
54
|
+
if cancel.load(std::sync::atomic::Ordering::Relaxed) {
|
|
55
|
+
return None;
|
|
56
|
+
}
|
|
57
|
+
match receiver.recv_timeout(std::time::Duration::from_millis(50)) {
|
|
58
|
+
Ok(item) => return Some(Ok(item)),
|
|
59
|
+
Err(crossbeam_channel::RecvTimeoutError::Timeout) => continue,
|
|
60
|
+
Err(crossbeam_channel::RecvTimeoutError::Disconnected) => {
|
|
61
|
+
return Some(Err(crossbeam_channel::RecvError));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
Err(crossbeam_channel::TryRecvError::Disconnected) => {
|
|
66
|
+
return Some(Err(crossbeam_channel::RecvError));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|