itsi-scheduler 0.2.26 → 0.2.27

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.
@@ -1,16 +1,17 @@
1
1
  use crate::default_responses::{NOT_FOUND_RESPONSE, TIMEOUT_RESPONSE};
2
2
  use crate::ruby_types::itsi_server::itsi_server_config::ItsiServerTokenPreference;
3
3
  use crate::server::http_message_types::{
4
- ConversionExt, HttpRequest, HttpResponse, RequestExt, ResponseFormat,
4
+ ConversionExt, HttpBody, HttpRequest, HttpResponse, RequestExt, ResponseFormat,
5
5
  };
6
6
  use crate::server::lifecycle_event::LifecycleEvent;
7
7
  use crate::server::middleware_stack::MiddlewareLayer;
8
8
  use crate::server::serve_strategy::acceptor::AcceptorArgs;
9
9
  use crate::server::signal::{send_lifecycle_event, SHUTDOWN_REQUESTED};
10
+ use bytes::Bytes;
10
11
  use chrono::{self, DateTime, Local};
11
12
  use either::Either;
12
13
  use http::header::ACCEPT_ENCODING;
13
- use http::{HeaderValue, Request};
14
+ use http::{HeaderValue, Request, StatusCode};
14
15
  use hyper::body::Incoming;
15
16
  use regex::Regex;
16
17
  use smallvec::SmallVec;
@@ -174,6 +175,7 @@ impl HttpRequestContext {
174
175
  const SERVER_TOKEN_VERSION: HeaderValue =
175
176
  HeaderValue::from_static(concat!("Itsi/", env!("CARGO_PKG_VERSION")));
176
177
  const SERVER_TOKEN_NAME: HeaderValue = HeaderValue::from_static("Itsi");
178
+ const TEXT_PLAIN_UTF8: HeaderValue = HeaderValue::from_static("text/plain; charset=utf-8");
177
179
 
178
180
  impl ItsiHttpService {
179
181
  pub async fn handle_request(&self, req: Request<Incoming>) -> itsi_error::Result<HttpResponse> {
@@ -188,14 +190,15 @@ impl ItsiHttpService {
188
190
  let token_preference = self.server_params.itsi_server_token_preference;
189
191
 
190
192
  let service_future = async move {
191
- let middleware_stack = self
192
- .server_params
193
- .middleware
194
- .get()
195
- .unwrap()
196
- .stack_for(&req)
197
- .unwrap();
198
- let (stack, matching_pattern) = middleware_stack;
193
+ if let Some(acme_response) = self.acme_http01_response(&req) {
194
+ return Ok(acme_response);
195
+ }
196
+
197
+ let Some((stack, matching_pattern)) =
198
+ self.server_params.middleware.get().unwrap().stack_for(&req)
199
+ else {
200
+ return Ok(NOT_FOUND_RESPONSE.to_http_response(accept).await);
201
+ };
199
202
  let mut resp: Option<HttpResponse> = None;
200
203
 
201
204
  let mut context =
@@ -267,4 +270,42 @@ impl ItsiHttpService {
267
270
  service_future.await
268
271
  }
269
272
  }
273
+
274
+ fn acme_http01_response(&self, req: &HttpRequest) -> Option<HttpResponse> {
275
+ let host = normalize_host_header(req.header("host")?)?;
276
+ let managers = self.server_params.acme_managers.read();
277
+ let key_authorization = managers
278
+ .iter()
279
+ .find_map(|(_, manager)| manager.http01_response(host, req.uri().path()))?;
280
+
281
+ let mut builder = http::Response::builder()
282
+ .status(StatusCode::OK)
283
+ .header(http::header::CONTENT_TYPE, TEXT_PLAIN_UTF8);
284
+
285
+ if req.method() == http::Method::HEAD {
286
+ builder = builder.header(http::header::CONTENT_LENGTH, "0");
287
+ return builder.body(HttpBody::empty()).ok();
288
+ }
289
+
290
+ builder
291
+ .header(
292
+ http::header::CONTENT_LENGTH,
293
+ key_authorization.len().to_string(),
294
+ )
295
+ .body(HttpBody::full(Bytes::from(key_authorization)))
296
+ .ok()
297
+ }
298
+ }
299
+
300
+ fn normalize_host_header(host: &str) -> Option<&str> {
301
+ let host = host.trim();
302
+ if host.is_empty() {
303
+ return None;
304
+ }
305
+
306
+ if let Some(stripped) = host.strip_prefix('[') {
307
+ return stripped.split(']').next();
308
+ }
309
+
310
+ Some(host.split(':').next().unwrap_or(host))
270
311
  }
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Scheduler
5
- VERSION = "0.2.26"
5
+ VERSION = "0.2.27"
6
6
  end
7
7
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "etc"
4
+
3
5
  require_relative "scheduler/version"
4
6
  require_relative "scheduler/native_extension"
5
7
  require_relative "schedule_refinement"
@@ -7,6 +9,7 @@ require_relative "schedule_refinement"
7
9
  module Itsi
8
10
  class Scheduler
9
11
  class Error < StandardError; end
12
+ WorkRequest = Struct.new(:fiber, :work, :result, :error, keyword_init: true)
10
13
 
11
14
  def self.resume_token
12
15
  @resume_token ||= 0
@@ -17,12 +20,14 @@ module Itsi
17
20
  @join_waiters = {}.compare_by_identity
18
21
  @token_map = {}.compare_by_identity
19
22
  @resume_tokens = {}.compare_by_identity
23
+ @timeout_requests = {}
20
24
  @unblocked = [[], []]
21
25
  @unblock_idx = 0
22
26
  @unblocked_mux = Mutex.new
23
27
  @resume_fiber = method(:resume_fiber).to_proc
24
28
  @resume_fiber_with_readiness = method(:resume_fiber_with_readiness).to_proc
25
29
  @resume_blocked = method(:resume_blocked).to_proc
30
+ setup_worker_pool
26
31
  end
27
32
 
28
33
  def block(_, timeout, fiber = Fiber.current, token = Scheduler.resume_token)
@@ -33,6 +38,7 @@ module Itsi
33
38
  @token_map[fiber] = token
34
39
  Fiber.yield
35
40
  ensure
41
+ cancel_wait(token)
36
42
  @resume_tokens.delete(token)
37
43
  @token_map.delete(fiber)
38
44
  @join_waiters.delete(fiber)
@@ -61,6 +67,60 @@ module Itsi
61
67
  block nil, duration
62
68
  end
63
69
 
70
+ def timeout_after(duration, klass = Timeout::Error, message = "execution expired")
71
+ fiber = Fiber.current
72
+ token = Scheduler.resume_token
73
+ exception = klass.is_a?(Class) ? klass.new(message) : klass
74
+ @timeout_requests[token] = [fiber, exception]
75
+ start_timer(duration, token)
76
+ yield duration
77
+ ensure
78
+ clear_timer(token) if token
79
+ @timeout_requests.delete(token) if token
80
+ end
81
+
82
+ def fiber_interrupt(fiber, exception)
83
+ cancel_wait(@token_map[fiber]) if @token_map.key?(fiber)
84
+ fiber.raise(exception)
85
+ true
86
+ rescue FiberError
87
+ false
88
+ end
89
+
90
+ def blocking_operation_wait(work)
91
+ request = WorkRequest.new(fiber: Fiber.current, work: work)
92
+ @worker_queue << request
93
+ block(nil, nil, request.fiber)
94
+ raise request.error if request.error
95
+
96
+ request.result
97
+ end
98
+
99
+ def io_select(readables, writables, exceptables, timeout)
100
+ readables = Array(readables).compact
101
+ writables = Array(writables).compact
102
+ exceptables = Array(exceptables).compact
103
+ ios = (readables + writables + exceptables).uniq
104
+
105
+ if ios.length == 1
106
+ io = ios.first
107
+ events = 0
108
+ events |= IO::READABLE if readables.include?(io)
109
+ events |= IO::WRITABLE if writables.include?(io)
110
+ events |= IO::PRIORITY if exceptables.include?(io)
111
+ readiness = io_wait(io, events, timeout)
112
+ return nil unless readiness
113
+
114
+ return [
115
+ (readiness & IO::READABLE).zero? ? [] : readables.select { |entry| entry == io },
116
+ (readiness & IO::WRITABLE).zero? ? [] : writables.select { |entry| entry == io },
117
+ (readiness & IO::PRIORITY).zero? ? [] : exceptables.select { |entry| entry == io }
118
+ ]
119
+ end
120
+
121
+ blocking_operation_wait(-> { IO.select(readables, writables, exceptables, timeout) })
122
+ end
123
+
64
124
  def tick
65
125
  events = fetch_due_events
66
126
  timers = fetch_due_timers
@@ -72,6 +132,12 @@ module Itsi
72
132
  end
73
133
 
74
134
  def resume_fiber(token)
135
+ if (request = @timeout_requests.delete(token))
136
+ fiber, exception = request
137
+ fiber_interrupt(fiber, exception)
138
+ return
139
+ end
140
+
75
141
  if (fiber = @resume_tokens.delete(token))
76
142
  fiber.resume
77
143
  end
@@ -126,6 +192,7 @@ module Itsi
126
192
  def close
127
193
  run
128
194
  ensure
195
+ shutdown_worker_pool
129
196
  @closed ||= true
130
197
  freeze
131
198
  end
@@ -133,12 +200,17 @@ module Itsi
133
200
  # Need to defer to Process::Status rather than our extension
134
201
  # as we don't have a means of creating our own Process::Status.
135
202
  def process_wait(pid, flags)
136
- result = nil
137
- thread = Thread.new do
138
- result = Process::Status.wait(pid, flags)
139
- end
140
- thread.join
141
- result
203
+ blocking_operation_wait(-> { Process::Status.wait(pid, flags) })
204
+ end
205
+
206
+ def address_resolve(hostname)
207
+ blocking_operation_wait(-> { native_address_resolve(hostname) })
208
+ end
209
+
210
+ def process_fork
211
+ shutdown_worker_pool
212
+ setup_worker_pool
213
+ nil
142
214
  end
143
215
 
144
216
  def closed?
@@ -149,5 +221,48 @@ module Itsi
149
221
  def fiber(&blk)
150
222
  Fiber.new(blocking: false, &blk).tap(&:resume)
151
223
  end
224
+
225
+ private
226
+
227
+ def setup_worker_pool
228
+ @worker_stop_token = Object.new
229
+ @worker_queue = Queue.new
230
+ @worker_threads = Array.new(worker_pool_size) { start_worker_thread }
231
+ end
232
+
233
+ def start_worker_thread
234
+ Thread.new do
235
+ Thread.current.report_on_exception = false
236
+ Thread.current.thread_variable_set(:fork_safe, true)
237
+
238
+ loop do
239
+ request = @worker_queue.pop
240
+ break if request.equal?(@worker_stop_token)
241
+
242
+ begin
243
+ request.result = request.work.call
244
+ rescue Exception => exception
245
+ request.error = exception
246
+ ensure
247
+ unblock(nil, request.fiber)
248
+ end
249
+ end
250
+ end
251
+ end
252
+
253
+ def shutdown_worker_pool
254
+ return unless @worker_threads
255
+
256
+ @worker_threads.size.times { @worker_queue << @worker_stop_token }
257
+ @worker_threads.each(&:join)
258
+ @worker_threads.clear
259
+ end
260
+
261
+ def worker_pool_size
262
+ size = ENV.fetch("ITSI_WORKER_POOL_SIZE", Etc.nprocessors.to_s).to_i
263
+ size.positive? ? size : 1
264
+ rescue StandardError
265
+ 1
266
+ end
152
267
  end
153
268
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: itsi-scheduler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.26
4
+ version: 0.2.27
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wouter Coppieters
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-13 00:00:00.000000000 Z
11
+ date: 2026-06-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys
@@ -53,6 +53,7 @@ files:
53
53
  - ext/itsi_acme/src/caches/no.rs
54
54
  - ext/itsi_acme/src/caches/test.rs
55
55
  - ext/itsi_acme/src/config.rs
56
+ - ext/itsi_acme/src/http_challenge.rs
56
57
  - ext/itsi_acme/src/https_helper.rs
57
58
  - ext/itsi_acme/src/incoming.rs
58
59
  - ext/itsi_acme/src/jose.rs