itsi 0.2.12 → 0.2.14
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/CHANGELOG.md +9 -0
- data/Cargo.lock +3 -2
- data/crates/itsi_scheduler/Cargo.toml +1 -1
- data/crates/itsi_server/Cargo.toml +2 -1
- data/crates/itsi_server/src/ruby_types/itsi_http_response.rs +46 -5
- data/examples/hybrid_scheduler_mode/README.md +46 -0
- data/gems/scheduler/Cargo.lock +1 -1
- data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
- data/gems/server/Cargo.lock +2 -1
- data/gems/server/lib/itsi/server/config/middleware/static_assets.rb +3 -2
- data/gems/server/lib/itsi/server/config/middleware/static_response.rb +7 -6
- data/gems/server/lib/itsi/server/config/options/scheduler_threads.md +1 -1
- data/gems/server/lib/itsi/server/version.rb +1 -1
- data/gems/server/test/rack/test_rack_server.rb +32 -1
- data/lib/itsi/version.rb +1 -1
- metadata +6 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 239b868f5edfaff5a1c7dd9e47b9f6023f284ae7c06a43438ec127ab83b5f1c1
|
4
|
+
data.tar.gz: 8f0e2154a4e05281eeabb9e8eaded247be04ea17efa66e72cccbad7df356dcfb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: db430b1ba0736844a54eb9ebe00a2f12b51342b27544bdd58ccce2440fb5eb1e6e961cf95dc3a5665def3384ba1f8cd2cfa457f93e62d01022be3419a1f3b00c
|
7
|
+
data.tar.gz: e626d98db8d34bddca498e8086ff8b4dfe41091b1363f7c3f0899b061ecab0ff79b628f2393f82b54dca073c78f85801fb07af3d5c0880f334103fd3cf7a7b86
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
## [0.2.14] - 2025-04-30
|
2
|
+
- Support new-line separated headers for Rack 2 backward compatibility.
|
3
|
+
|
4
|
+
## [0.2.12] - 2025-04-29
|
5
|
+
- Max Rust edition is now "2021"
|
6
|
+
- Removed invalid rbs files causing RI doc generation failure
|
7
|
+
- Fixed header clobbering in Rack
|
8
|
+
- Added new `ruby_thread_request_backlog_size` option
|
9
|
+
|
1
10
|
## [0.2.3] - 2025-04-22
|
2
11
|
|
3
12
|
- Public release!
|
data/Cargo.lock
CHANGED
@@ -1644,7 +1644,7 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
|
1644
1644
|
|
1645
1645
|
[[package]]
|
1646
1646
|
name = "itsi-scheduler"
|
1647
|
-
version = "0.2.
|
1647
|
+
version = "0.2.14"
|
1648
1648
|
dependencies = [
|
1649
1649
|
"bytes",
|
1650
1650
|
"derive_more",
|
@@ -1662,7 +1662,7 @@ dependencies = [
|
|
1662
1662
|
|
1663
1663
|
[[package]]
|
1664
1664
|
name = "itsi-server"
|
1665
|
-
version = "0.2.
|
1665
|
+
version = "0.2.14"
|
1666
1666
|
dependencies = [
|
1667
1667
|
"argon2",
|
1668
1668
|
"async-channel",
|
@@ -1692,6 +1692,7 @@ dependencies = [
|
|
1692
1692
|
"jsonwebtoken",
|
1693
1693
|
"magnus",
|
1694
1694
|
"md5",
|
1695
|
+
"memchr",
|
1695
1696
|
"moka",
|
1696
1697
|
"nix",
|
1697
1698
|
"notify",
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[package]
|
2
2
|
name = "itsi-server"
|
3
|
-
version = "0.2.
|
3
|
+
version = "0.2.14"
|
4
4
|
edition = "2021"
|
5
5
|
authors = ["Wouter Coppieters <wc@pico.net.nz>"]
|
6
6
|
license = "MIT"
|
@@ -88,3 +88,4 @@ percent-encoding = "2.3.1"
|
|
88
88
|
sha-crypt = "0.5.0"
|
89
89
|
argon2 = "0.5.3"
|
90
90
|
core_affinity = "0.8.3"
|
91
|
+
memchr = "2.7.4"
|
@@ -1,4 +1,4 @@
|
|
1
|
-
use bytes::{Bytes, BytesMut};
|
1
|
+
use bytes::{Buf, Bytes, BytesMut};
|
2
2
|
use derive_more::Debug;
|
3
3
|
use futures::stream::{unfold, StreamExt};
|
4
4
|
use http::{
|
@@ -12,6 +12,7 @@ use hyper_util::rt::TokioIo;
|
|
12
12
|
use itsi_error::Result;
|
13
13
|
use itsi_tracing::error;
|
14
14
|
use magnus::error::Result as MagnusResult;
|
15
|
+
use memchr::{memchr, memchr_iter};
|
15
16
|
use parking_lot::RwLock;
|
16
17
|
use std::{
|
17
18
|
collections::HashMap,
|
@@ -345,13 +346,54 @@ impl ItsiHttpResponse {
|
|
345
346
|
let header_name: HeaderName = HeaderName::from_bytes(&name).map_err(|e| {
|
346
347
|
itsi_error::ItsiError::InvalidInput(format!("Invalid header name {:?}: {:?}", name, e))
|
347
348
|
})?;
|
348
|
-
let header_value = unsafe { HeaderValue::from_maybe_shared_unchecked(value) };
|
349
349
|
if let Some(ref mut resp) = *self.data.response.write() {
|
350
|
-
resp.headers_mut()
|
350
|
+
let headers_mut = resp.headers_mut();
|
351
|
+
self.insert_header(headers_mut, &header_name, value);
|
351
352
|
}
|
352
353
|
Ok(())
|
353
354
|
}
|
354
355
|
|
356
|
+
pub fn insert_header(
|
357
|
+
&self,
|
358
|
+
headers_mut: &mut HeaderMap,
|
359
|
+
header_name: &HeaderName,
|
360
|
+
value: Bytes,
|
361
|
+
) {
|
362
|
+
static MAX_SPLIT_HEADERS: usize = 100;
|
363
|
+
|
364
|
+
let mut start = 0usize;
|
365
|
+
let mut emitted = 0usize;
|
366
|
+
|
367
|
+
for idx in memchr_iter(b'\n', &value).chain(std::iter::once(value.len())) {
|
368
|
+
if idx == start {
|
369
|
+
start += 1;
|
370
|
+
continue;
|
371
|
+
}
|
372
|
+
|
373
|
+
let mut part = value.slice(start..idx);
|
374
|
+
if part.ends_with(b"\r") {
|
375
|
+
part.truncate(part.len() - 1);
|
376
|
+
}
|
377
|
+
if let Some(&(b' ' | b'\t')) = part.first() {
|
378
|
+
part.advance(1);
|
379
|
+
}
|
380
|
+
if memchr(0, &part).is_some() || part.iter().any(|&b| b < 0x20) {
|
381
|
+
warn!("stripped control char from header {:?}", header_name);
|
382
|
+
start = idx + 1;
|
383
|
+
continue;
|
384
|
+
}
|
385
|
+
|
386
|
+
emitted += 1;
|
387
|
+
if emitted > MAX_SPLIT_HEADERS {
|
388
|
+
break;
|
389
|
+
}
|
390
|
+
|
391
|
+
let hv = unsafe { HeaderValue::from_maybe_shared_unchecked(part) };
|
392
|
+
headers_mut.append(header_name, hv);
|
393
|
+
start = idx + 1;
|
394
|
+
}
|
395
|
+
}
|
396
|
+
|
355
397
|
pub fn add_headers(&self, headers: HashMap<Bytes, Vec<Bytes>>) -> MagnusResult<()> {
|
356
398
|
if let Some(ref mut resp) = *self.data.response.write() {
|
357
399
|
let headers_mut = resp.headers_mut();
|
@@ -363,8 +405,7 @@ impl ItsiHttpResponse {
|
|
363
405
|
))
|
364
406
|
})?;
|
365
407
|
for value in values {
|
366
|
-
|
367
|
-
headers_mut.append(&header_name, header_value);
|
408
|
+
self.insert_header(headers_mut, &header_name, value);
|
368
409
|
}
|
369
410
|
}
|
370
411
|
}
|
@@ -0,0 +1,46 @@
|
|
1
|
+
## Hybrid Scheduler Mode Example
|
2
|
+
This example shows how you can route some requests to use traditional blocking threads,
|
3
|
+
and others to use threads that have a Fiber scheduler enabled.
|
4
|
+
|
5
|
+
First, run the slow_service inside ./slow_service.
|
6
|
+
E.g.
|
7
|
+
```bash
|
8
|
+
cd ./slow_service
|
9
|
+
itsi
|
10
|
+
```
|
11
|
+
|
12
|
+
Then in a second tab, run the hybrid_scheduler_mode example.
|
13
|
+
E.g.
|
14
|
+
```bash
|
15
|
+
itsi
|
16
|
+
```
|
17
|
+
|
18
|
+
If you run a benchmark on the `/nonblocking` endpoint, you
|
19
|
+
should see Itsi will concurrent execute all requests. By contrast, running on
|
20
|
+
the `/blocking` endpoint should show drastically worse throughput due to requests running sequentially.
|
21
|
+
|
22
|
+
```bash
|
23
|
+
# Non-blocking mode. 100 requests at a time, service takes 2s to respond. Throughput of ~50rps expected.
|
24
|
+
❯ wrk http://0.0.0.0:3000/nonblocking -c 100 -d 10
|
25
|
+
Running 10s test @ http://0.0.0.0:3000/nonblocking
|
26
|
+
2 threads and 100 connections
|
27
|
+
Thread Stats Avg Stdev Max +/- Stdev
|
28
|
+
Latency 0.00us 0.00us 0.00us nan%
|
29
|
+
Req/Sec 65.00 109.41 316.00 84.62%
|
30
|
+
500 requests in 10.10s, 69.34KB read
|
31
|
+
Socket errors: connect 0, read 0, write 0, timeout 500
|
32
|
+
Requests/sec: 49.50
|
33
|
+
Transfer/sec: 6.86KB
|
34
|
+
# Blocking mode. Requests are executed sequentially , service takes 2s to respond. Throughput of ~0.5rps expected.
|
35
|
+
❯ wrk http://0.0.0.0:3000/nonblocking -c 100 -d 10
|
36
|
+
Running 10s test @ http://0.0.0.0:3000/blocking
|
37
|
+
2 threads and 100 connections
|
38
|
+
Thread Stats Avg Stdev Max +/- Stdev
|
39
|
+
Latency 0.00us 0.00us 0.00us nan%
|
40
|
+
Req/Sec 0.00 0.00 0.00 100.00%
|
41
|
+
5 requests in 10.10s, 710.00B read
|
42
|
+
Socket errors: connect 0, read 0, write 0, timeout 5
|
43
|
+
Requests/sec: 0.49
|
44
|
+
```
|
45
|
+
|
46
|
+
See the `Itsi.rb` file for more details.
|
data/gems/scheduler/Cargo.lock
CHANGED
data/gems/server/Cargo.lock
CHANGED
@@ -1644,7 +1644,7 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
|
1644
1644
|
|
1645
1645
|
[[package]]
|
1646
1646
|
name = "itsi-server"
|
1647
|
-
version = "0.2.
|
1647
|
+
version = "0.2.14"
|
1648
1648
|
dependencies = [
|
1649
1649
|
"argon2",
|
1650
1650
|
"async-channel",
|
@@ -1674,6 +1674,7 @@ dependencies = [
|
|
1674
1674
|
"jsonwebtoken",
|
1675
1675
|
"magnus",
|
1676
1676
|
"md5",
|
1677
|
+
"memchr",
|
1677
1678
|
"moka",
|
1678
1679
|
"nix",
|
1679
1680
|
"notify",
|
@@ -18,7 +18,8 @@ module Itsi
|
|
18
18
|
serve_hidden_files: ${11|true,false|}
|
19
19
|
SNIPPET
|
20
20
|
|
21
|
-
detail "Serves static files from a designated directory with options for auto indexing, in-memory caching,
|
21
|
+
detail "Serves static files from a designated directory with options for auto indexing, in-memory caching, "\
|
22
|
+
"and custom header support. Supports relative path rewriting and file range requests."
|
22
23
|
|
23
24
|
ErrorResponse = TypedStruct.new do
|
24
25
|
{
|
@@ -42,7 +43,7 @@ module Itsi
|
|
42
43
|
{
|
43
44
|
root_dir: (Type(String) & Required()).default("./"),
|
44
45
|
not_found_behavior: Or(
|
45
|
-
Enum([
|
46
|
+
Enum(%w[fallthrough index redirect internal_server_error]),
|
46
47
|
Type(IndexResponse),
|
47
48
|
Type(RedirectResponse),
|
48
49
|
Type(ErrorResponse)
|
@@ -2,20 +2,21 @@ module Itsi
|
|
2
2
|
class Server
|
3
3
|
module Config
|
4
4
|
class StaticResponse < Middleware
|
5
|
+
|
5
6
|
insert_text <<~SNIPPET
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
7
|
+
static_response \\
|
8
|
+
code: ${1|200,404,500|},
|
9
|
+
headers: [${2|%w[content-type text/plain],%w[cache-control max-age=60]|}],
|
10
|
+
body: ${3|"OK", "Not Found"|}
|
10
11
|
SNIPPET
|
11
12
|
|
12
13
|
detail "Immediately return a fixed HTTP response with code, headers, and body."
|
13
14
|
|
14
15
|
schema do
|
15
16
|
{
|
16
|
-
code:
|
17
|
+
code: (Type(Integer) & Required()),
|
17
18
|
headers: Array(Array(Type(String), Type(String))).default([]),
|
18
|
-
body:
|
19
|
+
body: Type(String).default("")
|
19
20
|
}
|
20
21
|
end
|
21
22
|
|
@@ -38,4 +38,4 @@ rackup_file "./config.ru"
|
|
38
38
|
```
|
39
39
|
## Examples.
|
40
40
|
|
41
|
-
See [https://github.com/wouterken/itsi/tree/main/examples/hybrid_scheduler_mode
|
41
|
+
See [a code example](https://github.com/wouterken/itsi/tree/main/examples/hybrid_scheduler_mode) in the Git repository.
|
@@ -356,7 +356,7 @@ class TestRackServer < Minitest::Test
|
|
356
356
|
end
|
357
357
|
|
358
358
|
def test_multi_field_headers
|
359
|
-
server(app_with_lint: lambda do |
|
359
|
+
server(app_with_lint: lambda do |_|
|
360
360
|
[200, { "content-type" => "text/plain", "x-example" => ["one, two, three", "four, five"] }, ["Multiple Field Headers"]]
|
361
361
|
end) do
|
362
362
|
response = get_resp("/")
|
@@ -365,4 +365,35 @@ class TestRackServer < Minitest::Test
|
|
365
365
|
assert_equal "Multiple Field Headers", response.body
|
366
366
|
end
|
367
367
|
end
|
368
|
+
|
369
|
+
# For backwards compatibility with Rack-2, which uses "\n" for multiple values in headers.
|
370
|
+
# https://github.com/rack/rack/blob/df6c47357f6c6bec2d585f45f417285d813d9b3a/lib/rack/utils.rb#L271
|
371
|
+
#
|
372
|
+
# In Rack 3, the behavior has changed to using Arrays of Strings exclusively.
|
373
|
+
# Note we don't use Rack lint here, because it'll complain about the invalid header value.
|
374
|
+
def test_multiline_headers_legacy_cookie
|
375
|
+
cookies = "one\r\ntwo\n three\n\tfour\nfive\n\n"
|
376
|
+
server(app: ->(_) { [200, { "set-cookie" => cookies }, ["OK"]] }) do
|
377
|
+
resp = get_resp("/")
|
378
|
+
assert_equal "200", resp.code
|
379
|
+
# folded lines should coalesce; empty lines disappear
|
380
|
+
assert_equal %w[one two three four five], resp.get_fields("set-cookie")
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
def test_control_chars_are_stripped
|
385
|
+
evil = "good\nbad\x01bad\ngood"
|
386
|
+
server(app: ->(_) { [200, { "x-evil" => evil }, ["body"]] }) do
|
387
|
+
resp = get_resp("/")
|
388
|
+
assert_equal %w[good good], resp.get_fields("x-evil")
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
def test_rack3_array_is_untouched
|
393
|
+
server(app: ->(_) { [200,
|
394
|
+
{ "set-cookie" => ["a=b", "c=d"] }, ["OK"] ] }) do
|
395
|
+
resp = get_resp("/")
|
396
|
+
assert_equal %w[a=b c=d], resp.get_fields("set-cookie")
|
397
|
+
end
|
398
|
+
end
|
368
399
|
end
|
data/lib/itsi/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: itsi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.14
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Wouter Coppieters
|
@@ -15,28 +15,28 @@ dependencies:
|
|
15
15
|
requirements:
|
16
16
|
- - "~>"
|
17
17
|
- !ruby/object:Gem::Version
|
18
|
-
version: 0.2.
|
18
|
+
version: 0.2.14
|
19
19
|
type: :runtime
|
20
20
|
prerelease: false
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
22
22
|
requirements:
|
23
23
|
- - "~>"
|
24
24
|
- !ruby/object:Gem::Version
|
25
|
-
version: 0.2.
|
25
|
+
version: 0.2.14
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
27
|
name: itsi-server
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
29
29
|
requirements:
|
30
30
|
- - "~>"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: 0.2.
|
32
|
+
version: 0.2.14
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
36
36
|
requirements:
|
37
37
|
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version: 0.2.
|
39
|
+
version: 0.2.14
|
40
40
|
description: Wrapper Gem for both the Itsi server and the Itsi Fiber scheduler
|
41
41
|
email:
|
42
42
|
- wc@pico.net.nz
|
@@ -254,6 +254,7 @@ files:
|
|
254
254
|
- examples/file_server/subdirectory/movies.csv
|
255
255
|
- examples/helpers/datastore.rb
|
256
256
|
- examples/hybrid_scheduler_mode/Itsi.rb
|
257
|
+
- examples/hybrid_scheduler_mode/README.md
|
257
258
|
- examples/hybrid_scheduler_mode/config.ru
|
258
259
|
- examples/hybrid_scheduler_mode/slow_service/Itsi.rb
|
259
260
|
- examples/media_server/Itsi.rb
|