itsi-scheduler 0.1.0 → 0.1.2

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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +219 -23
  3. data/Rakefile +7 -1
  4. data/ext/itsi_error/Cargo.toml +2 -0
  5. data/ext/itsi_error/src/from.rs +70 -0
  6. data/ext/itsi_error/src/lib.rs +10 -37
  7. data/ext/itsi_instrument_entry/Cargo.toml +15 -0
  8. data/ext/itsi_instrument_entry/src/lib.rs +31 -0
  9. data/ext/itsi_rb_helpers/Cargo.toml +2 -0
  10. data/ext/itsi_rb_helpers/src/heap_value.rs +121 -0
  11. data/ext/itsi_rb_helpers/src/lib.rs +90 -10
  12. data/ext/itsi_scheduler/Cargo.toml +9 -1
  13. data/ext/itsi_scheduler/extconf.rb +1 -1
  14. data/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
  15. data/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
  16. data/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
  17. data/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
  18. data/ext/itsi_scheduler/src/lib.rs +31 -10
  19. data/ext/itsi_server/Cargo.toml +41 -0
  20. data/ext/itsi_server/extconf.rb +6 -0
  21. data/ext/itsi_server/src/body_proxy/big_bytes.rs +104 -0
  22. data/ext/itsi_server/src/body_proxy/itsi_body_proxy.rs +122 -0
  23. data/ext/itsi_server/src/body_proxy/mod.rs +2 -0
  24. data/ext/itsi_server/src/lib.rs +103 -0
  25. data/ext/itsi_server/src/request/itsi_request.rs +277 -0
  26. data/ext/itsi_server/src/request/mod.rs +1 -0
  27. data/ext/itsi_server/src/response/itsi_response.rs +347 -0
  28. data/ext/itsi_server/src/response/mod.rs +1 -0
  29. data/ext/itsi_server/src/server/bind.rs +168 -0
  30. data/ext/itsi_server/src/server/bind_protocol.rs +37 -0
  31. data/ext/itsi_server/src/server/io_stream.rs +104 -0
  32. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +13 -0
  33. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +5 -0
  34. data/ext/itsi_server/src/server/itsi_server.rs +230 -0
  35. data/ext/itsi_server/src/server/lifecycle_event.rs +8 -0
  36. data/ext/itsi_server/src/server/listener.rs +259 -0
  37. data/ext/itsi_server/src/server/mod.rs +11 -0
  38. data/ext/itsi_server/src/server/process_worker.rs +196 -0
  39. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +253 -0
  40. data/ext/itsi_server/src/server/serve_strategy/mod.rs +27 -0
  41. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +238 -0
  42. data/ext/itsi_server/src/server/signal.rs +57 -0
  43. data/ext/itsi_server/src/server/thread_worker.rs +368 -0
  44. data/ext/itsi_server/src/server/tls.rs +152 -0
  45. data/ext/itsi_tracing/Cargo.toml +4 -0
  46. data/ext/itsi_tracing/src/lib.rs +36 -6
  47. data/lib/itsi/scheduler/version.rb +1 -1
  48. data/lib/itsi/scheduler.rb +137 -1
  49. metadata +38 -4
@@ -0,0 +1,308 @@
1
+ mod io_helpers;
2
+ mod io_waiter;
3
+ mod timer;
4
+ use io_helpers::{build_interest, poll_readiness, set_nonblocking};
5
+ use io_waiter::IoWaiter;
6
+ use itsi_error::ItsiError;
7
+ use itsi_rb_helpers::{call_without_gvl, create_ruby_thread};
8
+ use magnus::{
9
+ error::Result as MagnusResult,
10
+ value::{InnerValue, Opaque, ReprValue},
11
+ Module, RClass, Ruby, Value,
12
+ };
13
+ use mio::{Events, Poll, Token, Waker};
14
+ use parking_lot::{Mutex, RwLock};
15
+ use std::{
16
+ collections::{BinaryHeap, HashMap, VecDeque},
17
+ os::fd::RawFd,
18
+ sync::Arc,
19
+ time::Duration,
20
+ };
21
+ use timer::Timer;
22
+ use tracing::{debug, info, warn};
23
+
24
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
25
+ pub(crate) struct Readiness(i16);
26
+
27
+ impl std::fmt::Debug for ItsiScheduler {
28
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29
+ f.debug_struct("ItsiScheduler").finish()
30
+ }
31
+ }
32
+
33
+ const WAKE_TOKEN: Token = Token(0);
34
+
35
+ #[magnus::wrap(class = "Itsi::Scheduler", free_immediately, size)]
36
+ pub(crate) struct ItsiScheduler {
37
+ timers: Mutex<BinaryHeap<Timer>>,
38
+ io_waiters: Mutex<HashMap<Token, IoWaiter>>,
39
+ registry: Mutex<HashMap<RawFd, VecDeque<IoWaiter>>>,
40
+ poll: Mutex<Poll>,
41
+ events: Mutex<Events>,
42
+ waker: Mutex<Waker>,
43
+ }
44
+
45
+ impl Default for ItsiScheduler {
46
+ fn default() -> Self {
47
+ let poll = Poll::new().unwrap();
48
+ let waker = Waker::new(poll.registry(), WAKE_TOKEN).unwrap();
49
+ let events = Events::with_capacity(1024);
50
+
51
+ ItsiScheduler {
52
+ timers: Mutex::new(BinaryHeap::new()),
53
+ io_waiters: Mutex::new(HashMap::new()),
54
+ registry: Mutex::new(HashMap::new()),
55
+ poll: Mutex::new(poll),
56
+ events: Mutex::new(events),
57
+ waker: Mutex::new(waker),
58
+ }
59
+ }
60
+ }
61
+
62
+ impl ItsiScheduler {
63
+ pub fn initialize(&self) {}
64
+
65
+ pub fn wake(&self) -> MagnusResult<()> {
66
+ self.waker.lock().wake().map_err(|_| {
67
+ magnus::Error::new(
68
+ magnus::exception::exception(),
69
+ "Failed to wake the scheduler",
70
+ )
71
+ })?;
72
+ Ok(())
73
+ }
74
+ pub fn register_io_wait(
75
+ &self,
76
+ io_obj: i32,
77
+ events: i16,
78
+ timeout: Option<f64>,
79
+ token: usize,
80
+ ) -> MagnusResult<Option<i16>> {
81
+ debug!(
82
+ "Registering IO Wait for {:?}, {:?}, {:?}, {:?}",
83
+ io_obj, events, timeout, token
84
+ );
85
+ let fd: RawFd = io_obj;
86
+
87
+ let readiness = poll_readiness(fd, events).unwrap_or(Readiness(0));
88
+ if readiness == Readiness(events) {
89
+ return Ok(Some(readiness.0));
90
+ }
91
+
92
+ set_nonblocking(fd)?;
93
+ let interest = build_interest(events)?;
94
+ let token = Token(token);
95
+ let mut waiter = IoWaiter::new(fd, events, token);
96
+ self.io_waiters.lock().insert(token, waiter.clone());
97
+ let mut binding = self.registry.lock();
98
+ let queue = binding.entry(fd).or_default();
99
+
100
+ queue.push_back(waiter.clone());
101
+
102
+ if queue.len() == 1 {
103
+ self.poll
104
+ .lock()
105
+ .registry()
106
+ .register(&mut waiter, token, interest)
107
+ .map_err(|e| ItsiError::ArgumentError(format!("register error: {}", e)))?;
108
+ }
109
+ Ok(None)
110
+ }
111
+
112
+ pub fn start_timer(&self, timeout: Option<f64>, token: usize) {
113
+ if timeout.is_some_and(|t| t >= 0.0) {
114
+ let timer_entry = Timer::new(Duration::from_secs_f64(timeout.unwrap()), Token(token));
115
+ self.timers.lock().push(timer_entry);
116
+ }
117
+ }
118
+ pub fn has_pending_io(&self) -> bool {
119
+ !self.timers.lock().is_empty() || !self.io_waiters.lock().is_empty()
120
+ }
121
+
122
+ pub fn class_info(msg: String) {
123
+ info!(msg);
124
+ }
125
+
126
+ pub fn info(&self, msg: String) {
127
+ info!(msg);
128
+ }
129
+
130
+ pub fn warn(&self, msg: String) {
131
+ warn!(msg);
132
+ }
133
+
134
+ pub fn debug(&self, msg: String) {
135
+ debug!(msg);
136
+ }
137
+
138
+ pub fn fetch_due_events(&self) -> MagnusResult<Option<Vec<(usize, i16)>>> {
139
+ call_without_gvl(|| {
140
+ let timeout = if let Some(timer) = self.timers.lock().peek() {
141
+ timer.duration().or(Some(Duration::ZERO))
142
+ } else {
143
+ None
144
+ };
145
+ let mut due_fibers: Option<Vec<(usize, i16)>> = None;
146
+ let mut io_waiters = self.io_waiters.lock();
147
+ if !io_waiters.is_empty() || timeout.is_none() {
148
+ let mut events = self.events.lock();
149
+ {
150
+ let mut poll = self.poll.lock();
151
+ poll.poll(&mut events, timeout)
152
+ .map_err(|e| ItsiError::ArgumentError(format!("poll error: {}", e)))?;
153
+ };
154
+
155
+ for event in events.iter() {
156
+ let token = event.token();
157
+ if token == WAKE_TOKEN {
158
+ continue;
159
+ }
160
+
161
+ let waiter = io_waiters.remove(&token);
162
+ if waiter.is_none() {
163
+ continue;
164
+ }
165
+ let mut waiter = waiter.unwrap();
166
+ let mut evt_readiness = 0;
167
+ if event.is_readable() {
168
+ evt_readiness |= 1;
169
+ }
170
+ if event.is_priority() {
171
+ evt_readiness |= 2;
172
+ }
173
+ if event.is_writable() {
174
+ evt_readiness |= 4
175
+ }
176
+ self.poll
177
+ .lock()
178
+ .registry()
179
+ .deregister(&mut waiter)
180
+ .map_err(|_| {
181
+ ItsiError::ArgumentError("Failed to deregister".to_string())
182
+ })?;
183
+
184
+ due_fibers
185
+ .get_or_insert_default()
186
+ .push((waiter.token.0, evt_readiness));
187
+
188
+ let mut binding = self.registry.lock();
189
+ // Pop the current item for the current waiter off the queue
190
+ let queue = binding.get_mut(&(waiter.fd)).unwrap();
191
+ queue.pop_front();
192
+
193
+ if let Some(head) = queue.get_mut(0) {
194
+ // Register the next item in the queue if there is one.
195
+ let interest = build_interest(head.readiness)?;
196
+ self.poll
197
+ .lock()
198
+ .registry()
199
+ .register(head, head.token, interest)
200
+ .map_err(|_| {
201
+ ItsiError::ArgumentError("Failed to deregister".to_string())
202
+ })?;
203
+ } else {
204
+ // Otherwise we drop the queue altogether.
205
+ binding.remove(&waiter.fd);
206
+ }
207
+ }
208
+ return Ok(due_fibers);
209
+ }
210
+ Ok(None)
211
+ })
212
+ }
213
+
214
+ pub fn run_blocking_in_thread<T, F>(&self, ruby: &Ruby, work: F) -> MagnusResult<Option<T>>
215
+ where
216
+ T: Send + Sync + std::fmt::Debug + 'static,
217
+ F: FnOnce() -> Option<T> + Send + 'static,
218
+ {
219
+ let result: Arc<RwLock<Option<T>>> = Arc::new(RwLock::new(None));
220
+ let result_clone = Arc::clone(&result);
221
+
222
+ let current_fiber = Opaque::from(ruby.fiber_current());
223
+ let scheduler = Opaque::from(
224
+ ruby.module_kernel()
225
+ .const_get::<_, RClass>("Fiber")
226
+ .unwrap()
227
+ .funcall::<_, _, Value>("scheduler", ())
228
+ .unwrap(),
229
+ );
230
+
231
+ create_ruby_thread(move || {
232
+ call_without_gvl(|| {
233
+ let outcome = work();
234
+ *result_clone.write() = outcome;
235
+ });
236
+
237
+ let ruby = Ruby::get().unwrap();
238
+ scheduler
239
+ .get_inner_with(&ruby)
240
+ .funcall::<_, _, Value>("unblock", (None::<String>, current_fiber))
241
+ .unwrap();
242
+ });
243
+
244
+ scheduler
245
+ .get_inner_with(ruby)
246
+ .funcall::<_, _, Value>("block", (None::<Value>, None::<u64>))?;
247
+
248
+ let result_opt = Arc::try_unwrap(result).unwrap().write().take();
249
+ Ok(result_opt)
250
+ }
251
+
252
+ pub fn address_resolve(
253
+ ruby: &Ruby,
254
+ rself: &Self,
255
+ hostname: String,
256
+ ) -> MagnusResult<Option<Vec<String>>> {
257
+ let result: Option<Vec<String>> = rself.run_blocking_in_thread(ruby, move || {
258
+ use std::net::ToSocketAddrs;
259
+ let addrs_res = (hostname.as_str(), 0).to_socket_addrs();
260
+ match addrs_res {
261
+ Ok(addrs) => {
262
+ let ips: Vec<String> = addrs.map(|s| s.ip().to_string()).collect();
263
+ Some(ips)
264
+ }
265
+ Err(_) => None,
266
+ }
267
+ })?;
268
+ Ok(result)
269
+ }
270
+
271
+ pub fn fetch_due_timers(&self) -> MagnusResult<Option<Vec<usize>>> {
272
+ call_without_gvl(|| {
273
+ let mut timers = self.timers.lock();
274
+ let mut io_waiters = self.io_waiters.lock();
275
+ let mut due_fibers: Option<Vec<usize>> = None;
276
+ while let Some(timer) = timers.peek() {
277
+ if timer.is_due() {
278
+ due_fibers.get_or_insert_default().push(timer.token.0);
279
+ if let Some(waiter) = io_waiters.remove(&timer.token) {
280
+ let mut binding = self.registry.lock();
281
+ // Pop the current item for the current waiter off the queue
282
+ let queue = binding.get_mut(&waiter.fd).unwrap();
283
+ queue.pop_front();
284
+
285
+ if let Some(head) = queue.get_mut(0) {
286
+ // Register the next item in the queue if there is one.
287
+ let interest = build_interest(head.readiness)?;
288
+ self.poll
289
+ .lock()
290
+ .registry()
291
+ .register(head, head.token, interest)
292
+ .map_err(|_| {
293
+ ItsiError::ArgumentError("Failed to deregister".to_string())
294
+ })?;
295
+ } else {
296
+ // Otherwise we drop the queue altogether.
297
+ binding.remove(&waiter.fd);
298
+ }
299
+ }
300
+ timers.pop();
301
+ } else {
302
+ break;
303
+ }
304
+ }
305
+ Ok(due_fibers)
306
+ })
307
+ }
308
+ }
@@ -1,17 +1,38 @@
1
- use itsi_tracing::info;
2
- use magnus::{function, prelude::*, Error, Ruby};
3
-
4
- fn hello(subject: String) -> String {
5
- format!("Hello from Rust, {subject}!")
6
- }
1
+ use itsi_scheduler::ItsiScheduler;
2
+ use magnus::{function, method, Class, Error, Module, Object, Ruby};
3
+ mod itsi_scheduler;
7
4
 
8
5
  #[magnus::init]
9
6
  fn init(ruby: &Ruby) -> Result<(), Error> {
10
7
  itsi_tracing::init();
11
- info!("Initializing Itsi::Scheduler");
12
-
13
8
  let module = ruby.define_module("Itsi")?;
14
- let class = module.define_class("Scheduler", ruby.class_object())?;
15
- class.define_method("hello", function!(hello, 1))?;
9
+ let scheduler = module.define_class("Scheduler", ruby.class_object())?;
10
+ scheduler.define_singleton_method("info", function!(ItsiScheduler::class_info, 1))?;
11
+ scheduler.define_alloc_func::<ItsiScheduler>();
12
+ scheduler.define_method("initialize", method!(ItsiScheduler::initialize, 0))?;
13
+ scheduler.define_method("wake", method!(ItsiScheduler::wake, 0))?;
14
+ scheduler.define_method(
15
+ "register_io_wait",
16
+ method!(ItsiScheduler::register_io_wait, 4),
17
+ )?;
18
+ scheduler.define_method("info", method!(ItsiScheduler::info, 1))?;
19
+ scheduler.define_method("debug", method!(ItsiScheduler::debug, 1))?;
20
+ scheduler.define_method("warn", method!(ItsiScheduler::warn, 1))?;
21
+ scheduler.define_method("start_timer", method!(ItsiScheduler::start_timer, 2))?;
22
+ scheduler.define_method(
23
+ "address_resolve",
24
+ method!(ItsiScheduler::address_resolve, 1),
25
+ )?;
26
+ scheduler.define_method("has_pending_io?", method!(ItsiScheduler::has_pending_io, 0))?;
27
+
28
+ scheduler.define_method(
29
+ "fetch_due_timers",
30
+ method!(ItsiScheduler::fetch_due_timers, 0),
31
+ )?;
32
+ scheduler.define_method(
33
+ "fetch_due_events",
34
+ method!(ItsiScheduler::fetch_due_events, 0),
35
+ )?;
36
+
16
37
  Ok(())
17
38
  }
@@ -0,0 +1,41 @@
1
+ [package]
2
+ name = "itsi-server"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ authors = ["Wouter Coppieters <wc@pico.net.nz>"]
6
+ license = "MIT"
7
+ publish = false
8
+
9
+ [lib]
10
+ crate-type = ["cdylib"]
11
+
12
+ [dependencies]
13
+ magnus = { version = "0.7.1", features = ["bytes", "rb-sys"] }
14
+ itsi_tracing = { path = "../itsi_tracing" }
15
+ itsi_rb_helpers = { path = "../itsi_rb_helpers" }
16
+ itsi_error = { path = "../itsi_error" }
17
+ socket2 = "0.5.8"
18
+ parking_lot = "0.12.3"
19
+ rustls-pemfile = "2.2.0"
20
+ tokio-rustls = "0.23"
21
+ bytes = "1.3"
22
+ rcgen = { version = "0.13.2", features = ["x509-parser", "pem"] }
23
+ base64 = "0.22.1"
24
+ http-body-util = "0.1.2"
25
+ hyper = { version = "1.5.0", features = ["full", "server", "http1", "http2"] }
26
+ tokio = { version = "1", features = ["full"] }
27
+ hyper-util = { version = "0.1.10", features = ["full"] }
28
+ derive_more = { version = "2.0.1", features = ["debug"] }
29
+ http = "1.3.1"
30
+ crossbeam = "0.8.4"
31
+ futures = "0.3.31"
32
+ nix = { version = "0.29.0", features = ["socket", "uio", "signal"] }
33
+ pin-project = "1.1.9"
34
+ rb-sys = "0.9.111"
35
+ tracing = "0.1.41"
36
+ tokio-util = "0.7.13"
37
+ tokio-stream = "0.1.17"
38
+ httparse = "1.10.1"
39
+ async-channel = "2.3.1"
40
+ tempfile = "3.18.0"
41
+ sysinfo = "0.33.1"
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("server/itsi_server")
@@ -0,0 +1,104 @@
1
+ use bytes::Bytes;
2
+ use magnus::{IntoValue, Ruby, Value};
3
+ use std::io::{Result as IoResult, Write};
4
+ use std::path::PathBuf;
5
+ use tempfile::NamedTempFile;
6
+
7
+ const THRESHOLD: usize = 1024 * 1024; // 1 MB
8
+
9
+ /// An container that holds data in memory if it’s small, or in a temporary file on disk if it exceeds THRESHOLD.
10
+ /// Used for providing Rack input data.
11
+ pub enum BigBytes {
12
+ InMemory(Vec<u8>),
13
+ OnDisk(NamedTempFile),
14
+ }
15
+
16
+ /// The result type for reading the contents of a `BigBytes` value.
17
+ pub enum BigBytesReadResult {
18
+ /// When the data is stored in memory, returns the cached bytes.
19
+ InMemory(Vec<u8>),
20
+ /// When the data is stored on disk, returns the path to the temporary file.
21
+ OnDisk(PathBuf),
22
+ }
23
+
24
+ impl BigBytes {
25
+ /// Creates a new, empty BigBytes instance (initially in memory).
26
+ pub fn new() -> Self {
27
+ BigBytes::InMemory(Vec::new())
28
+ }
29
+
30
+ /// Reads the entire contents that have been written.
31
+ ///
32
+ /// - If stored in memory, returns a clone of the bytes.
33
+ /// - If stored on disk, returns the file path of the temporary file.
34
+ pub fn read(&self) -> IoResult<BigBytesReadResult> {
35
+ match self {
36
+ BigBytes::InMemory(vec) => Ok(BigBytesReadResult::InMemory(vec.clone())),
37
+ BigBytes::OnDisk(temp_file) => {
38
+ // Flush to be safe, then return the file path.
39
+ temp_file.as_file().sync_all()?;
40
+ Ok(BigBytesReadResult::OnDisk(temp_file.path().to_path_buf()))
41
+ }
42
+ }
43
+ }
44
+
45
+ pub fn as_value(&self) -> Value {
46
+ match self {
47
+ BigBytes::InMemory(bytes) => {
48
+ let bytes = Bytes::from(bytes.to_owned());
49
+ bytes.into_value()
50
+ }
51
+ BigBytes::OnDisk(path) => {
52
+ let ruby = Ruby::get().unwrap();
53
+ let rarray = ruby.ary_new();
54
+ rarray.push(path.path().to_str().unwrap().into_value()).ok();
55
+ rarray.into_value()
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ impl Drop for BigBytes {
62
+ fn drop(&mut self) {
63
+ match self {
64
+ BigBytes::InMemory(_) => {}
65
+ BigBytes::OnDisk(path) => {
66
+ let _ = std::fs::remove_file(path);
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ impl Default for BigBytes {
73
+ fn default() -> Self {
74
+ Self::new()
75
+ }
76
+ }
77
+
78
+ impl Write for BigBytes {
79
+ fn write(&mut self, buf: &[u8]) -> IoResult<usize> {
80
+ match self {
81
+ BigBytes::InMemory(vec) => {
82
+ // Check if writing the new bytes would exceed the threshold.
83
+ if vec.len() + buf.len() > THRESHOLD {
84
+ let mut tmp = NamedTempFile::new()?;
85
+ tmp.write_all(vec)?;
86
+ tmp.write_all(buf)?;
87
+ *self = BigBytes::OnDisk(tmp);
88
+ Ok(buf.len())
89
+ } else {
90
+ vec.extend_from_slice(buf);
91
+ Ok(buf.len())
92
+ }
93
+ }
94
+ BigBytes::OnDisk(tmp_file) => tmp_file.write(buf),
95
+ }
96
+ }
97
+
98
+ fn flush(&mut self) -> IoResult<()> {
99
+ match self {
100
+ BigBytes::InMemory(_) => Ok(()),
101
+ BigBytes::OnDisk(tmp_file) => tmp_file.flush(),
102
+ }
103
+ }
104
+ }
@@ -0,0 +1,122 @@
1
+ use super::big_bytes::BigBytes;
2
+ use bytes::Bytes;
3
+ use futures::executor::block_on;
4
+ use http_body_util::{BodyDataStream, BodyExt};
5
+ use hyper::body::Incoming;
6
+ use magnus::{error::Result as MagnusResult, scan_args, IntoValue, RString, Ruby, Value};
7
+ use parking_lot::Mutex;
8
+ use std::sync::{
9
+ atomic::{self, AtomicBool},
10
+ Arc,
11
+ };
12
+ use tokio_stream::StreamExt;
13
+
14
+ #[magnus::wrap(class = "Itsi::BodyProxy", free_immediately, size)]
15
+ #[derive(Debug, Clone)]
16
+ pub struct ItsiBodyProxy {
17
+ pub incoming: Arc<Mutex<BodyDataStream<Incoming>>>,
18
+ pub closed: Arc<AtomicBool>,
19
+ pub buf: Arc<Mutex<Vec<u8>>>,
20
+ }
21
+
22
+ pub enum ItsiBody {
23
+ Buffered(BigBytes),
24
+ Stream(ItsiBodyProxy),
25
+ }
26
+
27
+ impl ItsiBody {
28
+ pub fn into_value(&self) -> Value {
29
+ match self {
30
+ ItsiBody::Buffered(bytes) => bytes.as_value(),
31
+ ItsiBody::Stream(proxy) => proxy.clone().into_value(),
32
+ }
33
+ }
34
+ }
35
+ impl ItsiBodyProxy {
36
+ pub fn new(incoming: Incoming) -> Self {
37
+ ItsiBodyProxy {
38
+ incoming: Arc::new(Mutex::new(incoming.into_data_stream())),
39
+ closed: Arc::new(AtomicBool::new(false)),
40
+ buf: Arc::new(Mutex::new(vec![])),
41
+ }
42
+ }
43
+ /// Read up to the next line-break OR EOF
44
+ pub fn gets(&self) -> MagnusResult<Option<Bytes>> {
45
+ self.verify_open()?;
46
+ let mut stream = self.incoming.lock();
47
+ let mut buf = self.buf.lock();
48
+ while !buf.contains(&b'\n') {
49
+ if let Some(chunk) = block_on(stream.next()) {
50
+ let chunk = chunk.map_err(|err| {
51
+ magnus::Error::new(
52
+ magnus::exception::exception(),
53
+ format!("Error reading body {:?}", err),
54
+ )
55
+ })?;
56
+ buf.extend_from_slice(&chunk);
57
+ } else {
58
+ break;
59
+ }
60
+ }
61
+ if let Some(pos) = buf.iter().position(|&x| x == b'\n') {
62
+ let line = buf.drain(..=pos).collect::<Vec<u8>>();
63
+ Ok(Some(line.into()))
64
+ } else if !buf.is_empty() {
65
+ let line = buf.drain(..).collect::<Vec<u8>>();
66
+ Ok(Some(line.into()))
67
+ } else {
68
+ Ok(None)
69
+ }
70
+ }
71
+
72
+ pub fn read(&self, args: &[Value]) -> MagnusResult<Option<RString>> {
73
+ self.verify_open()?;
74
+ let scanned =
75
+ scan_args::scan_args::<(), (Option<usize>, Option<RString>), (), (), (), ()>(args)?;
76
+ let (length, mut buffer) = scanned.optional;
77
+ let mut stream = self.incoming.lock();
78
+ let mut buf = self.buf.lock();
79
+
80
+ while length.is_none_or(|target_length| buf.len() < target_length) {
81
+ if let Some(chunk) = block_on(stream.next()) {
82
+ let chunk = chunk.map_err(|err| {
83
+ magnus::Error::new(
84
+ magnus::exception::exception(),
85
+ format!("Error reading body {:?}", err),
86
+ )
87
+ })?;
88
+ buf.extend_from_slice(&chunk);
89
+ } else if length.is_some() {
90
+ return Ok(None);
91
+ } else {
92
+ break;
93
+ }
94
+ }
95
+ let output_string = buffer.take().unwrap_or(RString::buf_new(buf.len()));
96
+ output_string.cat(buf.clone());
97
+ buf.clear();
98
+ Ok(Some(output_string))
99
+ }
100
+
101
+ /// Equivalent to calling gets and yielding it, until we reach EOF
102
+ pub fn each(ruby: &Ruby, rbself: &Self) -> MagnusResult<()> {
103
+ let proc = ruby.block_proc()?;
104
+ while let Some(str) = rbself.gets()? {
105
+ proc.call::<_, Value>((str,))?;
106
+ }
107
+ Ok(())
108
+ }
109
+
110
+ fn verify_open(&self) -> MagnusResult<()> {
111
+ if self.closed.load(atomic::Ordering::SeqCst) {
112
+ return Err(magnus::Error::new(
113
+ magnus::exception::exception(),
114
+ "Body stream is closed",
115
+ ));
116
+ }
117
+ Ok(())
118
+ }
119
+ pub fn close(&self) {
120
+ self.closed.store(true, atomic::Ordering::SeqCst);
121
+ }
122
+ }
@@ -0,0 +1,2 @@
1
+ pub mod big_bytes;
2
+ pub mod itsi_body_proxy;