itsi 0.2.26 → 0.2.27.rc1

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +7 -3
  3. data/Rakefile +24 -3
  4. data/crates/itsi_acme/Cargo.toml +2 -1
  5. data/crates/itsi_acme/src/acceptor.rs +1 -1
  6. data/crates/itsi_acme/src/acme.rs +31 -3
  7. data/crates/itsi_acme/src/http_challenge.rs +81 -0
  8. data/crates/itsi_acme/src/https_helper.rs +3 -1
  9. data/crates/itsi_acme/src/jose.rs +6 -2
  10. data/crates/itsi_acme/src/lib.rs +2 -0
  11. data/crates/itsi_acme/src/resolver.rs +27 -4
  12. data/crates/itsi_acme/src/state.rs +183 -22
  13. data/crates/itsi_scheduler/Cargo.toml +1 -1
  14. data/crates/itsi_scheduler/src/itsi_scheduler.rs +115 -64
  15. data/crates/itsi_scheduler/src/lib.rs +2 -1
  16. data/crates/itsi_server/Cargo.toml +2 -1
  17. data/crates/itsi_server/src/lib.rs +15 -0
  18. data/crates/itsi_server/src/ruby_types/itsi_http_request.rs +9 -0
  19. data/crates/itsi_server/src/ruby_types/itsi_http_response.rs +95 -0
  20. data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +22 -1
  21. data/crates/itsi_server/src/ruby_types/itsi_server.rs +100 -0
  22. data/crates/itsi_server/src/server/binds/listener.rs +9 -24
  23. data/crates/itsi_server/src/server/binds/tls.rs +372 -67
  24. data/crates/itsi_server/src/services/itsi_http_service.rs +46 -2
  25. data/gems/scheduler/Cargo.lock +4011 -527
  26. data/gems/scheduler/Gemfile +8 -2
  27. data/gems/scheduler/Gemfile.lock +107 -0
  28. data/gems/scheduler/Rakefile +33 -9
  29. data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
  30. data/gems/scheduler/lib/itsi/scheduler.rb +121 -6
  31. data/gems/scheduler/test/helpers/test_helper.rb +2 -0
  32. data/gems/scheduler/test/test_address_resolve.rb +8 -2
  33. data/gems/scheduler/test/test_itsi_scheduler.rb +80 -0
  34. data/gems/scheduler/test/test_timeout_after.rb +102 -0
  35. data/gems/server/Cargo.lock +30 -1
  36. data/gems/server/Gemfile +2 -0
  37. data/gems/server/Gemfile.lock +123 -0
  38. data/gems/server/Rakefile +18 -5
  39. data/gems/server/lib/itsi/http_request.rb +10 -0
  40. data/gems/server/lib/itsi/server/rack_interface.rb +45 -2
  41. data/gems/server/lib/itsi/server/version.rb +1 -1
  42. data/gems/server/lib/itsi/server.rb +24 -0
  43. data/gems/server/test/acme/local_acme_challenges.rb +190 -0
  44. data/gems/server/test/helpers/local_acme.rb +218 -0
  45. data/gems/server/test/helpers/test_helper.rb +7 -9
  46. data/gems/server/test/middleware/endpoint.rb +9 -6
  47. data/gems/server/test/rack/test_rack_server.rb +79 -0
  48. data/lib/itsi/version.rb +1 -1
  49. metadata +12 -6
@@ -2,10 +2,16 @@ source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
4
 
5
- group :development, :test do
5
+ group :development do
6
6
  gem "bundler"
7
- gem "minitest", "~> 5.16"
8
7
  gem "rake", "~> 13.0"
9
8
  gem "rake-compiler"
10
9
  gem "rb_sys", "~> 0.9.91"
11
10
  end
11
+
12
+ group :test do
13
+ gem "activerecord"
14
+ gem "minitest", "~> 5.16"
15
+ gem "minitest-reporters"
16
+ gem "pg"
17
+ end
@@ -0,0 +1,107 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ itsi-scheduler (0.2.27.rc1)
5
+ rb_sys (~> 0.9.91)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ activemodel (8.1.3)
11
+ activesupport (= 8.1.3)
12
+ activerecord (8.1.3)
13
+ activemodel (= 8.1.3)
14
+ activesupport (= 8.1.3)
15
+ timeout (>= 0.4.0)
16
+ activesupport (8.1.3)
17
+ base64
18
+ bigdecimal
19
+ concurrent-ruby (~> 1.0, >= 1.3.1)
20
+ connection_pool (>= 2.2.5)
21
+ drb
22
+ i18n (>= 1.6, < 2)
23
+ json
24
+ logger (>= 1.4.2)
25
+ minitest (>= 5.1)
26
+ securerandom (>= 0.3)
27
+ tzinfo (~> 2.0, >= 2.0.5)
28
+ uri (>= 0.13.1)
29
+ ansi (1.6.0)
30
+ base64 (0.3.0)
31
+ bigdecimal (4.1.2)
32
+ builder (3.3.0)
33
+ concurrent-ruby (1.3.6)
34
+ connection_pool (3.0.2)
35
+ drb (2.2.3)
36
+ i18n (1.14.8)
37
+ concurrent-ruby (~> 1.0)
38
+ json (2.19.4)
39
+ logger (1.7.0)
40
+ minitest (5.27.0)
41
+ minitest-reporters (1.8.0)
42
+ ansi
43
+ builder
44
+ minitest (>= 5.0, < 7)
45
+ ruby-progressbar
46
+ pg (1.6.3)
47
+ pg (1.6.3-arm64-darwin)
48
+ rake (13.3.1)
49
+ rake-compiler (1.3.0)
50
+ rake
51
+ rake-compiler-dock (1.11.0)
52
+ rb_sys (0.9.126)
53
+ json (>= 2)
54
+ rake-compiler-dock (= 1.11.0)
55
+ ruby-progressbar (1.13.0)
56
+ securerandom (0.4.1)
57
+ timeout (0.6.1)
58
+ tzinfo (2.0.6)
59
+ concurrent-ruby (~> 1.0)
60
+ uri (1.1.1)
61
+
62
+ PLATFORMS
63
+ arm64-darwin-24
64
+ ruby
65
+
66
+ DEPENDENCIES
67
+ activerecord
68
+ bundler
69
+ itsi-scheduler!
70
+ minitest (~> 5.16)
71
+ minitest-reporters
72
+ pg
73
+ rake (~> 13.0)
74
+ rake-compiler
75
+ rb_sys (~> 0.9.91)
76
+
77
+ CHECKSUMS
78
+ activemodel (8.1.3) sha256=90c05cbe4cef3649b8f79f13016191ea94c4525ce4a5c0fb7ef909c4b91c8219
79
+ activerecord (8.1.3) sha256=8003be7b2466ba0a2a670e603eeb0a61dd66058fccecfc49901e775260ac70ab
80
+ activesupport (8.1.3) sha256=21a5e0dfbd4c3ddd9e1317ec6a4d782fa226e7867dc70b0743acda81a1dca20e
81
+ ansi (1.6.0) sha256=ac9ea0c0ea8d32fb4e271348e609963ac78882f34b73836c2a02b3622e666658
82
+ base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
83
+ bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
84
+ builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
85
+ concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
86
+ connection_pool (3.0.2) sha256=33fff5ba71a12d2aa26cb72b1db8bba2a1a01823559fb01d29eb74c286e62e0a
87
+ drb (2.2.3) sha256=0b00d6fdb50995fe4a45dea13663493c841112e4068656854646f418fda13373
88
+ i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
89
+ itsi-scheduler (0.2.27.rc1)
90
+ json (2.19.4) sha256=670a7d333fb3b18ca5b29cb255eb7bef099e40d88c02c80bd42a3f30fe5239ac
91
+ logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
92
+ minitest (5.27.0) sha256=2d3b17f8a36fe7801c1adcffdbc38233b938eb0b4966e97a6739055a45fa77d5
93
+ minitest-reporters (1.8.0) sha256=8ce5280fb73ad3178ae525454df169b6f28c1b38b1d088ea91815d3a370ba384
94
+ pg (1.6.3) sha256=1388d0563e13d2758c1089e35e973a3249e955c659592d10e5b77c468f628a99
95
+ pg (1.6.3-arm64-darwin) sha256=7240330b572e6355d7c75a7de535edb5dfcbd6295d9c7777df4d9dddfb8c0e5f
96
+ rake (13.3.1) sha256=8c9e89d09f66a26a01264e7e3480ec0607f0c497a861ef16063604b1b08eb19c
97
+ rake-compiler (1.3.0) sha256=eec272ef6d4dad27b36f5cdcf5b9ee4df2193751f4082b095f981ebf9cdf4127
98
+ rake-compiler-dock (1.11.0) sha256=eab51f2cd533eb35cea6b624a75281f047123e70a64c58b607471bb49428f8c2
99
+ rb_sys (0.9.126) sha256=ba958e0b8b4b89eeae0b3d24b64c809eb2c37e0ab0773a49e9b1c2e22c95aef8
100
+ ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
101
+ securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
102
+ timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb
103
+ tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b
104
+ uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
105
+
106
+ BUNDLED WITH
107
+ 2.6.9
@@ -1,7 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
- require "minitest/test_task"
4
+
5
+ def test_tasks_requested?
6
+ requested = Rake.application.top_level_tasks
7
+ return true if requested.empty?
8
+
9
+ requested.any? do |task_name|
10
+ task_name == "test" || task_name.start_with?("test:")
11
+ end
12
+ end
13
+
14
+ require "minitest/test_task" if test_tasks_requested?
5
15
 
6
16
  SMOKE_TEST_GLOBS = %w[
7
17
  test/test_itsi_scheduler.rb
@@ -11,6 +21,16 @@ SMOKE_TEST_GLOBS = %w[
11
21
  test/test_file_io.rb
12
22
  ].freeze
13
23
 
24
+ def native_build_tasks_requested?
25
+ requested = Rake.application.top_level_tasks
26
+ return true if requested.empty?
27
+
28
+ requested.any? do |task_name|
29
+ task_name == "default" ||
30
+ task_name.start_with?("build", "compile", "cross", "clobber")
31
+ end
32
+ end
33
+
14
34
  def configure_test_task(task_name, test_globs)
15
35
  Minitest::TestTask.create(task_name) do |t|
16
36
  t.libs << "test"
@@ -21,19 +41,23 @@ def configure_test_task(task_name, test_globs)
21
41
  end
22
42
  end
23
43
 
24
- configure_test_task(:test, ["test/**/*.rb"])
25
- configure_test_task("test:smoke", SMOKE_TEST_GLOBS)
44
+ if test_tasks_requested?
45
+ configure_test_task(:test, ["test/**/*.rb"])
46
+ configure_test_task("test:smoke", SMOKE_TEST_GLOBS)
26
47
 
27
- task "test:full" => :test
48
+ task "test:full" => :test
49
+ end
28
50
 
29
- require "rb_sys/extensiontask"
51
+ if native_build_tasks_requested?
52
+ require "rb_sys/extensiontask"
30
53
 
31
- task build: :compile
54
+ task build: :compile
32
55
 
33
- GEMSPEC = Gem::Specification.load("itsi-scheduler.gemspec")
56
+ GEMSPEC = Gem::Specification.load("itsi-scheduler.gemspec")
34
57
 
35
- RbSys::ExtensionTask.new("itsi-scheduler", GEMSPEC) do |ext|
36
- ext.lib_dir = "lib/itsi/scheduler"
58
+ RbSys::ExtensionTask.new("itsi-scheduler", GEMSPEC) do |ext|
59
+ ext.lib_dir = "lib/itsi/scheduler"
60
+ end
37
61
  end
38
62
 
39
63
  task default: %i[compile test rubocop]
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Scheduler
5
- VERSION = "0.2.26"
5
+ VERSION = "0.2.27.rc1"
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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ Warning[:experimental] = false if Warning.respond_to?(:[]=)
4
+
3
5
  require "minitest/reporters"
4
6
  Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
5
7
 
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "ipaddr"
4
+
3
5
  class TestAddressResolve < Minitest::Test
4
6
  include Itsi::Scheduler::TestHelper
5
7
 
@@ -15,7 +17,11 @@ class TestAddressResolve < Minitest::Test
15
17
  end
16
18
  end
17
19
 
18
- assert results.all?{|results| results.find(&:ipv4?) }
19
- assert results.all?{|results| results.find(&:ipv6?) }
20
+ assert_equal 2, results.length
21
+ assert results.all?(&:any?)
22
+ results.flatten.each do |addrinfo|
23
+ assert_instance_of Addrinfo, addrinfo
24
+ assert IPAddr.new(addrinfo.ip_address)
25
+ end
20
26
  end
21
27
  end
@@ -29,4 +29,84 @@ class TestItsiScheduler < Minitest::Test
29
29
  assert_equal total, 6
30
30
  assert_match /terminated on exception/, out
31
31
  end
32
+
33
+ def test_blocking_operation_wait_returns_result_without_stalling_scheduler
34
+ result = nil
35
+ marker = nil
36
+
37
+ with_scheduler do |scheduler|
38
+ Fiber.schedule do
39
+ result = scheduler.blocking_operation_wait(-> do
40
+ sleep 0.05
41
+ :done
42
+ end)
43
+ end
44
+
45
+ Fiber.schedule do
46
+ sleep 0.01
47
+ marker = :progressed
48
+ end
49
+ end
50
+
51
+ assert_equal :done, result
52
+ assert_equal :progressed, marker
53
+ end
54
+
55
+ def test_blocking_operation_wait_propagates_exceptions
56
+ error = nil
57
+
58
+ with_scheduler do |scheduler|
59
+ Fiber.schedule do
60
+ begin
61
+ scheduler.blocking_operation_wait(-> { raise ArgumentError, "boom" })
62
+ rescue => exception
63
+ error = exception
64
+ end
65
+ end
66
+ end
67
+
68
+ refute_nil error
69
+ assert_equal ArgumentError, error.class
70
+ assert_equal "boom", error.message
71
+ end
72
+
73
+ def test_io_select_returns_ready_descriptors_without_stalling_scheduler
74
+ ready = nil
75
+ marker = nil
76
+ reader, writer = IO.pipe
77
+
78
+ with_scheduler do |scheduler|
79
+ Fiber.schedule do
80
+ ready = scheduler.io_select([reader], nil, nil, 0.2)
81
+ end
82
+
83
+ Fiber.schedule do
84
+ sleep 0.01
85
+ marker = :progressed
86
+ writer.write("x")
87
+ end
88
+ end
89
+
90
+ assert_equal :progressed, marker
91
+ assert_equal [[reader], [], []], ready
92
+ ensure
93
+ reader&.close
94
+ writer&.close
95
+ end
96
+
97
+ def test_process_fork_reinitializes_worker_pool
98
+ worker_threads = nil
99
+ refreshed_threads = nil
100
+
101
+ with_scheduler do |scheduler|
102
+ worker_threads = scheduler.instance_variable_get(:@worker_threads)
103
+ scheduler.process_fork
104
+ refreshed_threads = scheduler.instance_variable_get(:@worker_threads)
105
+ end
106
+
107
+ refute_nil worker_threads
108
+ refute_nil refreshed_threads
109
+ refute_same worker_threads, refreshed_threads
110
+ assert_equal worker_threads.length, refreshed_threads.length
111
+ end
32
112
  end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "timeout"
4
+
5
+ class TestTimeoutAfter < Minitest::Test
6
+ include Itsi::Scheduler::TestHelper
7
+
8
+ def test_timeout_after_raises_timeout_error
9
+ result = nil
10
+
11
+ with_scheduler do
12
+ Fiber.schedule do
13
+ begin
14
+ Timeout.timeout(0.01, Timeout::Error, "benchmark timeout") do
15
+ sleep 0.05
16
+ end
17
+ rescue Timeout::Error => error
18
+ result = error.message
19
+ end
20
+ end
21
+ end
22
+
23
+ assert_equal "benchmark timeout", result
24
+ end
25
+
26
+ def test_timeout_after_cancels_fast_work_without_waiting_for_timeout
27
+ result = nil
28
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
29
+
30
+ with_scheduler do
31
+ Fiber.schedule do
32
+ Timeout.timeout(0.05, Timeout::Error, "benchmark timeout") do
33
+ sleep 0.001
34
+ result = :completed
35
+ end
36
+ end
37
+ end
38
+
39
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
40
+
41
+ assert_equal :completed, result
42
+ assert_operator elapsed, :<, 0.03
43
+ end
44
+
45
+ def test_timeout_after_interrupts_io_wait_without_leaking_waiters
46
+ result = nil
47
+ reader, writer = IO.pipe
48
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
49
+
50
+ with_scheduler do
51
+ Fiber.schedule do
52
+ begin
53
+ Timeout.timeout(0.01, Timeout::Error, "benchmark timeout") do
54
+ reader.readpartial(1)
55
+ end
56
+ rescue Timeout::Error
57
+ result = :timed_out
58
+ end
59
+ end
60
+ end
61
+
62
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
63
+
64
+ assert_equal :timed_out, result
65
+ assert_operator elapsed, :<, 0.05
66
+ ensure
67
+ reader&.close
68
+ writer&.close
69
+ end
70
+
71
+ def test_cascading_timeout_workload_completes
72
+ completed = 0
73
+ timed_out = 0
74
+ total = 0
75
+
76
+ with_scheduler do
77
+ 20.times do |index|
78
+ Fiber.schedule do
79
+ 50.times do |iteration|
80
+ slow = ((iteration * 7 + index * 3) % 10) < 3
81
+
82
+ begin
83
+ Timeout.timeout(0.01, Timeout::Error, "benchmark timeout") do
84
+ sleep(slow ? 0.05 : 0.002)
85
+ completed += 1
86
+ end
87
+ rescue Timeout::Error
88
+ timed_out += 1
89
+ ensure
90
+ total += 1
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ assert_equal 1000, total
98
+ assert_operator completed, :>, 0
99
+ assert_operator timed_out, :>, 0
100
+ assert_equal total, completed + timed_out
101
+ end
102
+ end
@@ -1660,9 +1660,27 @@ version = "1.0.15"
1660
1660
  source = "registry+https://github.com/rust-lang/crates.io-index"
1661
1661
  checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
1662
1662
 
1663
+ [[package]]
1664
+ name = "itsi-scheduler"
1665
+ version = "0.2.27"
1666
+ dependencies = [
1667
+ "bytes",
1668
+ "derive_more",
1669
+ "itsi_error",
1670
+ "itsi_instrument_entry",
1671
+ "itsi_rb_helpers",
1672
+ "itsi_tracing",
1673
+ "magnus",
1674
+ "mio",
1675
+ "nix",
1676
+ "parking_lot",
1677
+ "rb-sys",
1678
+ "tracing",
1679
+ ]
1680
+
1663
1681
  [[package]]
1664
1682
  name = "itsi-server"
1665
- version = "0.2.26"
1683
+ version = "0.2.27"
1666
1684
  dependencies = [
1667
1685
  "argon2",
1668
1686
  "async-channel",
@@ -1724,6 +1742,7 @@ dependencies = [
1724
1742
  "tokio-util",
1725
1743
  "tracing",
1726
1744
  "url",
1745
+ "webpki-roots",
1727
1746
  ]
1728
1747
 
1729
1748
  [[package]]
@@ -1739,6 +1758,7 @@ dependencies = [
1739
1758
  "futures",
1740
1759
  "log",
1741
1760
  "num-bigint",
1761
+ "parking_lot",
1742
1762
  "pem",
1743
1763
  "proc-macro2",
1744
1764
  "rcgen",
@@ -1772,6 +1792,15 @@ dependencies = [
1772
1792
  "thiserror 2.0.12",
1773
1793
  ]
1774
1794
 
1795
+ [[package]]
1796
+ name = "itsi_instrument_entry"
1797
+ version = "0.1.0"
1798
+ dependencies = [
1799
+ "proc-macro2",
1800
+ "quote",
1801
+ "syn 1.0.109",
1802
+ ]
1803
+
1775
1804
  [[package]]
1776
1805
  name = "itsi_rb_helpers"
1777
1806
  version = "0.1.0"
data/gems/server/Gemfile CHANGED
@@ -1,10 +1,12 @@
1
1
  source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
+ gemspec path: "../scheduler"
4
5
 
5
6
  group :development, :test do
6
7
  gem "bundler"
7
8
  gem "minitest", "~> 5.16"
9
+ gem "minitest-reporters"
8
10
  gem "rack"
9
11
  gem "rackup"
10
12
  gem "rake", "~> 13.0"