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,232 @@
|
|
|
1
|
+
use std::os::fd::RawFd;
|
|
2
|
+
|
|
3
|
+
/// A pipe that have a pair of descriptors for reading and writing.
|
|
4
|
+
pub struct PipeNotify {
|
|
5
|
+
read_fd: RawFd,
|
|
6
|
+
write_fd: RawFd,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
impl PipeNotify {
|
|
10
|
+
pub fn new() -> std::io::Result<Self> {
|
|
11
|
+
let mut fds = [0 as RawFd; 2];
|
|
12
|
+
let ret = unsafe { libc::pipe(fds.as_mut_ptr()) };
|
|
13
|
+
if ret != 0 {
|
|
14
|
+
return Err(std::io::Error::last_os_error());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Set the pipe to non-blocking mode.
|
|
18
|
+
unsafe {
|
|
19
|
+
libc::fcntl(fds[0], libc::F_SETFL, libc::O_NONBLOCK);
|
|
20
|
+
libc::fcntl(fds[1], libc::F_SETFL, libc::O_NONBLOCK);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
Ok(Self {
|
|
24
|
+
read_fd: fds[0],
|
|
25
|
+
write_fd: fds[1],
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
/// Write a notification byte. Called by the producer after channel.send().
|
|
29
|
+
pub fn notify(&self) {
|
|
30
|
+
let buf: [u8; 1] = [1];
|
|
31
|
+
unsafe {
|
|
32
|
+
// Ignore errors — if the pipe is full, the consumer will drain it
|
|
33
|
+
libc::write(self.write_fd, buf.as_ptr() as *const libc::c_void, 1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/// Drain all notification bytes.
|
|
37
|
+
pub fn drain(&self) {
|
|
38
|
+
let mut buf = [0u8; 64];
|
|
39
|
+
loop {
|
|
40
|
+
let n = unsafe {
|
|
41
|
+
libc::read(
|
|
42
|
+
self.read_fd,
|
|
43
|
+
buf.as_mut_ptr() as *mut libc::c_void,
|
|
44
|
+
buf.len(),
|
|
45
|
+
)
|
|
46
|
+
};
|
|
47
|
+
if n <= 0 {
|
|
48
|
+
// EAGAIN (non-blocking) or error
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/// Get the read fd for IO.select.
|
|
54
|
+
pub fn read_fd(&self) -> RawFd {
|
|
55
|
+
self.read_fd
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
unsafe impl Send for PipeNotify {}
|
|
59
|
+
unsafe impl Sync for PipeNotify {}
|
|
60
|
+
impl Drop for PipeNotify {
|
|
61
|
+
fn drop(&mut self) {
|
|
62
|
+
unsafe {
|
|
63
|
+
libc::close(self.read_fd);
|
|
64
|
+
libc::close(self.write_fd);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[cfg(test)]
|
|
70
|
+
mod tests {
|
|
71
|
+
use super::*;
|
|
72
|
+
use serial_test::serial;
|
|
73
|
+
use std::sync::Arc;
|
|
74
|
+
|
|
75
|
+
#[test]
|
|
76
|
+
fn test_new_creates_valid_pipe() {
|
|
77
|
+
let pipe = PipeNotify::new().expect("pipe creation should succeed");
|
|
78
|
+
assert!(pipe.read_fd >= 0, "read_fd should be valid");
|
|
79
|
+
assert!(pipe.write_fd >= 0, "write_fd should be valid");
|
|
80
|
+
assert_ne!(
|
|
81
|
+
pipe.read_fd, pipe.write_fd,
|
|
82
|
+
"read and write fds should differ"
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[test]
|
|
87
|
+
fn test_notify_then_drain() {
|
|
88
|
+
let pipe = PipeNotify::new().unwrap();
|
|
89
|
+
pipe.notify();
|
|
90
|
+
pipe.drain();
|
|
91
|
+
// Drain again — should be a no-op (pipe is empty)
|
|
92
|
+
pipe.drain();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
#[test]
|
|
96
|
+
fn test_drain_on_empty_pipe_does_not_block() {
|
|
97
|
+
let pipe = PipeNotify::new().unwrap();
|
|
98
|
+
let start = std::time::Instant::now();
|
|
99
|
+
pipe.drain();
|
|
100
|
+
let elapsed = start.elapsed();
|
|
101
|
+
assert!(
|
|
102
|
+
elapsed < std::time::Duration::from_millis(10),
|
|
103
|
+
"drain on empty pipe should return instantly, took {elapsed:?}"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#[test]
|
|
108
|
+
fn test_multiple_notifies_single_drain() {
|
|
109
|
+
let pipe = PipeNotify::new().unwrap();
|
|
110
|
+
for _ in 0..100 {
|
|
111
|
+
pipe.notify();
|
|
112
|
+
}
|
|
113
|
+
pipe.drain();
|
|
114
|
+
// After drain, pipe should be empty — another drain is instant
|
|
115
|
+
let start = std::time::Instant::now();
|
|
116
|
+
pipe.drain();
|
|
117
|
+
assert!(start.elapsed() < std::time::Duration::from_millis(10));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#[test]
|
|
121
|
+
fn test_notify_drain_interleaved() {
|
|
122
|
+
let pipe = PipeNotify::new().unwrap();
|
|
123
|
+
for _ in 0..50 {
|
|
124
|
+
pipe.notify();
|
|
125
|
+
pipe.drain();
|
|
126
|
+
}
|
|
127
|
+
// Pipe should be empty at the end
|
|
128
|
+
pipe.drain();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#[test]
|
|
132
|
+
fn test_read_fd_is_stable() {
|
|
133
|
+
let pipe = PipeNotify::new().unwrap();
|
|
134
|
+
let fd1 = pipe.read_fd();
|
|
135
|
+
let fd2 = pipe.read_fd();
|
|
136
|
+
assert_eq!(fd1, fd2, "read_fd should return the same value");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#[test]
|
|
140
|
+
fn test_no_byte_accumulation_with_channel() {
|
|
141
|
+
// Simulates the real producer/consumer pattern:
|
|
142
|
+
// send item through channel + notify, then drain + try_recv
|
|
143
|
+
use crossbeam_channel::bounded;
|
|
144
|
+
|
|
145
|
+
let pipe = Arc::new(PipeNotify::new().unwrap());
|
|
146
|
+
let (tx, rx) = bounded::<i64>(16);
|
|
147
|
+
|
|
148
|
+
let producer_pipe = pipe.clone();
|
|
149
|
+
let producer = std::thread::spawn(move || {
|
|
150
|
+
for i in 0..100 {
|
|
151
|
+
tx.send(i).unwrap();
|
|
152
|
+
producer_pipe.notify();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
let mut items = Vec::with_capacity(100);
|
|
157
|
+
for _ in 0..100 {
|
|
158
|
+
pipe.drain();
|
|
159
|
+
let item = rx
|
|
160
|
+
.recv_timeout(std::time::Duration::from_secs(1))
|
|
161
|
+
.expect("producer should make progress while consumer drains");
|
|
162
|
+
items.push(item);
|
|
163
|
+
}
|
|
164
|
+
producer.join().unwrap();
|
|
165
|
+
|
|
166
|
+
assert_eq!(items, (0..100).collect::<Vec<_>>());
|
|
167
|
+
|
|
168
|
+
// Pipe should be fully drained — no leftover bytes
|
|
169
|
+
pipe.drain(); // should be no-op
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
#[test]
|
|
173
|
+
fn test_cross_thread_notify_drain() {
|
|
174
|
+
let pipe = Arc::new(PipeNotify::new().unwrap());
|
|
175
|
+
let pipe_clone = pipe.clone();
|
|
176
|
+
|
|
177
|
+
let handle = std::thread::spawn(move || {
|
|
178
|
+
for _ in 0..50 {
|
|
179
|
+
pipe_clone.notify();
|
|
180
|
+
std::thread::sleep(std::time::Duration::from_millis(1));
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Drain periodically from the main thread
|
|
185
|
+
let mut drain_count = 0;
|
|
186
|
+
while !handle.is_finished() || drain_count < 5 {
|
|
187
|
+
pipe.drain();
|
|
188
|
+
drain_count += 1;
|
|
189
|
+
std::thread::sleep(std::time::Duration::from_millis(10));
|
|
190
|
+
}
|
|
191
|
+
handle.join().unwrap();
|
|
192
|
+
|
|
193
|
+
// Final drain to catch any stragglers
|
|
194
|
+
pipe.drain();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
#[test]
|
|
198
|
+
#[serial]
|
|
199
|
+
fn test_drop_closes_fds() {
|
|
200
|
+
// Note: This test is inherently racy — another thread can reuse the
|
|
201
|
+
// fd number between drop and fcntl check. We retry once to reduce
|
|
202
|
+
// flakiness, but the core behavior (Drop closes fds) is also verified
|
|
203
|
+
// by test_many_pipes_no_fd_leak which would fail on fd exhaustion.
|
|
204
|
+
let pipe = PipeNotify::new().unwrap();
|
|
205
|
+
let read_fd = pipe.read_fd;
|
|
206
|
+
let write_fd = pipe.write_fd;
|
|
207
|
+
drop(pipe);
|
|
208
|
+
|
|
209
|
+
let r = unsafe { libc::fcntl(read_fd, libc::F_GETFD) };
|
|
210
|
+
let w = unsafe { libc::fcntl(write_fd, libc::F_GETFD) };
|
|
211
|
+
|
|
212
|
+
// If either fd was reused by another thread, skip rather than fail
|
|
213
|
+
if r == 0 || w == 0 {
|
|
214
|
+
// Fd was reused — can't reliably test. The 500-pipe leak test
|
|
215
|
+
// covers this behavior more reliably.
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
assert_eq!(r, -1, "read_fd should be closed after drop");
|
|
219
|
+
assert_eq!(w, -1, "write_fd should be closed after drop");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
#[test]
|
|
223
|
+
fn test_many_pipes_no_fd_leak() {
|
|
224
|
+
// Create and drop many pipes — should not exhaust fds
|
|
225
|
+
for _ in 0..500 {
|
|
226
|
+
let pipe = PipeNotify::new().expect("should not run out of fds");
|
|
227
|
+
pipe.notify();
|
|
228
|
+
pipe.drain();
|
|
229
|
+
drop(pipe);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from asyncio import AbstractEventLoop
|
|
3
|
+
from typing import AsyncGenerator
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AsyncToSync:
|
|
7
|
+
def __init__(self, async_gen: AsyncGenerator):
|
|
8
|
+
self._agen = async_gen
|
|
9
|
+
self._loop: AbstractEventLoop = asyncio.new_event_loop()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def __iter__(self):
|
|
13
|
+
return self
|
|
14
|
+
|
|
15
|
+
def __next__(self):
|
|
16
|
+
try:
|
|
17
|
+
coro = self._agen.__anext__()
|
|
18
|
+
return self._loop.run_until_complete(coro)
|
|
19
|
+
except StopAsyncIteration:
|
|
20
|
+
self._loop.close()
|
|
21
|
+
raise StopIteration
|
|
22
|
+
except BaseException:
|
|
23
|
+
self._loop.close()
|
|
24
|
+
raise
|
|
25
|
+
|
|
26
|
+
def close(self):
|
|
27
|
+
try:
|
|
28
|
+
self._loop.run_until_complete(self._agen.aclose())
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
31
|
+
self._loop.close()
|