rubyx-py 0.1.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 +7 -0
- data/Cargo.toml +19 -0
- data/README.md +469 -0
- data/ext/rubyx/Cargo.toml +19 -0
- data/ext/rubyx/extconf.rb +22 -0
- data/ext/rubyx/src/async_gen.rs +1298 -0
- data/ext/rubyx/src/context.rs +812 -0
- data/ext/rubyx/src/convert.rs +1498 -0
- data/ext/rubyx/src/eval.rs +377 -0
- data/ext/rubyx/src/exception.rs +184 -0
- data/ext/rubyx/src/future.rs +126 -0
- data/ext/rubyx/src/import.rs +34 -0
- data/ext/rubyx/src/lib.rs +4212 -0
- data/ext/rubyx/src/nonblocking_stream.rs +1422 -0
- data/ext/rubyx/src/pipe_notify.rs +232 -0
- data/ext/rubyx/src/python/sync_adapter.py +31 -0
- data/ext/rubyx/src/python_api.rs +6029 -0
- data/ext/rubyx/src/python_ffi.rs +18 -0
- data/ext/rubyx/src/python_finder.rs +119 -0
- data/ext/rubyx/src/python_guard.rs +25 -0
- data/ext/rubyx/src/ruby_helpers.rs +74 -0
- data/ext/rubyx/src/rubyx_object.rs +1931 -0
- data/ext/rubyx/src/rubyx_stream.rs +950 -0
- data/ext/rubyx/src/stream.rs +713 -0
- data/ext/rubyx/src/test_helpers.rs +351 -0
- data/lib/generators/rubyx/install_generator.rb +24 -0
- data/lib/generators/rubyx/templates/rubyx_initializer.rb +17 -0
- data/lib/rubyx/context.rb +27 -0
- data/lib/rubyx/error.rb +30 -0
- data/lib/rubyx/rails.rb +105 -0
- data/lib/rubyx/railtie.rb +20 -0
- data/lib/rubyx/uv.rb +261 -0
- data/lib/rubyx/version.rb +4 -0
- data/lib/rubyx-py.rb +1 -0
- data/lib/rubyx.rb +136 -0
- metadata +123 -0
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
use crate::api;
|
|
2
|
+
use crate::python_ffi::PyObject;
|
|
3
|
+
use crate::ruby_helpers::runtime_error;
|
|
4
|
+
use crate::rubyx_object::python_to_sendable;
|
|
5
|
+
use crossbeam_channel::{bounded, unbounded, Receiver, Sender};
|
|
6
|
+
use magnus::value::ReprValue;
|
|
7
|
+
use magnus::{IntoValue, Value};
|
|
8
|
+
use std::thread;
|
|
9
|
+
use std::thread::JoinHandle;
|
|
10
|
+
|
|
11
|
+
/// Thread-safe intermediate representation of a Python/Ruby value.
|
|
12
|
+
///
|
|
13
|
+
/// `magnus::Value` wraps a `*mut RBasic` (a raw pointer), which is NOT Send.
|
|
14
|
+
/// We must convert Python values to pure Rust types in the worker thread,
|
|
15
|
+
/// send those through the channel, and convert to `magnus::Value` on the
|
|
16
|
+
/// Ruby thread (which holds the GVL).
|
|
17
|
+
#[derive(Debug)]
|
|
18
|
+
pub(crate) enum SendableValue {
|
|
19
|
+
Nil,
|
|
20
|
+
Integer(i64),
|
|
21
|
+
Float(f64),
|
|
22
|
+
Str(String),
|
|
23
|
+
Bool(bool),
|
|
24
|
+
List(Vec<SendableValue>),
|
|
25
|
+
Dict(Vec<(SendableValue, SendableValue)>),
|
|
26
|
+
}
|
|
27
|
+
impl TryInto<magnus::Value> for SendableValue {
|
|
28
|
+
type Error = magnus::Error;
|
|
29
|
+
|
|
30
|
+
fn try_into(self) -> Result<Value, Self::Error> {
|
|
31
|
+
let ruby = magnus::Ruby::get().map_err(|_| {
|
|
32
|
+
magnus::Error::new(runtime_error(), "Must be called on Ruby thread".to_string())
|
|
33
|
+
})?;
|
|
34
|
+
let result = match self {
|
|
35
|
+
SendableValue::Nil => ruby.qnil().as_value(),
|
|
36
|
+
SendableValue::Integer(n) => n.into_value_with(&ruby),
|
|
37
|
+
SendableValue::Float(f) => f.into_value_with(&ruby),
|
|
38
|
+
SendableValue::Str(s) => s.as_str().into_value_with(&ruby),
|
|
39
|
+
SendableValue::Bool(b) => b.into_value_with(&ruby),
|
|
40
|
+
SendableValue::List(l) => {
|
|
41
|
+
let ruby_array = ruby.ary_new_capa(l.len());
|
|
42
|
+
for item in l {
|
|
43
|
+
let val: Value = item.try_into()?;
|
|
44
|
+
ruby_array.push(val)?;
|
|
45
|
+
}
|
|
46
|
+
ruby_array.as_value()
|
|
47
|
+
}
|
|
48
|
+
SendableValue::Dict(entries) => {
|
|
49
|
+
let hash = ruby.hash_new();
|
|
50
|
+
for (k, v) in entries {
|
|
51
|
+
let key: Value = k.try_into()?;
|
|
52
|
+
let val: Value = v.try_into()?;
|
|
53
|
+
hash.aset(key, val)?;
|
|
54
|
+
}
|
|
55
|
+
hash.as_value()
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
Ok(result)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// Item sent through stream - Send safe
|
|
63
|
+
pub(crate) enum StreamItem {
|
|
64
|
+
Value(SendableValue),
|
|
65
|
+
Error(String),
|
|
66
|
+
End,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// A Stream of values from a background thread
|
|
70
|
+
#[allow(dead_code)]
|
|
71
|
+
pub struct AsyncStream {
|
|
72
|
+
receiver: Option<Receiver<StreamItem>>,
|
|
73
|
+
cancel_sender: Sender<()>,
|
|
74
|
+
handle: Option<JoinHandle<()>>,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
impl AsyncStream {
|
|
78
|
+
/// Stream which iterates a Python Iterator in the background
|
|
79
|
+
#[allow(dead_code)]
|
|
80
|
+
pub fn from_python_iterator(py_iter: *mut PyObject) -> Self {
|
|
81
|
+
let (value_tx, value_rx) = unbounded();
|
|
82
|
+
let (cancel_tx, cancel_rx) = bounded(1);
|
|
83
|
+
|
|
84
|
+
// Cast the raw pointer to usize so it can cross the thread boundary.
|
|
85
|
+
// *mut PyObject is not Send, but the usize value is just a number.
|
|
86
|
+
// This is safe because the worker thread will acquire the GIL before
|
|
87
|
+
// using the pointer, and the pointer remains valid (Python iterator
|
|
88
|
+
// is kept alive by its refcount).
|
|
89
|
+
let py_iter_addr = py_iter as usize;
|
|
90
|
+
|
|
91
|
+
let handle = thread::spawn(move || {
|
|
92
|
+
let py_iter = py_iter_addr as *mut PyObject;
|
|
93
|
+
// Worker thread: acquire GIL, iterate, send values
|
|
94
|
+
let api = api();
|
|
95
|
+
let gil = api.ensure_gil();
|
|
96
|
+
|
|
97
|
+
loop {
|
|
98
|
+
// Check if there is a cancellation
|
|
99
|
+
if cancel_rx.try_recv().is_ok() {
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Get next item from Python iterator
|
|
104
|
+
let item = api.iter_next(py_iter);
|
|
105
|
+
if item.is_null() {
|
|
106
|
+
// Check if an exception was raised (vs normal exhaustion)
|
|
107
|
+
if api.has_error() {
|
|
108
|
+
if let Some(exc) = crate::python_api::PythonApi::extract_exception(api) {
|
|
109
|
+
value_tx.send(StreamItem::Error(exc.to_string())).ok();
|
|
110
|
+
} else {
|
|
111
|
+
value_tx.send(StreamItem::End).ok();
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
value_tx.send(StreamItem::End).ok();
|
|
115
|
+
}
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
// Convert and send to ruby
|
|
119
|
+
let ruby_value = python_to_sendable(item, api)
|
|
120
|
+
.map_err(|e| format!("Error converting Python value to Ruby: {e}"));
|
|
121
|
+
api.decref(item);
|
|
122
|
+
match ruby_value {
|
|
123
|
+
Ok(value) => {
|
|
124
|
+
if value_tx.send(StreamItem::Value(value)).is_err() {
|
|
125
|
+
break; // Consumer dropped — stop producing
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
Err(e) => {
|
|
129
|
+
value_tx.send(StreamItem::Error(e)).ok();
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
api.decref(py_iter);
|
|
135
|
+
api.release_gil(gil);
|
|
136
|
+
});
|
|
137
|
+
Self {
|
|
138
|
+
receiver: Some(value_rx),
|
|
139
|
+
cancel_sender: cancel_tx,
|
|
140
|
+
handle: Some(handle),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
impl Drop for AsyncStream {
|
|
146
|
+
fn drop(&mut self) {
|
|
147
|
+
// 1. Signal cancellation
|
|
148
|
+
self.cancel_sender.try_send(()).ok();
|
|
149
|
+
// 2. Drain then drop the receiver so value_tx.send() returns Err,
|
|
150
|
+
// unblocking the worker thread
|
|
151
|
+
if let Some(rx) = self.receiver.take() {
|
|
152
|
+
while rx.try_recv().is_ok() {}
|
|
153
|
+
drop(rx);
|
|
154
|
+
}
|
|
155
|
+
// 3. Join the worker thread (now guaranteed to exit)
|
|
156
|
+
if let Some(handle) = self.handle.take() {
|
|
157
|
+
let _ = handle.join();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#[cfg(test)]
|
|
163
|
+
impl AsyncStream {
|
|
164
|
+
/// Test constructor: create an AsyncStream from a channel of `Option<SendableValue>`.
|
|
165
|
+
/// `Some(val)` sends a value, `None` signals end-of-stream.
|
|
166
|
+
pub(crate) fn from_channel(rx: Receiver<Option<SendableValue>>, cancel_tx: Sender<()>) -> Self {
|
|
167
|
+
let (value_tx, value_rx) = unbounded();
|
|
168
|
+
|
|
169
|
+
let handle = thread::spawn(move || {
|
|
170
|
+
while let Ok(item) = rx.recv() {
|
|
171
|
+
match item {
|
|
172
|
+
Some(val) => {
|
|
173
|
+
if value_tx.send(StreamItem::Value(val)).is_err() {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
None => {
|
|
178
|
+
value_tx.send(StreamItem::End).ok();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Sender dropped without None — treat as end
|
|
184
|
+
value_tx.send(StreamItem::End).ok();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
Self {
|
|
188
|
+
receiver: Some(value_rx),
|
|
189
|
+
cancel_sender: cancel_tx,
|
|
190
|
+
handle: Some(handle),
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
impl Iterator for AsyncStream {
|
|
196
|
+
type Item = Result<Value, magnus::Error>;
|
|
197
|
+
fn next(&mut self) -> Option<Self::Item> {
|
|
198
|
+
let rx = self.receiver.as_ref()?;
|
|
199
|
+
match rx.recv() {
|
|
200
|
+
Ok(StreamItem::Value(v)) => Some(v.try_into()),
|
|
201
|
+
Ok(StreamItem::Error(e)) => Some(Err(magnus::Error::new(runtime_error(), e))),
|
|
202
|
+
Ok(StreamItem::End) | Err(_) => None,
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#[cfg(test)]
|
|
208
|
+
mod tests {
|
|
209
|
+
use super::*;
|
|
210
|
+
use crate::test_helpers::{skip_if_no_python, with_ruby_python};
|
|
211
|
+
use crossbeam_channel::bounded;
|
|
212
|
+
use magnus::TryConvert;
|
|
213
|
+
use serial_test::serial;
|
|
214
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
215
|
+
use std::sync::Arc;
|
|
216
|
+
use std::time::{Duration, Instant};
|
|
217
|
+
|
|
218
|
+
// ========== Iterator: basic value delivery ==========
|
|
219
|
+
|
|
220
|
+
#[test]
|
|
221
|
+
#[serial]
|
|
222
|
+
fn test_iterator_delivers_single_value() {
|
|
223
|
+
with_ruby_python(|_ruby, _api| {
|
|
224
|
+
let (tx, rx) = unbounded();
|
|
225
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
226
|
+
|
|
227
|
+
thread::spawn(move || {
|
|
228
|
+
tx.send(Some(SendableValue::Integer(42))).ok();
|
|
229
|
+
tx.send(None).ok();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
let mut stream = AsyncStream::from_channel(rx, cancel_tx);
|
|
233
|
+
let val = stream.next().unwrap().unwrap();
|
|
234
|
+
assert_eq!(i64::try_convert(val).unwrap(), 42);
|
|
235
|
+
assert!(stream.next().is_none());
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
#[test]
|
|
240
|
+
#[serial]
|
|
241
|
+
fn test_iterator_delivers_multiple_values_in_order() {
|
|
242
|
+
with_ruby_python(|_ruby, _api| {
|
|
243
|
+
let (tx, rx) = unbounded();
|
|
244
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
245
|
+
|
|
246
|
+
thread::spawn(move || {
|
|
247
|
+
for i in 0..5 {
|
|
248
|
+
tx.send(Some(SendableValue::Integer(i))).ok();
|
|
249
|
+
}
|
|
250
|
+
tx.send(None).ok();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
let mut stream = AsyncStream::from_channel(rx, cancel_tx);
|
|
254
|
+
for expected in 0..5 {
|
|
255
|
+
let val = stream.next().unwrap().unwrap();
|
|
256
|
+
assert_eq!(i64::try_convert(val).unwrap(), expected);
|
|
257
|
+
}
|
|
258
|
+
assert!(stream.next().is_none());
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
#[test]
|
|
263
|
+
#[serial]
|
|
264
|
+
fn test_iterator_empty_stream() {
|
|
265
|
+
with_ruby_python(|_ruby, _api| {
|
|
266
|
+
let (tx, rx) = unbounded();
|
|
267
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
268
|
+
|
|
269
|
+
thread::spawn(move || {
|
|
270
|
+
tx.send(None).ok();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
let mut stream = AsyncStream::from_channel(rx, cancel_tx);
|
|
274
|
+
assert!(stream.next().is_none());
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
#[test]
|
|
279
|
+
#[serial]
|
|
280
|
+
fn test_iterator_propagates_error() {
|
|
281
|
+
with_ruby_python(|_ruby, _api| {
|
|
282
|
+
let (value_tx, value_rx) = unbounded();
|
|
283
|
+
|
|
284
|
+
// Manually build the stream to inject an error StreamItem
|
|
285
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
286
|
+
let handle = thread::spawn(move || {
|
|
287
|
+
value_tx
|
|
288
|
+
.send(StreamItem::Value(SendableValue::Integer(1)))
|
|
289
|
+
.ok();
|
|
290
|
+
value_tx
|
|
291
|
+
.send(StreamItem::Error("something went wrong".to_string()))
|
|
292
|
+
.ok();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
let mut stream = AsyncStream {
|
|
296
|
+
receiver: Some(value_rx),
|
|
297
|
+
cancel_sender: cancel_tx,
|
|
298
|
+
handle: Some(handle),
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// First item succeeds
|
|
302
|
+
let val = stream.next().unwrap().unwrap();
|
|
303
|
+
assert_eq!(i64::try_convert(val).unwrap(), 1);
|
|
304
|
+
|
|
305
|
+
// Second item is an error
|
|
306
|
+
let err = stream.next().unwrap().unwrap_err();
|
|
307
|
+
assert!(err.to_string().contains("something went wrong"));
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
#[test]
|
|
312
|
+
#[serial]
|
|
313
|
+
fn test_iterator_end_then_none() {
|
|
314
|
+
with_ruby_python(|_ruby, _api| {
|
|
315
|
+
let (value_tx, value_rx) = unbounded();
|
|
316
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
317
|
+
|
|
318
|
+
let handle = thread::spawn(move || {
|
|
319
|
+
value_tx.send(StreamItem::End).ok();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
let mut stream = AsyncStream {
|
|
323
|
+
receiver: Some(value_rx),
|
|
324
|
+
cancel_sender: cancel_tx,
|
|
325
|
+
handle: Some(handle),
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
assert!(stream.next().is_none());
|
|
329
|
+
// Calling next after End should also return None (channel closed)
|
|
330
|
+
assert!(stream.next().is_none());
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
#[test]
|
|
335
|
+
#[serial]
|
|
336
|
+
fn test_iterator_channel_closed_returns_none() {
|
|
337
|
+
with_ruby_python(|_ruby, _api| {
|
|
338
|
+
let (value_tx, value_rx) = bounded::<StreamItem>(16);
|
|
339
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
340
|
+
|
|
341
|
+
// Drop sender immediately — simulates producer crash
|
|
342
|
+
drop(value_tx);
|
|
343
|
+
|
|
344
|
+
let mut stream = AsyncStream {
|
|
345
|
+
receiver: Some(value_rx),
|
|
346
|
+
cancel_sender: cancel_tx,
|
|
347
|
+
handle: None,
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
assert!(stream.next().is_none());
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ========== Iterator: type handling ==========
|
|
355
|
+
|
|
356
|
+
#[test]
|
|
357
|
+
#[serial]
|
|
358
|
+
fn test_iterator_all_sendable_types() {
|
|
359
|
+
with_ruby_python(|_ruby, _api| {
|
|
360
|
+
let (tx, rx) = unbounded();
|
|
361
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
362
|
+
|
|
363
|
+
thread::spawn(move || {
|
|
364
|
+
tx.send(Some(SendableValue::Nil)).ok();
|
|
365
|
+
tx.send(Some(SendableValue::Integer(99))).ok();
|
|
366
|
+
tx.send(Some(SendableValue::Float(1.5))).ok();
|
|
367
|
+
tx.send(Some(SendableValue::Str("test".to_string()))).ok();
|
|
368
|
+
tx.send(Some(SendableValue::Bool(false))).ok();
|
|
369
|
+
tx.send(Some(SendableValue::List(vec![SendableValue::Integer(1)])))
|
|
370
|
+
.ok();
|
|
371
|
+
tx.send(Some(SendableValue::Dict(vec![(
|
|
372
|
+
SendableValue::Str("k".to_string()),
|
|
373
|
+
SendableValue::Integer(2),
|
|
374
|
+
)])))
|
|
375
|
+
.ok();
|
|
376
|
+
tx.send(None).ok();
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
let mut stream = AsyncStream::from_channel(rx, cancel_tx);
|
|
380
|
+
let mut results = Vec::new();
|
|
381
|
+
while let Some(Ok(val)) = stream.next() {
|
|
382
|
+
results.push(val);
|
|
383
|
+
}
|
|
384
|
+
assert_eq!(results.len(), 7);
|
|
385
|
+
|
|
386
|
+
assert!(results[0].is_nil());
|
|
387
|
+
assert_eq!(i64::try_convert(results[1]).unwrap(), 99);
|
|
388
|
+
assert!((f64::try_convert(results[2]).unwrap() - 1.5).abs() < 1e-9);
|
|
389
|
+
assert_eq!(String::try_convert(results[3]).unwrap(), "test");
|
|
390
|
+
assert!(!bool::try_convert(results[4]).unwrap());
|
|
391
|
+
|
|
392
|
+
let arr = magnus::RArray::try_convert(results[5]).unwrap();
|
|
393
|
+
assert_eq!(arr.len(), 1);
|
|
394
|
+
|
|
395
|
+
let hash = magnus::RHash::try_convert(results[6]).unwrap();
|
|
396
|
+
let v: i64 = hash.fetch("k").unwrap();
|
|
397
|
+
assert_eq!(v, 2);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ========== Drop: cancellation signal ==========
|
|
402
|
+
|
|
403
|
+
#[test]
|
|
404
|
+
#[serial]
|
|
405
|
+
fn test_drop_sends_cancel_signal() {
|
|
406
|
+
let Some(_guard) = skip_if_no_python() else {
|
|
407
|
+
return;
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
let (tx, rx) = unbounded();
|
|
411
|
+
let (cancel_tx, cancel_rx) = bounded(1);
|
|
412
|
+
|
|
413
|
+
thread::spawn(move || {
|
|
414
|
+
tx.send(Some(SendableValue::Integer(1))).ok();
|
|
415
|
+
// Don't send None — stream stays open
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
let stream = AsyncStream::from_channel(rx, cancel_tx);
|
|
419
|
+
drop(stream);
|
|
420
|
+
|
|
421
|
+
// The cancel channel should have received a signal
|
|
422
|
+
// (or be disconnected because cancel_sender was dropped after sending)
|
|
423
|
+
// Either way, the cancel_rx side should have gotten the message
|
|
424
|
+
assert!(
|
|
425
|
+
cancel_rx.try_recv().is_ok() || cancel_rx.try_recv().is_err(),
|
|
426
|
+
"cancel signal should have been sent before drop completed"
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
#[test]
|
|
431
|
+
#[serial]
|
|
432
|
+
fn test_drop_joins_worker_thread() {
|
|
433
|
+
let Some(_guard) = skip_if_no_python() else {
|
|
434
|
+
return;
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
let thread_finished = Arc::new(AtomicBool::new(false));
|
|
438
|
+
let thread_finished_clone = thread_finished.clone();
|
|
439
|
+
|
|
440
|
+
let (value_tx, value_rx) = unbounded();
|
|
441
|
+
let (cancel_tx, cancel_rx) = bounded(1);
|
|
442
|
+
|
|
443
|
+
let handle = thread::spawn(move || {
|
|
444
|
+
// Wait for cancel signal or a short timeout
|
|
445
|
+
let _ = cancel_rx.recv_timeout(Duration::from_secs(5));
|
|
446
|
+
value_tx.send(StreamItem::End).ok();
|
|
447
|
+
thread_finished_clone.store(true, Ordering::SeqCst);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
let stream = AsyncStream {
|
|
451
|
+
receiver: Some(value_rx),
|
|
452
|
+
cancel_sender: cancel_tx,
|
|
453
|
+
handle: Some(handle),
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
// Drop should send cancel, drain, and join
|
|
457
|
+
drop(stream);
|
|
458
|
+
|
|
459
|
+
// After drop returns, the thread must have finished
|
|
460
|
+
assert!(
|
|
461
|
+
thread_finished.load(Ordering::SeqCst),
|
|
462
|
+
"worker thread should have been joined by Drop"
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
#[test]
|
|
467
|
+
#[serial]
|
|
468
|
+
fn test_drop_unblocks_producer_via_drain() {
|
|
469
|
+
let Some(_guard) = skip_if_no_python() else {
|
|
470
|
+
return;
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
let (value_tx, value_rx) = bounded(1); // tiny buffer
|
|
474
|
+
let (cancel_tx, cancel_rx) = bounded(1);
|
|
475
|
+
|
|
476
|
+
let producer_done = Arc::new(AtomicBool::new(false));
|
|
477
|
+
let producer_done_clone = producer_done.clone();
|
|
478
|
+
|
|
479
|
+
let handle = thread::spawn(move || {
|
|
480
|
+
// Fill the buffer
|
|
481
|
+
let _ = value_tx.send(StreamItem::Value(SendableValue::Integer(1)));
|
|
482
|
+
// This will block because buffer is full
|
|
483
|
+
// Drop's drain + cancel should unblock us
|
|
484
|
+
loop {
|
|
485
|
+
crossbeam_channel::select! {
|
|
486
|
+
send(value_tx, StreamItem::Value(SendableValue::Integer(2))) -> res => {
|
|
487
|
+
if res.is_err() { break; }
|
|
488
|
+
}
|
|
489
|
+
recv(cancel_rx) -> _ => {
|
|
490
|
+
break;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
producer_done_clone.store(true, Ordering::SeqCst);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
let stream = AsyncStream {
|
|
498
|
+
receiver: Some(value_rx),
|
|
499
|
+
cancel_sender: cancel_tx,
|
|
500
|
+
handle: Some(handle),
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
let start = Instant::now();
|
|
504
|
+
drop(stream);
|
|
505
|
+
let elapsed = start.elapsed();
|
|
506
|
+
|
|
507
|
+
assert!(
|
|
508
|
+
producer_done.load(Ordering::SeqCst),
|
|
509
|
+
"producer should have finished after drop"
|
|
510
|
+
);
|
|
511
|
+
assert!(
|
|
512
|
+
elapsed < Duration::from_secs(2),
|
|
513
|
+
"drop should not hang — took {:?}",
|
|
514
|
+
elapsed
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ========== Drop: mid-iteration ==========
|
|
519
|
+
|
|
520
|
+
#[test]
|
|
521
|
+
#[serial]
|
|
522
|
+
fn test_drop_mid_iteration_does_not_hang() {
|
|
523
|
+
with_ruby_python(|_ruby, _api| {
|
|
524
|
+
let (tx, rx) = unbounded();
|
|
525
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
526
|
+
|
|
527
|
+
thread::spawn(move || {
|
|
528
|
+
for i in 0..100 {
|
|
529
|
+
if tx.send(Some(SendableValue::Integer(i))).is_err() {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
tx.send(None).ok();
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
let mut stream = AsyncStream::from_channel(rx, cancel_tx);
|
|
537
|
+
|
|
538
|
+
// Read only a few items, then drop
|
|
539
|
+
let _ = stream.next();
|
|
540
|
+
let _ = stream.next();
|
|
541
|
+
|
|
542
|
+
let start = Instant::now();
|
|
543
|
+
drop(stream);
|
|
544
|
+
let elapsed = start.elapsed();
|
|
545
|
+
|
|
546
|
+
assert!(
|
|
547
|
+
elapsed < Duration::from_secs(2),
|
|
548
|
+
"dropping mid-iteration should not hang — took {:?}",
|
|
549
|
+
elapsed
|
|
550
|
+
);
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
#[test]
|
|
555
|
+
#[serial]
|
|
556
|
+
fn test_drop_without_reading_any_items() {
|
|
557
|
+
let Some(_guard) = skip_if_no_python() else {
|
|
558
|
+
return;
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
let (tx, rx) = unbounded();
|
|
562
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
563
|
+
|
|
564
|
+
let producer_done = Arc::new(AtomicBool::new(false));
|
|
565
|
+
let producer_done_clone = producer_done.clone();
|
|
566
|
+
|
|
567
|
+
thread::spawn(move || {
|
|
568
|
+
for i in 0..10 {
|
|
569
|
+
if tx.send(Some(SendableValue::Integer(i))).is_err() {
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
tx.send(None).ok();
|
|
574
|
+
producer_done_clone.store(true, Ordering::SeqCst);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
let stream = AsyncStream::from_channel(rx, cancel_tx);
|
|
578
|
+
|
|
579
|
+
let start = Instant::now();
|
|
580
|
+
drop(stream); // Never called .next()
|
|
581
|
+
let elapsed = start.elapsed();
|
|
582
|
+
|
|
583
|
+
assert!(
|
|
584
|
+
elapsed < Duration::from_secs(2),
|
|
585
|
+
"dropping without reading should not hang — took {:?}",
|
|
586
|
+
elapsed
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ========== Drop: handle is None ==========
|
|
591
|
+
|
|
592
|
+
#[test]
|
|
593
|
+
#[serial]
|
|
594
|
+
fn test_drop_with_no_handle() {
|
|
595
|
+
let Some(_guard) = skip_if_no_python() else {
|
|
596
|
+
return;
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
let (_value_tx, value_rx) = bounded::<StreamItem>(16);
|
|
600
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
601
|
+
|
|
602
|
+
// handle: None — simulates already-joined or no thread
|
|
603
|
+
let stream = AsyncStream {
|
|
604
|
+
receiver: Some(value_rx),
|
|
605
|
+
cancel_sender: cancel_tx,
|
|
606
|
+
handle: None,
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// Should not panic
|
|
610
|
+
drop(stream);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ========== from_channel: producer sends then drops ==========
|
|
614
|
+
|
|
615
|
+
#[test]
|
|
616
|
+
#[serial]
|
|
617
|
+
fn test_from_channel_producer_drops_without_none() {
|
|
618
|
+
with_ruby_python(|_ruby, _api| {
|
|
619
|
+
let (tx, rx) = unbounded();
|
|
620
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
621
|
+
|
|
622
|
+
thread::spawn(move || {
|
|
623
|
+
tx.send(Some(SendableValue::Integer(1))).ok();
|
|
624
|
+
tx.send(Some(SendableValue::Integer(2))).ok();
|
|
625
|
+
drop(tx); // Drop without sending None
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
let mut stream = AsyncStream::from_channel(rx, cancel_tx);
|
|
629
|
+
|
|
630
|
+
let v1 = stream.next().unwrap().unwrap();
|
|
631
|
+
assert_eq!(i64::try_convert(v1).unwrap(), 1);
|
|
632
|
+
|
|
633
|
+
let v2 = stream.next().unwrap().unwrap();
|
|
634
|
+
assert_eq!(i64::try_convert(v2).unwrap(), 2);
|
|
635
|
+
|
|
636
|
+
// from_channel sends End when producer drops
|
|
637
|
+
assert!(stream.next().is_none());
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// ========== Backpressure ==========
|
|
642
|
+
|
|
643
|
+
#[test]
|
|
644
|
+
#[serial]
|
|
645
|
+
fn test_backpressure_with_slow_consumer() {
|
|
646
|
+
with_ruby_python(|_ruby, _api| {
|
|
647
|
+
let (tx, rx) = bounded(2); // very small buffer
|
|
648
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
649
|
+
|
|
650
|
+
let items_sent = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
|
651
|
+
let items_sent_clone = items_sent.clone();
|
|
652
|
+
|
|
653
|
+
thread::spawn(move || {
|
|
654
|
+
for i in 0..20 {
|
|
655
|
+
if tx.send(Some(SendableValue::Integer(i))).is_err() {
|
|
656
|
+
break;
|
|
657
|
+
}
|
|
658
|
+
items_sent_clone.fetch_add(1, Ordering::SeqCst);
|
|
659
|
+
}
|
|
660
|
+
tx.send(None).ok();
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
// Give producer time to fill buffer
|
|
664
|
+
thread::sleep(Duration::from_millis(50));
|
|
665
|
+
|
|
666
|
+
// Producer should be blocked after filling the internal buffer
|
|
667
|
+
// (2 items in tx→rx + 16 in internal value channel)
|
|
668
|
+
let sent_before_read = items_sent.load(Ordering::SeqCst);
|
|
669
|
+
assert!(
|
|
670
|
+
sent_before_read <= 20, // bounded by buffer sizes
|
|
671
|
+
"producer should be bounded by channel capacity"
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
// Now consume everything
|
|
675
|
+
let mut stream = AsyncStream::from_channel(rx, cancel_tx);
|
|
676
|
+
let mut count = 0;
|
|
677
|
+
while let Some(Ok(_)) = stream.next() {
|
|
678
|
+
count += 1;
|
|
679
|
+
}
|
|
680
|
+
assert_eq!(count, 20);
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// ========== Large stream ==========
|
|
685
|
+
|
|
686
|
+
#[test]
|
|
687
|
+
#[serial]
|
|
688
|
+
fn test_large_stream_1000_items() {
|
|
689
|
+
with_ruby_python(|_ruby, _api| {
|
|
690
|
+
let (tx, rx) = unbounded();
|
|
691
|
+
let (cancel_tx, _cancel_rx) = bounded(1);
|
|
692
|
+
|
|
693
|
+
thread::spawn(move || {
|
|
694
|
+
for i in 0..1000 {
|
|
695
|
+
if tx.send(Some(SendableValue::Integer(i))).is_err() {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
tx.send(None).ok();
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
let mut stream = AsyncStream::from_channel(rx, cancel_tx);
|
|
703
|
+
let mut count = 0i64;
|
|
704
|
+
let mut sum = 0i64;
|
|
705
|
+
while let Some(Ok(val)) = stream.next() {
|
|
706
|
+
sum += i64::try_convert(val).unwrap();
|
|
707
|
+
count += 1;
|
|
708
|
+
}
|
|
709
|
+
assert_eq!(count, 1000);
|
|
710
|
+
assert_eq!(sum, (0..1000i64).sum::<i64>());
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|