itsi-server 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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +1 -1
  3. data/ext/itsi_error/Cargo.toml +1 -1
  4. data/ext/itsi_rb_helpers/Cargo.toml +1 -1
  5. data/ext/itsi_scheduler/Cargo.toml +1 -1
  6. data/ext/itsi_server/Cargo.toml +1 -1
  7. data/ext/itsi_server/src/default_responses/mod.rs +3 -0
  8. data/ext/itsi_server/src/lib.rs +2 -0
  9. data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +15 -10
  10. data/ext/itsi_server/src/ruby_types/itsi_http_response.rs +10 -2
  11. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +6 -0
  12. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +13 -7
  13. data/ext/itsi_server/src/server/thread_worker.rs +4 -1
  14. data/lib/itsi/http_request.rb +19 -20
  15. data/lib/itsi/server/config/config_helpers.rb +5 -2
  16. data/lib/itsi/server/config/middleware/endpoint/_index.md +2 -1
  17. data/lib/itsi/server/config/middleware/endpoint/delete.rb +3 -2
  18. data/lib/itsi/server/config/middleware/endpoint/endpoint.rb +14 -3
  19. data/lib/itsi/server/config/middleware/endpoint/get.rb +3 -2
  20. data/lib/itsi/server/config/middleware/endpoint/patch.rb +3 -2
  21. data/lib/itsi/server/config/middleware/endpoint/post.rb +3 -2
  22. data/lib/itsi/server/config/middleware/endpoint/put.rb +3 -2
  23. data/lib/itsi/server/config/middleware/rackup_file.md +18 -0
  24. data/lib/itsi/server/config/middleware/rackup_file.rb +2 -0
  25. data/lib/itsi/server/config/middleware/run.md +20 -1
  26. data/lib/itsi/server/config/middleware/run.rb +2 -0
  27. data/lib/itsi/server/config/options/certificates.md +2 -2
  28. data/lib/itsi/server/config/options/ruby_thread_request_backlog_size.md +18 -0
  29. data/lib/itsi/server/config/options/ruby_thread_request_backlog_size.rb +19 -0
  30. data/lib/itsi/server/config/options/scheduler_threads.md +8 -1
  31. data/lib/itsi/server/config.rb +3 -0
  32. data/lib/itsi/server/default_config/Itsi.rb +2 -2
  33. data/lib/itsi/server/rack/handler/itsi.rb +3 -2
  34. data/lib/itsi/server/rack_interface.rb +5 -6
  35. data/lib/itsi/server/version.rb +1 -1
  36. metadata +3 -3
  37. data/README.md +0 -49
  38. data/sig/itsi_server.rbs +0 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1299ba07e4b4de522049dbf9df9c8948810c0b10f0a82da2f79585d66f803a51
4
- data.tar.gz: 1f6d3ed4821cb0d8652a7659ea9ab6d60643901b307f7e925e818b69fbc5db2c
3
+ metadata.gz: 4a7ed672076127c57c4691c730e0a1545edaa1659573652b91e4a5db2bab3f02
4
+ data.tar.gz: 438a6d6e9693ae19080604e642709aa0381cd3486e4f562b175300549cf417f9
5
5
  SHA512:
6
- metadata.gz: ff5e1d66975a9406fb832e6a742679cc018507223078fac096776fd51324def81a022db3b1324456fa86f2b36f92ce7b79f4cce05c93567e48b25fc02a1ff25d
7
- data.tar.gz: bd898dedfdfa333c5eb7e69d55883585218bcc7ed34c6318b33fd1cf9cf9d66fdb44b4fdeab7f5c87c8ebba3937574545fbc1ba1955e78227f2334fb8acbea77
6
+ metadata.gz: 7c203ef2a97ed945256f138561359f7c4a32f79417b3caaded682f12a56ff2737b2becfd5e9994e133c547a2e455974e2b1255749ff61ada8e5c8f20aaa95721
7
+ data.tar.gz: 87908e166b10ef36fd3404d5f80cfca03baa5ac12e974947d3e8d9e65b93d73919c0159c0d6d77d62ff33e1eb100dc37ab3bf96fa455e7eec370bc582986c9e7
data/Cargo.lock CHANGED
@@ -1644,7 +1644,7 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
1644
1644
 
1645
1645
  [[package]]
1646
1646
  name = "itsi-server"
1647
- version = "0.2.10"
1647
+ version = "0.2.12"
1648
1648
  dependencies = [
1649
1649
  "argon2",
1650
1650
  "async-channel",
@@ -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())?;
@@ -11,7 +11,8 @@ module Itsi
11
11
  include ResponseStatusShortcodes
12
12
  attr_accessor :hijacked
13
13
 
14
- EMPTY_IO = StringIO.new("")
14
+ EMPTY_IO = StringIO.new("").tap { |io| io.set_encoding(Encoding::ASCII_8BIT) }
15
+
15
16
  RACK_HEADER_MAP = StandardHeaders::ALL.map do |header|
16
17
  rack_form = if header == "content-type"
17
18
  "CONTENT_TYPE"
@@ -49,9 +50,7 @@ module Itsi
49
50
  "rack.version" => nil,
50
51
  "rack.url_scheme" => "",
51
52
  "rack.input" => "",
52
- "rack.hijack" => "",
53
- "CONTENT_TYPE" => nil,
54
- "CONTENT_LENGTH" => nil
53
+ "rack.hijack" => ""
55
54
  }.freeze
56
55
 
57
56
  def to_rack_env
@@ -75,20 +74,20 @@ module Itsi
75
74
  env["rack.hijack"] = method(:hijack)
76
75
  headers.each do |(k, v)|
77
76
  env[case k
78
- when "content-type" then "CONTENT_TYPE"
79
- when "content-length" then "CONTENT_LENGTH"
80
- when "accept" then "HTTP_ACCEPT"
81
- when "accept-encoding" then "HTTP_ACCEPT_ENCODING"
82
- when "accept-language" then "HTTP_ACCEPT_LANGUAGE"
83
- when "user-agent" then "HTTP_USER_AGENT"
84
- when "referer" then "HTTP_REFERER"
85
- when "origin" then "HTTP_ORIGIN"
86
- when "cookie" then "HTTP_COOKIE"
87
- when "authorization" then "HTTP_AUTHORIZATION"
88
- when "x-forwarded-for" then "HTTP_X_FORWARDED_FOR"
89
- when "x-forwarded-proto" then "HTTP_X_FORWARDED_PROTO"
90
- else RACK_HEADER_MAP[k]
91
- end
77
+ when "content-type" then "CONTENT_TYPE"
78
+ when "content-length" then "CONTENT_LENGTH"
79
+ when "accept" then "HTTP_ACCEPT"
80
+ when "accept-encoding" then "HTTP_ACCEPT_ENCODING"
81
+ when "accept-language" then "HTTP_ACCEPT_LANGUAGE"
82
+ when "user-agent" then "HTTP_USER_AGENT"
83
+ when "referer" then "HTTP_REFERER"
84
+ when "origin" then "HTTP_ORIGIN"
85
+ when "cookie" then "HTTP_COOKIE"
86
+ when "authorization" then "HTTP_AUTHORIZATION"
87
+ when "x-forwarded-for" then "HTTP_X_FORWARDED_FOR"
88
+ when "x-forwarded-proto" then "HTTP_X_FORWARDED_PROTO"
89
+ else RACK_HEADER_MAP[k]
90
+ end
92
91
  ] = v
93
92
  end
94
93
  env
@@ -154,8 +153,8 @@ module Itsi
154
153
  def build_input_io
155
154
  case body_parts
156
155
  when nil then EMPTY_IO
157
- when String then StringIO.new(body_parts)
158
- when Array then File.open(body_parts.first, "rb")
156
+ when String then StringIO.new(body_parts).tap { |io| io.set_encoding(Encoding::ASCII_8BIT) }
157
+ when Array then File.open(body_parts.first, "rb").tap { |io| io.set_encoding(Encoding::ASCII_8BIT) }
159
158
  else body_parts
160
159
  end
161
160
  end
@@ -11,14 +11,17 @@ module Itsi
11
11
  ].flatten
12
12
 
13
13
  listing.each do |file|
14
- current = klass.subclasses
14
+ current = klass.subclasses.dup
15
15
  require file
16
16
  following = klass.subclasses
17
17
  new_class = (following - current).first
18
18
 
19
19
  documentation_file = "#{file[/(.*)\.rb/, 1]}.md"
20
20
  documentation_file = "#{file[%r{(.*)/[^/]+\.rb}, 1]}/_index.md" unless File.exist?(documentation_file)
21
- next unless File.exist?(documentation_file) && new_class
21
+ unless File.exist?(documentation_file) && new_class
22
+ new_class&.documentation "Documentation not found"
23
+ next
24
+ end
22
25
 
23
26
  new_class.documentation IO.read(documentation_file)
24
27
  .gsub(/^---.*?\n.*?-+/m, "") # Strip frontmatter
@@ -5,9 +5,10 @@ prev: deny_list/
5
5
  next: controller/
6
6
  ---
7
7
 
8
+ > If you're after running a rack app using a fully-featured framework, e.g. a Ruby on Rails or Sinatra, take a look at the [Rackup File](/middleware/rackup_file) middleware instead.
9
+
8
10
  The **endpoint** middleware allows you to define an ultra light-weight, inline, Ruby endpoint.
9
11
 
10
- > If you're after running a rack app using a fully-featured framework, e.g. a Ruby on Rails or Sinatra, take a look at the [Rackup File](/middleware/rackup_file) middleware instead.
11
12
  This feature can be useful for quickly prototyping, building small pieces of isolated functionality, or minimal endpoints where high throughput is essential.
12
13
 
13
14
  `endpoint` has several variants, that further restrict the endpoint to respond to specific HTTP methods:
@@ -26,12 +26,13 @@ module Itsi
26
26
  paths: Array(Or(Type(String), Type(Regexp))),
27
27
  handler: Type(Proc) & Required(),
28
28
  http_methods: Array(Type(String)),
29
+ script_name: Type(String).default(nil),
29
30
  nonblocking: Bool()
30
31
  }
31
32
  end
32
33
 
33
- def initialize(location, path="", handler=nil, http_methods: [], nonblocking: false, &handler_proc)
34
- location.endpoint(path, handler, http_methods: ["DELETE"], nonblocking: nonblocking, &handler_proc)
34
+ def initialize(location, path="", handler=nil, http_methods: [], nonblocking: false, script_name: nil, &handler_proc) # rubocop:disable Metrics/ParameterLists,Lint/MissingSuper
35
+ location.endpoint(path, handler, http_methods: ["DELETE"], nonblocking: nonblocking, script_name: script_name, &handler_proc)
35
36
  end
36
37
 
37
38
  def build!
@@ -28,17 +28,24 @@ module Itsi
28
28
  paths: Array(Or(Type(String), Type(Regexp))),
29
29
  handler: Type(Proc) & Required(),
30
30
  http_methods: Array(Type(String)),
31
+ script_name: Type(String).default(nil),
31
32
  nonblocking: Bool()
32
33
  }
33
34
  end
34
35
 
35
- def initialize(location, path="", handler=nil, http_methods: [], nonblocking: false, &handler_proc)
36
+ def initialize(location, path="", handler=nil, http_methods: [], nonblocking: false, script_name: nil, &handler_proc)
36
37
  raise "Can not combine a controller method and inline handler" if handler && handler_proc
37
38
  handler_proc = location.controller.method(handler).to_proc if handler.is_a?(Symbol) || handler.is_a?(String)
38
39
 
39
40
  super(
40
41
  location,
41
- { paths: Array(path), handler: handler_proc, http_methods: http_methods, nonblocking: nonblocking }
42
+ {
43
+ paths: Array(path),
44
+ handler: handler_proc,
45
+ http_methods: http_methods,
46
+ nonblocking: nonblocking,
47
+ script_name: script_name
48
+ }
42
49
  )
43
50
 
44
51
  num_required, keywords = Itsi::Server::TypedHandlers::SourceParser.extract_expr_from_source_location(handler_proc)
@@ -75,7 +82,11 @@ module Itsi
75
82
 
76
83
  def build!
77
84
  params = @params
78
- app = { preloader: -> { params[:handler] }, nonblocking: @params[:nonblocking] }
85
+ app = {
86
+ preloader: -> { params[:handler] },
87
+ nonblocking: @params[:nonblocking],
88
+ script_name: @params[:script_name]
89
+ }
79
90
 
80
91
  if @params[:paths] == [""] && @params[:http_methods].empty?
81
92
  location.middleware[:app] = app
@@ -26,12 +26,13 @@ module Itsi
26
26
  paths: Array(Or(Type(String), Type(Regexp))),
27
27
  handler: Type(Proc) & Required(),
28
28
  http_methods: Array(Type(String)),
29
+ script_name: Type(String).default(nil),
29
30
  nonblocking: Bool()
30
31
  }
31
32
  end
32
33
 
33
- def initialize(location, path="", handler=nil, http_methods: [], nonblocking: false, &handler_proc)
34
- location.endpoint(path, handler, http_methods: ["GET", "HEAD"], nonblocking: nonblocking, &handler_proc)
34
+ def initialize(location, path="", handler=nil, http_methods: [], nonblocking: false, script_name: nil, &handler_proc) # rubocop:disable Lint/MissingSuper,Metrics/ParameterLists
35
+ location.endpoint(path, handler, http_methods: ["GET", "HEAD"], nonblocking: nonblocking, script_name: script_name, &handler_proc)
35
36
  end
36
37
 
37
38
  def build!
@@ -26,12 +26,13 @@ module Itsi
26
26
  paths: Array(Or(Type(String), Type(Regexp))),
27
27
  handler: Type(Proc) & Required(),
28
28
  http_methods: Array(Type(String)),
29
+ script_name: Type(String).default(nil),
29
30
  nonblocking: Bool()
30
31
  }
31
32
  end
32
33
 
33
- def initialize(location, path="", handler=nil, http_methods: [], nonblocking: false, &handler_proc)
34
- location.endpoint(path, handler, http_methods: ["PATCH"], nonblocking: nonblocking, &handler_proc)
34
+ def initialize(location, path="", handler=nil, http_methods: [], nonblocking: false, script_name: nil, &handler_proc) # rubocop:disable Lint/MissingSuper
35
+ location.endpoint(path, handler, http_methods: ["PATCH"], nonblocking: nonblocking, script_name: script_name, &handler_proc)
35
36
  end
36
37
 
37
38
  def build!
@@ -26,12 +26,13 @@ module Itsi
26
26
  paths: Array(Or(Type(String), Type(Regexp))),
27
27
  handler: Type(Proc) & Required(),
28
28
  http_methods: Array(Type(String)),
29
+ script_name: Type(String).default(nil),
29
30
  nonblocking: Bool()
30
31
  }
31
32
  end
32
33
 
33
- def initialize(location, path="", handler=nil, http_methods: [], nonblocking: false, &handler_proc)
34
- location.endpoint(path, handler, http_methods: ["POST"], nonblocking: nonblocking, &handler_proc)
34
+ def initialize(location, path="", handler=nil, http_methods: [], nonblocking: false, script_name: nil, &handler_proc) # rubocop:disable Lint/MissingSuper
35
+ location.endpoint(path, handler, http_methods: ["POST"], nonblocking: nonblocking, script_name: script_name, &handler_proc)
35
36
  end
36
37
 
37
38
  def build!
@@ -26,12 +26,13 @@ module Itsi
26
26
  paths: Array(Or(Type(String), Type(Regexp))),
27
27
  handler: Type(Proc) & Required(),
28
28
  http_methods: Array(Type(String)),
29
+ script_name: Type(String).default(nil),
29
30
  nonblocking: Bool()
30
31
  }
31
32
  end
32
33
 
33
- def initialize(location, path="", handler=nil, http_methods: [], nonblocking: false, &handler_proc)
34
- location.endpoint(path, handler, http_methods: ["PUT"], nonblocking: nonblocking, &handler_proc)
34
+ def initialize(location, path="", handler=nil, http_methods: [], nonblocking: false, script_name: nil, &handler_proc) # rubocop:disable Lint/MissingSuper,Metrics/ParameterLists
35
+ location.endpoint(path, handler, http_methods: ["PUT"], nonblocking: nonblocking, script_name: script_name, &handler_proc)
35
36
  end
36
37
 
37
38
  def build!
@@ -44,6 +44,24 @@ $ curl http://0.0.0.0:3000/root/child_path
44
44
  :/root/child_path
45
45
  ```
46
46
 
47
+ ### `SCRIPT_NAME` and `PATH_INFO` Rack ENV variables.
48
+ Rack applications mounted at a subpath will, by default, receive a `SCRIPT_NAME` value that includes the subpath at which the app is mounted, and a `PATH_INFO` value that is relative to the subpath at which the rack app is mounted.
49
+ If you wish to use location blocks only to control the middleware that applies to a rack app, but still have it behave as if it were mounted elsewhere (e.g. at the root), you can explicitly set the `script_name` option.
50
+ E.g.
51
+
52
+ ```ruby
53
+ location "/subpath/*" do
54
+ rackup_file "config.ru", script_name: '/'
55
+ end
56
+ ```
57
+
58
+ ```bash
59
+ # Our script-name override kicks in here, even though the app is mounted under `/subpath`
60
+ $ curl http://0.0.0.0:3000/subpath/child_path
61
+ /:/subpath/child_path
62
+ ```
63
+
64
+
47
65
  ### Options
48
66
  * `nonblocking` (default false). Determines whether requests sent to this Rack application should be run on non-blocking threads. Only applies if running in hybrid (non-blocking and blocking thread pool) mode. Otherwise this is a no-op and will run in whatever mode is set globally.
49
67
  * `sendfile` (default true). Determines whether Itsi should respect the `X-Sendfile` header set by the Rack application and use the `sendfile` function to efficiently send files. (Despite the name, this does not use the OS-level `sendfile` system call). Note. Itsi enforces the restriction that the referenced file must be within a child directory of the application root.
@@ -15,6 +15,7 @@ module Itsi
15
15
  schema do
16
16
  {
17
17
  nonblocking: Bool().default(false),
18
+ script_name: Type(String).default(nil),
18
19
  sendfile: Bool().default(true)
19
20
  }
20
21
  end
@@ -31,6 +32,7 @@ module Itsi
31
32
  preloader: -> { @app },
32
33
  sendfile: @params[:sendfile],
33
34
  nonblocking: @params[:nonblocking],
35
+ script_name: @params[:script_name],
34
36
  base_path: "^(?<base_path>#{location.paths_from_parent.gsub(/\.\*\)$/, ")")}).*$"
35
37
  }
36
38
  location.middleware[:app] = app_args
@@ -39,7 +39,6 @@ run(Rack::Builder.app do
39
39
  use Rack::CommonLogger
40
40
  run ->(env) { [200, { 'content-type' => 'text/plain' }, [[env['SCRIPT_NAME'], env['PATH_INFO']].join(":") ] ] }
41
41
  end)
42
-
43
42
  ```
44
43
 
45
44
  ```bash
@@ -50,6 +49,26 @@ $ curl http://0.0.0.0:3000/root/child_path
50
49
  :/root/child_path
51
50
  ```
52
51
 
52
+ ### `SCRIPT_NAME` and `PATH_INFO` Rack ENV variables.
53
+ Rack applications mounted at a subpath will, by default, receive a `SCRIPT_NAME` value that includes the subpath at which the app is mounted, and a `PATH_INFO` value that is relative to the subpath at which the rack app is mounted.
54
+ If you wish to use location blocks only to control the middleware that applies to a rack app, but still have it behave as if it were mounted elsewhere (e.g. at the root), you can explicitly set the `script_name` option.
55
+ E.g.
56
+
57
+ ```ruby
58
+ location "/subpath/*" do
59
+ run(Rack::Builder.app do
60
+ use Rack::CommonLogger
61
+ run ->(env) { [200, { 'content-type' => 'text/plain' }, [[env['SCRIPT_NAME'], env['PATH_INFO']].join(":") ] ] }
62
+ end, script_name: '/')
63
+ end
64
+ ```
65
+
66
+ ```bash
67
+ # Our script-name override kicks in here, even though the app is mounted under `/subpath`
68
+ $ curl http://0.0.0.0:3000/subpath/child_path
69
+ /:/subpath/child_path
70
+ ```
71
+
53
72
  ### Options
54
73
  * `nonblocking` (default false). Determines whether requests sent to this Rack application should be run on non-blocking threads. Only applies if running in hybrid (non-blocking and blocking thread pool) mode. Otherwise this is a no-op and will run in whatever mode is set globally.
55
74
  * `sendfile` (default true). Determines whether Itsi should respect the `X-Sendfile` header set by the Rack application and use the `sendfile` function to efficiently send files. (Despite the name, this does not use the OS-level `sendfile` system call). Note. Itsi enforces the restriction that the referenced file must be within a child directory of the application root.
@@ -15,6 +15,7 @@ module Itsi
15
15
  schema do
16
16
  {
17
17
  nonblocking: Bool().default(false),
18
+ script_name: Type(String).default(nil),
18
19
  sendfile: Bool().default(true)
19
20
  }
20
21
  end
@@ -30,6 +31,7 @@ module Itsi
30
31
  preloader: -> { Itsi::Server::RackInterface.for(@app) },
31
32
  sendfile: @params[:sendfile],
32
33
  nonblocking: @params[:nonblocking],
34
+ script_name: @params[:script_name],
33
35
  base_path: "^(?<base_path>#{location.paths_from_parent.gsub(/\.\*\)$/, ')')}).*$"
34
36
  }
35
37
  location.middleware[:app] = app_args
@@ -9,11 +9,11 @@ Itsi can automatically generate TLS certificates for you, both in development an
9
9
  To automatically generate a TLS certificate in development, just bind using the `https` scheme.
10
10
  E.g.
11
11
  ```ruby {filename=Itsi.rb}
12
- bind "https://localhost"
12
+ bind "https://0.0.0.0"
13
13
 
14
14
  or
15
15
 
16
- bind "https://localhost:8443"
16
+ bind "https://0.0.0.0:8443"
17
17
  ```
18
18
  Itsi will generate a local development CA for you if it does not yet exist, then use this to
19
19
  sign a just-in-time certificate for your server.
@@ -0,0 +1,18 @@
1
+ ---
2
+ title: Ruby Thread Request Backlog Size
3
+ url: /options/ruby_thread_request_backlog_size
4
+ ---
5
+
6
+ Configures the size of the backlog queue for incoming requests in the Ruby thread pool.
7
+ Up to this many requests can be queued at once before the server rejects further requests to Ruby workers (note this does not block other requests to proxied hosts or for static assets).
8
+
9
+ The default value is `30 x number of threads`.
10
+
11
+ ## Configuration
12
+ ```ruby {filename=Itsi.rb}
13
+ ruby_thread_request_backlog_size 20
14
+ ```
15
+
16
+ ```ruby {filename=Itsi.rb}
17
+ ruby_thread_request_backlog_size 100
18
+ ```
@@ -0,0 +1,19 @@
1
+ module Itsi
2
+ class Server
3
+ module Config
4
+ class RubyThreadRequestBacklogSize < Option
5
+
6
+ insert_text <<~SNIPPET
7
+ ruby_thread_request_backlog_size ${1|10,25,50,100|}
8
+ SNIPPET
9
+
10
+ detail "The maximum number of requests that can be queued for processing by the Ruby thread."
11
+
12
+ schema do
13
+ (Type(Integer)).default(nil)
14
+ end
15
+
16
+ end
17
+ end
18
+ end
19
+ end
@@ -3,7 +3,7 @@ title: Scheduler Threads
3
3
  url: /options/scheduler_threads
4
4
  ---
5
5
 
6
- You can explicitly spawn a pool of non-blocking scheduler threads and divide work across traditional blocking and non-blocking threads, using [location](/middleware/location) blocks.
6
+ You can explicitly spawn a pool of non-blocking scheduler threads and divide work across traditional/blocking and non-blocking threads, using [location](/middleware/location) blocks.
7
7
 
8
8
  This allows you to safely dip your toes into using non-blocking threads for specific I/O heavy operations without having to port an entire application to non-blocking I/O.
9
9
 
@@ -25,10 +25,17 @@ fiber_scheduler true
25
25
  # We mount the same app *twice*.
26
26
  # For a specific route prefix, all requests will be sent to non blocking threads.
27
27
  # All others fall through to the default mount
28
+
28
29
  location "/heavy_io/*" do
30
+ # You can optionally use the `script_name: ""` option here to set the base path for the mounted app (useful if a nested app
31
+ # should still serve requests as if it was mounted at the root).
32
+ # Otherwise it will infer the script-name based on the parent location block.
29
33
  rackup_file "./config.ru", nonblocking: true
30
34
  end
31
35
 
32
36
  rackup_file "./config.ru"
33
37
 
34
38
  ```
39
+ ## Examples.
40
+
41
+ See [https://github.com/wouterken/itsi/tree/main/examples/hybrid_scheduler_mode](hybrid_scheduler_mode) example in the Git repository.
@@ -134,6 +134,9 @@ module Itsi
134
134
  oob_gc_responses_threshold: args.fetch(:oob_gc_responses_threshold) do
135
135
  itsifile_config.fetch(:oob_gc_responses_threshold, nil)
136
136
  end,
137
+ ruby_thread_request_backlog_size: args.fetch(:ruby_thread_request_backlog_size) do
138
+ itsifile_config.fetch(:ruby_thread_request_backlog_size, nil)
139
+ end,
137
140
  log_level: args.fetch(:log_level) { itsifile_config.fetch(:log_level, nil) },
138
141
  log_format: args.fetch(:log_format) { itsifile_config.fetch(:log_format, nil) },
139
142
  log_target: args.fetch(:log_target) { itsifile_config.fetch(:log_target, nil) },
@@ -31,8 +31,8 @@ fiber_scheduler nil
31
31
 
32
32
  # If you bind to https, without specifying a certificate, Itsi will use a self-signed certificate.
33
33
  # The self-signed certificate will use a CA generated for your host and stored inside `ITSI_LOCAL_CA_DIR` (Defaults to ~/.itsi)
34
- # bind "https://localhost:3000"
35
- # bind "https://localhost:3000?domains=dev.itsi.fyi"
34
+ # bind "https://0.0.0.0:3000"
35
+ # bind "https://0.0.0.0:3000?domains=dev.itsi.fyi"
36
36
  #
37
37
  # If you want to use let's encrypt to generate you a real certificate you and pass cert=acme and an acme_email address to generate one.
38
38
  # bind "https://itsi.fyi?cert=acme&acme_email=admin@itsi.fyi"
@@ -8,10 +8,11 @@ module Rack
8
8
  port = options.fetch(:Port, 3001)
9
9
  ::Itsi::Server.start(
10
10
  {
11
- app: app,
12
11
  binds: ["http://#{host}:#{port}"]
13
12
  }
14
- )
13
+ ) do
14
+ run app
15
+ end
15
16
  end
16
17
  end
17
18
  end
@@ -40,13 +40,12 @@ module Itsi
40
40
  # 2. Set Headers
41
41
  body_streamer = streaming_body?(body) ? body : headers.delete("rack.hijack")
42
42
  headers.each do |key, value|
43
- unless value.is_a?(Array)
43
+ if value.is_a?(Array)
44
+ value.each do |v|
45
+ response[key] = v
46
+ end
47
+ elsif value.is_a?(String)
44
48
  response[key] = value
45
- next
46
- end
47
-
48
- value.each do |v|
49
- response[key] = v
50
49
  end
51
50
  end
52
51
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Server
5
- VERSION = "0.2.10"
5
+ VERSION = "0.2.12"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: itsi-server
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
@@ -91,7 +91,6 @@ files:
91
91
  - ".rubocop.yml"
92
92
  - Cargo.lock
93
93
  - Cargo.toml
94
- - README.md
95
94
  - Rakefile
96
95
  - exe/itsi
97
96
  - ext/itsi_acme/Cargo.toml
@@ -498,6 +497,8 @@ files:
498
497
  - lib/itsi/server/config/options/reuse_address.rb
499
498
  - lib/itsi/server/config/options/reuse_port.md
500
499
  - lib/itsi/server/config/options/reuse_port.rb
500
+ - lib/itsi/server/config/options/ruby_thread_request_backlog_size.md
501
+ - lib/itsi/server/config/options/ruby_thread_request_backlog_size.rb
501
502
  - lib/itsi/server/config/options/scheduler_threads.md
502
503
  - lib/itsi/server/config/options/scheduler_threads.rb
503
504
  - lib/itsi/server/config/options/shutdown_timeout.md
@@ -533,7 +534,6 @@ files:
533
534
  - lib/itsi/standard_headers.rb
534
535
  - lib/ruby_lsp/itsi/addon.rb
535
536
  - lib/shell_completions/completions.rb
536
- - sig/itsi_server.rbs
537
537
  homepage: https://itsi.fyi
538
538
  licenses:
539
539
  - MIT
data/README.md DELETED
@@ -1,49 +0,0 @@
1
- ---
2
- type: docs
3
- sidebar:
4
- exclude: true
5
- ---
6
-
7
- # ItsiServer
8
-
9
- TODO: Delete this and the text below, and describe your gem
10
-
11
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/itsi_server`. To experiment with that code, run `bin/console` for an interactive prompt.
12
-
13
- ## Installation
14
-
15
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
16
-
17
- Install the gem and add to the application's Gemfile by executing:
18
-
19
- ```bash
20
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
21
- ```
22
-
23
- If bundler is not being used to manage dependencies, install the gem by executing:
24
-
25
- ```bash
26
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
27
- ```
28
-
29
- ## Usage
30
-
31
- TODO: Write usage instructions here
32
-
33
- ## Development
34
-
35
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
36
-
37
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
38
-
39
- ## Contributing
40
-
41
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/itsi_server. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/itsi_server/blob/master/CODE_OF_CONDUCT.md).
42
-
43
- ## License
44
-
45
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
46
-
47
- ## Code of Conduct
48
-
49
- Everyone interacting in the ItsiServer project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/itsi_server/blob/master/CODE_OF_CONDUCT.md).
data/sig/itsi_server.rbs DELETED
@@ -1,4 +0,0 @@
1
- module ItsiServer
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end