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 +4 -4
- data/Cargo.lock +1 -1
- data/ext/itsi_error/Cargo.toml +1 -1
- data/ext/itsi_rb_helpers/Cargo.toml +1 -1
- data/ext/itsi_scheduler/Cargo.toml +1 -1
- data/ext/itsi_server/Cargo.toml +1 -1
- data/ext/itsi_server/src/default_responses/mod.rs +3 -0
- data/ext/itsi_server/src/lib.rs +2 -0
- data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +15 -10
- data/ext/itsi_server/src/ruby_types/itsi_http_response.rs +10 -2
- data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +6 -0
- data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +13 -7
- data/ext/itsi_server/src/server/thread_worker.rs +4 -1
- data/lib/itsi/schedule_refinement.rb +96 -0
- data/lib/itsi/scheduler/version.rb +1 -1
- data/lib/itsi/scheduler.rb +1 -0
- metadata +2 -3
- data/README.md +0 -76
- data/sig/itsi_scheduler.rbs +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 92a729e2814758244bb48ab697c3e6f2cc5d239414df2f5f106beb6356d46e02
|
4
|
+
data.tar.gz: 4104654365bb5ace7881d90ac9e49f849e899ee1ca2b17ae03a7f5bf9d16dac2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3c8512c18fe4b5bde77b999ecf5f6772af2102d213bddfa8be2416178c2210b94787a8dc5531910cf85c815155ed2cf739999c08d6891342eb414a51bed5bc4f
|
7
|
+
data.tar.gz: 22914be4826e42af88929240d968ac493b8d9c82737807f33fe5aea0f979070eadac6de64104f912b0190dea2c3207e15199178f07c8c927ed5c5d0400936807
|
data/Cargo.lock
CHANGED
data/ext/itsi_error/Cargo.toml
CHANGED
data/ext/itsi_server/Cargo.toml
CHANGED
@@ -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);
|
data/ext/itsi_server/src/lib.rs
CHANGED
@@ -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
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
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().
|
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.
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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(
|
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
|
data/lib/itsi/scheduler.rb
CHANGED
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.
|
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!
|
data/sig/itsi_scheduler.rbs
DELETED