itsi-scheduler 0.2.10 → 0.2.12

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 06da68bc85607dea16257e5043d45b1a9ea6f376c9ef8d41e2bda7f4a89a0dcb
4
- data.tar.gz: 2932d32895a5bef79abd2156248bd2eb089944d19810389c1ee1086c0406bf5f
3
+ metadata.gz: 92a729e2814758244bb48ab697c3e6f2cc5d239414df2f5f106beb6356d46e02
4
+ data.tar.gz: 4104654365bb5ace7881d90ac9e49f849e899ee1ca2b17ae03a7f5bf9d16dac2
5
5
  SHA512:
6
- metadata.gz: 7419b864b913c1f80a50e8d3a7d061343382064bab38204e662a8e8eb4c3f5b31cbf26e473e74c7480c50bbfe04f792091c6960e903bbc3a7cfbb774a7f82e9e
7
- data.tar.gz: c94af7791e8da99fb1f174662587358a6994305650943372ba36635140eecd0b7b00c83bea33c4e1178f5d2e760acbb01ac7dd82c38b4de03ac1087df4656dc7
6
+ metadata.gz: 3c8512c18fe4b5bde77b999ecf5f6772af2102d213bddfa8be2416178c2210b94787a8dc5531910cf85c815155ed2cf739999c08d6891342eb414a51bed5bc4f
7
+ data.tar.gz: 22914be4826e42af88929240d968ac493b8d9c82737807f33fe5aea0f979070eadac6de64104f912b0190dea2c3207e15199178f07c8c927ed5c5d0400936807
data/Cargo.lock CHANGED
@@ -213,7 +213,7 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
213
213
 
214
214
  [[package]]
215
215
  name = "itsi-scheduler"
216
- version = "0.2.10"
216
+ version = "0.2.12"
217
217
  dependencies = [
218
218
  "bytes",
219
219
  "derive_more",
@@ -1,7 +1,7 @@
1
1
  [package]
2
2
  name = "itsi_error"
3
3
  version = "0.1.0"
4
- edition = "2024"
4
+ edition = "2021"
5
5
 
6
6
  [dependencies]
7
7
  thiserror = "2.0.11"
@@ -1,7 +1,7 @@
1
1
  [package]
2
2
  name = "itsi_rb_helpers"
3
3
  version = "0.1.0"
4
- edition = "2024"
4
+ edition = "2021"
5
5
 
6
6
  [dependencies]
7
7
  cfg-if = "1.0.0"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "itsi-scheduler"
3
- version = "0.2.10"
3
+ version = "0.2.12"
4
4
  edition = "2021"
5
5
  authors = ["Wouter Coppieters <wc@pico.net.nz>"]
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "itsi-server"
3
- version = "0.2.10"
3
+ version = "0.2.12"
4
4
  edition = "2021"
5
5
  authors = ["Wouter Coppieters <wc@pico.net.nz>"]
6
6
  license = "MIT"
@@ -9,3 +9,6 @@ pub static NOT_FOUND_RESPONSE: LazyLock<ErrorResponse> = LazyLock::new(ErrorResp
9
9
 
10
10
  pub static INTERNAL_SERVER_ERROR_RESPONSE: LazyLock<ErrorResponse> =
11
11
  LazyLock::new(ErrorResponse::internal_server_error);
12
+
13
+ pub static SERVICE_UNAVAILABLE_RESPONSE: LazyLock<ErrorResponse> =
14
+ LazyLock::new(ErrorResponse::service_unavailable);
@@ -86,6 +86,8 @@ fn init(ruby: &Ruby) -> Result<()> {
86
86
  response.define_method("<<", method!(ItsiHttpResponse::send_frame, 1))?;
87
87
  response.define_method("write", method!(ItsiHttpResponse::send_frame, 1))?;
88
88
  response.define_method("read", method!(ItsiHttpResponse::recv_frame, 0))?;
89
+ response.define_method("flush", method!(ItsiHttpResponse::flush, 0))?;
90
+ response.define_method("closed?", method!(ItsiHttpResponse::is_closed, 0))?;
89
91
  response.define_method(
90
92
  "send_and_close",
91
93
  method!(ItsiHttpResponse::send_and_close, 1),
@@ -22,6 +22,7 @@ use super::{
22
22
  itsi_http_response::ItsiHttpResponse,
23
23
  };
24
24
  use crate::{
25
+ default_responses::{INTERNAL_SERVER_ERROR_RESPONSE, SERVICE_UNAVAILABLE_RESPONSE},
25
26
  server::{
26
27
  byte_frame::ByteFrame,
27
28
  http_message_types::{HttpRequest, HttpResponse},
@@ -186,16 +187,20 @@ impl ItsiHttpRequest {
186
187
  } else {
187
188
  &context.sender
188
189
  };
189
- match sender
190
- .send(RequestJob::ProcessHttpRequest(request, app))
191
- .await
192
- {
193
- Err(err) => {
194
- error!("Error occurred: {}", err);
195
- let mut response = Response::new(BoxBody::new(Empty::new()));
196
- *response.status_mut() = StatusCode::BAD_REQUEST;
197
- Ok(response)
198
- }
190
+ match sender.try_send(RequestJob::ProcessHttpRequest(request, app)) {
191
+ Err(err) => match err {
192
+ async_channel::TrySendError::Full(_) => {
193
+ Ok(SERVICE_UNAVAILABLE_RESPONSE
194
+ .to_http_response(context.accept.clone())
195
+ .await)
196
+ }
197
+ async_channel::TrySendError::Closed(err) => {
198
+ error!("Error occurred: {:?}", err);
199
+ Ok(INTERNAL_SERVER_ERROR_RESPONSE
200
+ .to_http_response(context.accept.clone())
201
+ .await)
202
+ }
203
+ },
199
204
  _ => match receiver.recv().await {
200
205
  Some(first_frame) => Ok(response
201
206
  .build(first_frame, receiver, shutdown_channel)
@@ -272,6 +272,14 @@ impl ItsiHttpResponse {
272
272
  // not implemented
273
273
  }
274
274
 
275
+ pub fn flush(&self) {
276
+ // no-op
277
+ }
278
+
279
+ pub fn is_closed(&self) -> bool {
280
+ self.data.response_writer.write().is_none()
281
+ }
282
+
275
283
  pub fn send_and_close(&self, frame: Bytes) -> MagnusResult<()> {
276
284
  let result = self.send_frame_into(ByteFrame::End(frame), &self.data.response_writer);
277
285
  self.data.response_writer.write().take();
@@ -339,7 +347,7 @@ impl ItsiHttpResponse {
339
347
  })?;
340
348
  let header_value = unsafe { HeaderValue::from_maybe_shared_unchecked(value) };
341
349
  if let Some(ref mut resp) = *self.data.response.write() {
342
- resp.headers_mut().insert(header_name, header_value);
350
+ resp.headers_mut().append(header_name, header_value);
343
351
  }
344
352
  Ok(())
345
353
  }
@@ -356,7 +364,7 @@ impl ItsiHttpResponse {
356
364
  })?;
357
365
  for value in values {
358
366
  let header_value = unsafe { HeaderValue::from_maybe_shared_unchecked(value) };
359
- headers_mut.insert(&header_name, header_value);
367
+ headers_mut.append(&header_name, header_value);
360
368
  }
361
369
  }
362
370
  }
@@ -69,6 +69,7 @@ pub struct ServerParams {
69
69
  pub pin_worker_cores: bool,
70
70
  pub scheduler_class: Option<String>,
71
71
  pub oob_gc_responses_threshold: Option<u64>,
72
+ pub ruby_thread_request_backlog_size: Option<usize>,
72
73
  pub middleware_loader: HeapValue<Proc>,
73
74
  pub middleware: OnceLock<MiddlewareSet>,
74
75
  pub binds: Vec<Bind>,
@@ -222,6 +223,10 @@ impl ServerParams {
222
223
  let scheduler_class: Option<String> = rb_param_hash.fetch("scheduler_class")?;
223
224
  let oob_gc_responses_threshold: Option<u64> =
224
225
  rb_param_hash.fetch("oob_gc_responses_threshold")?;
226
+
227
+ let ruby_thread_request_backlog_size: Option<usize> =
228
+ rb_param_hash.fetch("ruby_thread_request_backlog_size")?;
229
+
225
230
  let middleware_loader: Proc = rb_param_hash.fetch("middleware_loader")?;
226
231
  let log_level: Option<String> = rb_param_hash.fetch("log_level")?;
227
232
  let log_target: Option<String> = rb_param_hash.fetch("log_target")?;
@@ -310,6 +315,7 @@ impl ServerParams {
310
315
  scheduler_threads,
311
316
  streamable_body,
312
317
  scheduler_class,
318
+ ruby_thread_request_backlog_size,
313
319
  oob_gc_responses_threshold,
314
320
  binds,
315
321
  itsi_server_token_preference,
@@ -18,6 +18,7 @@ use std::sync::Arc;
18
18
  pub struct RubyApp {
19
19
  app: Arc<HeapValue<Proc>>,
20
20
  request_type: RequestType,
21
+ script_name: Option<String>,
21
22
  sendfile: bool,
22
23
  nonblocking: bool,
23
24
  base_path: Regex,
@@ -53,6 +54,9 @@ impl RubyApp {
53
54
  let base_path_src = params
54
55
  .funcall::<_, _, String>(Symbol::new("[]"), ("base_path",))
55
56
  .unwrap_or("".to_owned());
57
+ let script_name = params
58
+ .funcall::<_, _, Option<String>>(Symbol::new("[]"), ("script_name",))
59
+ .unwrap_or(None);
56
60
  let base_path = Regex::new(&base_path_src).unwrap();
57
61
 
58
62
  let request_type: RequestType = params
@@ -65,6 +69,7 @@ impl RubyApp {
65
69
  app: Arc::new(app.into()),
66
70
  sendfile,
67
71
  nonblocking,
72
+ script_name,
68
73
  request_type,
69
74
  base_path,
70
75
  }))
@@ -82,13 +87,14 @@ impl MiddlewareLayer for RubyApp {
82
87
  match self.request_type {
83
88
  RequestType::Http => {
84
89
  let uri = req.uri().path();
85
- let script_name = self
86
- .base_path
87
- .captures(uri)
88
- .and_then(|caps| caps.name("base_path"))
89
- .map(|m| m.as_str())
90
- .unwrap_or("/")
91
- .to_owned();
90
+ let script_name = self.script_name.clone().unwrap_or_else(|| {
91
+ self.base_path
92
+ .captures(uri)
93
+ .and_then(|caps| caps.name("base_path"))
94
+ .map(|m| m.as_str())
95
+ .unwrap_or("/")
96
+ .to_owned()
97
+ });
92
98
  ItsiHttpRequest::process_request(
93
99
  self.app.clone(),
94
100
  req,
@@ -65,9 +65,12 @@ type ThreadWorkerBuildResult = Result<(
65
65
  pub fn build_thread_workers(params: Arc<ServerParams>, pid: Pid) -> ThreadWorkerBuildResult {
66
66
  let blocking_thread_count = params.threads;
67
67
  let nonblocking_thread_count = params.scheduler_threads;
68
+ let ruby_thread_request_backlog_size: usize = params
69
+ .ruby_thread_request_backlog_size
70
+ .unwrap_or_else(|| (blocking_thread_count as u16 * 30) as usize);
68
71
 
69
72
  let (blocking_sender, blocking_receiver) =
70
- async_channel::bounded((blocking_thread_count as u16 * 30) as usize);
73
+ async_channel::bounded(ruby_thread_request_backlog_size);
71
74
  let blocking_receiver_ref = Arc::new(blocking_receiver);
72
75
  let blocking_sender_ref = blocking_sender;
73
76
  let scheduler_class = load_scheduler_class(params.scheduler_class.clone())?;
@@ -0,0 +1,96 @@
1
+ module Itsi
2
+ module ScheduleRefinement
3
+ # Useful helper functions for using cooperative multi-tasking in Ruby.
4
+ # Opt-in to usage by executing `using Itsi::ScheduleRefinement` in any places
5
+ # you intend to use it.
6
+ #
7
+ # After this you can do things like the following
8
+ #
9
+ # 1. Launch batch concurrent fire-and-forget jobs.
10
+ # * 100.times.schedule_each{ sleep 0.1 }
11
+ #
12
+ # 2. Launch batch concurrent transofmrs
13
+ # See how `schedule_map` retains ordering, despite sleeping for randomized amount of time.
14
+ #
15
+ # * 100.times.schedule_map{|i| sleep Random.rand(0.0..0.05); i }
16
+ #
17
+ # 3. Manually organize fibers to run concurrently.
18
+ #
19
+ # require "net/http"
20
+ # schedule do
21
+ # req1, req2 = Queue.new, Queue.new
22
+ # schedule do
23
+ # puts "Making request 1"
24
+ # req1 << Net::HTTP.get(URI("http://httpbin.org/get"))
25
+ # puts "Finished request 1"
26
+ # end
27
+ #
28
+ # schedule do
29
+ # puts "Making request 2"
30
+ # req2 << Net::HTTP.get(URI("http://httpbin.org/get"))
31
+ # puts "Finished request 2"
32
+ # end
33
+ #
34
+ # res1, res2 = [req1, req2].map(&:pop)
35
+ # end
36
+ refine Kernel do
37
+ private def schedule(&blk) # rubocop:disable Metrics/MethodLength
38
+ return unless blk
39
+
40
+ if Fiber.scheduler.nil?
41
+ result = nil
42
+ Thread.new do
43
+ Fiber.set_scheduler Itsi::Scheduler.new
44
+ Fiber.schedule { result = blk.call }
45
+ end.join
46
+ result
47
+ else
48
+ Fiber.schedule(&blk)
49
+ end
50
+ end
51
+ end
52
+
53
+ module EnumerableExtensions
54
+ using ScheduleRefinement
55
+ def schedule_each(&block)
56
+ enum = Enumerator.new do |y|
57
+ schedule do
58
+ each { |item| schedule{ y.yield(item) } }
59
+ end
60
+ end
61
+
62
+ block_given? ? enum.each(&block) : enum.each
63
+ end
64
+
65
+ def schedule_map(&block)
66
+ return Enumerator.new do |y|
67
+ schedule do
68
+ with_index.each_with_object([]) do |(item, index), agg|
69
+ schedule do
70
+ agg[index] = (y << item)
71
+ end
72
+ end
73
+ end
74
+ end.map unless block_given?
75
+ schedule do
76
+ with_index.each_with_object([]) do |(item, index), agg|
77
+ schedule do
78
+ agg[index] = block[item]
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+
86
+ refine Enumerator do
87
+ define_method(:schedule_each, EnumerableExtensions.instance_method(:schedule_each))
88
+ define_method(:schedule_map, EnumerableExtensions.instance_method(:schedule_map))
89
+ end
90
+
91
+ refine Enumerable do
92
+ define_method(:schedule_each, EnumerableExtensions.instance_method(:schedule_each))
93
+ define_method(:schedule_map, EnumerableExtensions.instance_method(:schedule_map))
94
+ end
95
+ end
96
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Scheduler
5
- VERSION = "0.2.10"
5
+ VERSION = "0.2.12"
6
6
  end
7
7
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "scheduler/version"
4
4
  require_relative "scheduler/itsi_scheduler"
5
+ require_relative "schedule_refinement"
5
6
 
6
7
  module Itsi
7
8
  class Scheduler
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: itsi-scheduler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.10
4
+ version: 0.2.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wouter Coppieters
@@ -35,7 +35,6 @@ files:
35
35
  - ".rubocop.yml"
36
36
  - Cargo.lock
37
37
  - Cargo.toml
38
- - README.md
39
38
  - Rakefile
40
39
  - ext/itsi_acme/Cargo.toml
41
40
  - ext/itsi_acme/examples/high_level.rs
@@ -180,9 +179,9 @@ files:
180
179
  - ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock
181
180
  - ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock
182
181
  - itsi-scheduler-100.png
182
+ - lib/itsi/schedule_refinement.rb
183
183
  - lib/itsi/scheduler.rb
184
184
  - lib/itsi/scheduler/version.rb
185
- - sig/itsi_scheduler.rbs
186
185
  homepage: https://itsi.fyi
187
186
  licenses:
188
187
  - MIT
data/README.md DELETED
@@ -1,76 +0,0 @@
1
- ---
2
- title: Itsi Scheduler
3
- type: docs
4
- weight: 4
5
- sidebar:
6
- exclude: true
7
- ---
8
- <img src="itsi-scheduler-100.png" width="80px" style="display: block; margin-left: auto; margin-right: auto;">
9
-
10
- `Itsi Scheduler` is an implementation of a Ruby [Fiber Scheduler](https://docs.ruby-lang.org/en/3.2/Fiber/Scheduler.html).
11
-
12
- When combined with Itsi Server, you can write endpoints that look just like regular synchronous Ruby code. Behind the scenes, the scheduler will transparently pause and resume fibers to prevent threads from blocking, greatly increasing throughput for I/O-heavy workloads
13
-
14
- If you're purely after a lightweight, yet efficient Ruby scheduler,
15
- you can use Itsi Scheduler as a standalone scheduler for any Ruby application.
16
-
17
- Just use `Fiber.set_scheduler` to set an instance `Itsi::Scheduler` as a scheduler to opt in to this IO weaving behavior
18
- *automatically* for all blocking IO.
19
-
20
- ### Primer on Fiber Schedulers
21
-
22
- Fiber schedulers are a way to automatically manage the execution of non-blocking fibers in Ruby. A scheduler is responsible for the automatic pausing and resumption of Fibers based
23
- on whether or not they are awaiting IO operations.
24
- Ruby's Fiber scheduler implementation automatically invokes the current Fiber scheduler (if it exists) for each blocking operation, allowing it to seamlessly drive the execution of huge numbers of simultaneous non-blocking fibers
25
- while ensuring the main thread is never blocked on IO.
26
-
27
- This behind the scenes magic allows Ruby to provide async IO (just like we find in languages with `async/await` like `Rust`, `C#`, `JavaScript`) *but* with the added beauty
28
- that synchronous and asynchronous code is identical! (I.e. Ruby's functions are [colorless](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/))
29
-
30
- ## Getting Started
31
- To install and use Itsi Scheduler follow the instructions below:
32
-
33
-
34
- ### 1 - Install Itsi Scheduler
35
-
36
- **Prerequisites**
37
-
38
- You'll need at least `build-essential` and `libclang-dev` installed to build Itsi on Linux.
39
- E.g.
40
- ```bash
41
- apt-get install build-essential libclang-dev
42
- ```
43
-
44
- Then use `gem` to install the Itsi package. This will in turn install both the
45
- `itsi-server` gem, and the `itsi-scheduler` gem.
46
-
47
-
48
- ```bash
49
- gem install itsi-scheduler
50
- ```
51
-
52
-
53
- ### 2 - Use Itsi Scheduler
54
-
55
- Great! You now have Itsi Scheduler installed.
56
- Now you can run code like this:
57
-
58
- ```ruby
59
- require 'itsi/scheduler'
60
- require 'socket'
61
- results = Thread.new do
62
- Fiber.set_scheduler Itsi::Scheduler.new
63
- results = []
64
- Fiber.schedule do
65
- results << Addrinfo.getaddrinfo("www.ruby-lang.org", 80, nil, :STREAM)
66
- end
67
- Fiber.schedule do
68
- results << Addrinfo.getaddrinfo("www.google.com", 80, nil, :STREAM)
69
- end
70
- results
71
- end.value
72
-
73
- puts results.map(&:inspect)
74
- ```
75
-
76
- to run many blocking operations simultaneously all while occupying only a single Ruby thread!
@@ -1,4 +0,0 @@
1
- module ItsiScheduler
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end