itsi 0.1.0 → 0.1.2
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 +524 -44
- data/Rakefile +22 -33
- data/crates/itsi_error/Cargo.toml +2 -0
- data/crates/itsi_error/src/from.rs +70 -0
- data/crates/itsi_error/src/lib.rs +10 -37
- data/crates/itsi_instrument_entry/Cargo.toml +15 -0
- data/crates/itsi_instrument_entry/src/lib.rs +31 -0
- data/crates/itsi_rb_helpers/Cargo.toml +2 -0
- data/crates/itsi_rb_helpers/src/heap_value.rs +121 -0
- data/crates/itsi_rb_helpers/src/lib.rs +90 -10
- data/crates/itsi_scheduler/Cargo.toml +9 -1
- data/crates/itsi_scheduler/extconf.rb +1 -1
- data/crates/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
- data/crates/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
- data/crates/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
- data/crates/itsi_scheduler/src/itsi_scheduler.rs +308 -0
- data/crates/itsi_scheduler/src/lib.rs +31 -10
- data/crates/itsi_server/Cargo.toml +14 -2
- data/crates/itsi_server/extconf.rb +1 -1
- data/crates/itsi_server/src/body_proxy/big_bytes.rs +104 -0
- data/crates/itsi_server/src/body_proxy/itsi_body_proxy.rs +122 -0
- data/crates/itsi_server/src/body_proxy/mod.rs +2 -0
- data/crates/itsi_server/src/lib.rs +58 -7
- data/crates/itsi_server/src/request/itsi_request.rs +238 -104
- data/crates/itsi_server/src/response/itsi_response.rs +347 -0
- data/crates/itsi_server/src/response/mod.rs +1 -0
- data/crates/itsi_server/src/server/bind.rs +50 -20
- data/crates/itsi_server/src/server/bind_protocol.rs +37 -0
- data/crates/itsi_server/src/server/io_stream.rs +104 -0
- data/crates/itsi_server/src/server/itsi_ca/itsi_ca.crt +11 -30
- data/crates/itsi_server/src/server/itsi_ca/itsi_ca.key +3 -50
- data/crates/itsi_server/src/server/itsi_server.rs +181 -133
- data/crates/itsi_server/src/server/lifecycle_event.rs +8 -0
- data/crates/itsi_server/src/server/listener.rs +169 -128
- data/crates/itsi_server/src/server/mod.rs +7 -1
- data/crates/itsi_server/src/server/process_worker.rs +196 -0
- data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +253 -0
- data/crates/itsi_server/src/server/serve_strategy/mod.rs +27 -0
- data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +238 -0
- data/crates/itsi_server/src/server/signal.rs +57 -0
- data/crates/itsi_server/src/server/thread_worker.rs +368 -0
- data/crates/itsi_server/src/server/tls.rs +42 -28
- data/crates/itsi_tracing/Cargo.toml +4 -0
- data/crates/itsi_tracing/src/lib.rs +36 -6
- data/gems/scheduler/Cargo.lock +219 -23
- data/gems/scheduler/Rakefile +7 -1
- data/gems/scheduler/ext/itsi_error/Cargo.toml +2 -0
- data/gems/scheduler/ext/itsi_error/src/from.rs +70 -0
- data/gems/scheduler/ext/itsi_error/src/lib.rs +10 -37
- data/gems/scheduler/ext/itsi_instrument_entry/Cargo.toml +15 -0
- data/gems/scheduler/ext/itsi_instrument_entry/src/lib.rs +31 -0
- data/gems/scheduler/ext/itsi_rb_helpers/Cargo.toml +2 -0
- data/gems/scheduler/ext/itsi_rb_helpers/src/heap_value.rs +121 -0
- data/gems/scheduler/ext/itsi_rb_helpers/src/lib.rs +90 -10
- data/gems/scheduler/ext/itsi_scheduler/Cargo.toml +9 -1
- data/gems/scheduler/ext/itsi_scheduler/extconf.rb +1 -1
- data/gems/scheduler/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
- data/gems/scheduler/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
- data/gems/scheduler/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
- data/gems/scheduler/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
- data/gems/scheduler/ext/itsi_scheduler/src/lib.rs +31 -10
- data/gems/scheduler/ext/itsi_server/Cargo.toml +41 -0
- data/gems/scheduler/ext/itsi_server/extconf.rb +6 -0
- data/gems/scheduler/ext/itsi_server/src/body_proxy/big_bytes.rs +104 -0
- data/gems/scheduler/ext/itsi_server/src/body_proxy/itsi_body_proxy.rs +122 -0
- data/gems/scheduler/ext/itsi_server/src/body_proxy/mod.rs +2 -0
- data/gems/scheduler/ext/itsi_server/src/lib.rs +103 -0
- data/gems/scheduler/ext/itsi_server/src/request/itsi_request.rs +277 -0
- data/gems/scheduler/ext/itsi_server/src/request/mod.rs +1 -0
- data/gems/scheduler/ext/itsi_server/src/response/itsi_response.rs +347 -0
- data/gems/scheduler/ext/itsi_server/src/response/mod.rs +1 -0
- data/gems/scheduler/ext/itsi_server/src/server/bind.rs +168 -0
- data/gems/scheduler/ext/itsi_server/src/server/bind_protocol.rs +37 -0
- data/gems/scheduler/ext/itsi_server/src/server/io_stream.rs +104 -0
- data/gems/scheduler/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +13 -0
- data/gems/scheduler/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +5 -0
- data/gems/scheduler/ext/itsi_server/src/server/itsi_server.rs +230 -0
- data/gems/scheduler/ext/itsi_server/src/server/lifecycle_event.rs +8 -0
- data/gems/scheduler/ext/itsi_server/src/server/listener.rs +259 -0
- data/gems/scheduler/ext/itsi_server/src/server/mod.rs +11 -0
- data/gems/scheduler/ext/itsi_server/src/server/process_worker.rs +196 -0
- data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +253 -0
- data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/mod.rs +27 -0
- data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/single_mode.rs +238 -0
- data/gems/scheduler/ext/itsi_server/src/server/signal.rs +57 -0
- data/gems/scheduler/ext/itsi_server/src/server/thread_worker.rs +368 -0
- data/gems/scheduler/ext/itsi_server/src/server/tls.rs +152 -0
- data/gems/scheduler/ext/itsi_tracing/Cargo.toml +4 -0
- data/gems/scheduler/ext/itsi_tracing/src/lib.rs +36 -6
- data/gems/scheduler/itsi-scheduler.gemspec +2 -2
- data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
- data/gems/scheduler/lib/itsi/scheduler.rb +137 -1
- data/gems/scheduler/test/helpers/test_helper.rb +24 -0
- data/gems/scheduler/test/test_active_record.rb +158 -0
- data/gems/scheduler/test/test_address_resolve.rb +23 -0
- data/gems/scheduler/test/test_block_unblock.rb +229 -0
- data/gems/scheduler/test/test_file_io.rb +193 -0
- data/gems/scheduler/test/test_itsi_scheduler.rb +24 -1
- data/gems/scheduler/test/test_kernel_sleep.rb +91 -0
- data/gems/scheduler/test/test_nested_fibers.rb +286 -0
- data/gems/scheduler/test/test_network_io.rb +274 -0
- data/gems/scheduler/test/test_process_wait.rb +26 -0
- data/gems/server/exe/itsi +88 -28
- data/gems/server/ext/itsi_error/Cargo.toml +2 -0
- data/gems/server/ext/itsi_error/src/from.rs +70 -0
- data/gems/server/ext/itsi_error/src/lib.rs +10 -37
- data/gems/server/ext/itsi_instrument_entry/Cargo.toml +15 -0
- data/gems/server/ext/itsi_instrument_entry/src/lib.rs +31 -0
- data/gems/server/ext/itsi_rb_helpers/Cargo.toml +2 -0
- data/gems/server/ext/itsi_rb_helpers/src/heap_value.rs +121 -0
- data/gems/server/ext/itsi_rb_helpers/src/lib.rs +90 -10
- data/gems/server/ext/itsi_scheduler/Cargo.toml +24 -0
- data/gems/server/ext/itsi_scheduler/extconf.rb +6 -0
- data/gems/server/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
- data/gems/server/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
- data/gems/server/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
- data/gems/server/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
- data/gems/server/ext/itsi_scheduler/src/lib.rs +38 -0
- data/gems/server/ext/itsi_server/Cargo.toml +14 -2
- data/gems/server/ext/itsi_server/extconf.rb +1 -1
- data/gems/server/ext/itsi_server/src/body_proxy/big_bytes.rs +104 -0
- data/gems/server/ext/itsi_server/src/body_proxy/itsi_body_proxy.rs +122 -0
- data/gems/server/ext/itsi_server/src/body_proxy/mod.rs +2 -0
- data/gems/server/ext/itsi_server/src/lib.rs +58 -7
- data/gems/server/ext/itsi_server/src/request/itsi_request.rs +238 -104
- data/gems/server/ext/itsi_server/src/response/itsi_response.rs +347 -0
- data/gems/server/ext/itsi_server/src/response/mod.rs +1 -0
- data/gems/server/ext/itsi_server/src/server/bind.rs +50 -20
- data/gems/server/ext/itsi_server/src/server/bind_protocol.rs +37 -0
- data/gems/server/ext/itsi_server/src/server/io_stream.rs +104 -0
- data/gems/server/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +11 -30
- data/gems/server/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +3 -50
- data/gems/server/ext/itsi_server/src/server/itsi_server.rs +181 -133
- data/gems/server/ext/itsi_server/src/server/lifecycle_event.rs +8 -0
- data/gems/server/ext/itsi_server/src/server/listener.rs +169 -128
- data/gems/server/ext/itsi_server/src/server/mod.rs +7 -1
- data/gems/server/ext/itsi_server/src/server/process_worker.rs +196 -0
- data/gems/server/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +253 -0
- data/gems/server/ext/itsi_server/src/server/serve_strategy/mod.rs +27 -0
- data/gems/server/ext/itsi_server/src/server/serve_strategy/single_mode.rs +238 -0
- data/gems/server/ext/itsi_server/src/server/signal.rs +57 -0
- data/gems/server/ext/itsi_server/src/server/thread_worker.rs +368 -0
- data/gems/server/ext/itsi_server/src/server/tls.rs +42 -28
- data/gems/server/ext/itsi_tracing/Cargo.toml +4 -0
- data/gems/server/ext/itsi_tracing/src/lib.rs +36 -6
- data/gems/server/itsi-server.gemspec +4 -4
- data/gems/server/lib/itsi/request.rb +30 -14
- data/gems/server/lib/itsi/server/rack/handler/itsi.rb +25 -0
- data/gems/server/lib/itsi/server/scheduler_mode.rb +6 -0
- data/gems/server/lib/itsi/server/version.rb +1 -1
- data/gems/server/lib/itsi/server.rb +68 -2
- data/gems/server/lib/itsi/signals.rb +18 -0
- data/gems/server/lib/itsi/stream_io.rb +38 -0
- data/gems/server/test/test_helper.rb +2 -0
- data/gems/server/test/test_itsi_server.rb +1 -1
- data/lib/itsi/version.rb +1 -1
- data/tasks.txt +17 -0
- metadata +102 -12
- data/crates/itsi_server/src/server/transfer_protocol.rs +0 -23
- data/crates/itsi_server/src/stream_writer/mod.rs +0 -21
- data/gems/scheduler/test/test_helper.rb +0 -6
- data/gems/server/ext/itsi_server/src/server/transfer_protocol.rs +0 -23
- data/gems/server/ext/itsi_server/src/stream_writer/mod.rs +0 -21
@@ -6,6 +6,142 @@ require_relative "scheduler/itsi_scheduler"
|
|
6
6
|
module Itsi
|
7
7
|
class Scheduler
|
8
8
|
class Error < StandardError; end
|
9
|
-
|
9
|
+
|
10
|
+
def self.resume_token
|
11
|
+
@resume_token ||= 0
|
12
|
+
@resume_token += 1
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@join_waiters = {}.compare_by_identity
|
17
|
+
@token_map = {}.compare_by_identity
|
18
|
+
@resume_tokens = {}.compare_by_identity
|
19
|
+
@unblocked = [[], []]
|
20
|
+
@unblock_idx = 0
|
21
|
+
@unblocked_mux = Mutex.new
|
22
|
+
@resume_fiber = method(:resume_fiber).to_proc
|
23
|
+
@resume_fiber_with_readiness = method(:resume_fiber_with_readiness).to_proc
|
24
|
+
@resume_blocked = method(:resume_blocked).to_proc
|
25
|
+
end
|
26
|
+
|
27
|
+
def block(_, timeout, fiber = Fiber.current, token = Scheduler.resume_token)
|
28
|
+
@join_waiters[fiber] = true
|
29
|
+
|
30
|
+
start_timer(timeout, token) if timeout
|
31
|
+
@resume_tokens[token] = fiber
|
32
|
+
@token_map[fiber] = token
|
33
|
+
Fiber.yield
|
34
|
+
ensure
|
35
|
+
@resume_tokens.delete(token)
|
36
|
+
@token_map.delete(fiber)
|
37
|
+
@join_waiters.delete(fiber)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Register an IO waiter.
|
41
|
+
# This will get resumed by our scheduler inside the call to
|
42
|
+
# fetch_events.
|
43
|
+
def io_wait(io, events, duration)
|
44
|
+
fiber = Fiber.current
|
45
|
+
token = Scheduler.resume_token
|
46
|
+
readiness = register_io_wait(io.fileno, events, duration, token)
|
47
|
+
readiness || block(nil, duration, fiber, token)
|
48
|
+
end
|
49
|
+
|
50
|
+
def unblock(_blocker, fiber)
|
51
|
+
@unblocked_mux.synchronize do
|
52
|
+
@unblocked[@unblock_idx] << fiber
|
53
|
+
end
|
54
|
+
wake
|
55
|
+
end
|
56
|
+
|
57
|
+
def kernel_sleep(duration)
|
58
|
+
block nil, duration
|
59
|
+
end
|
60
|
+
|
61
|
+
def tick
|
62
|
+
events = fetch_due_events
|
63
|
+
timers = fetch_due_timers
|
64
|
+
unblocked = switch_unblock_batch
|
65
|
+
events&.each(&@resume_fiber_with_readiness)
|
66
|
+
unblocked.each(&@resume_blocked)
|
67
|
+
unblocked.clear
|
68
|
+
timers&.each(&@resume_fiber)
|
69
|
+
end
|
70
|
+
|
71
|
+
def resume_fiber(token)
|
72
|
+
if (fiber = @resume_tokens.delete(token))
|
73
|
+
fiber.resume
|
74
|
+
end
|
75
|
+
rescue StandardError => e
|
76
|
+
warn "Failed to resume fiber #{fiber}: #{e.message}"
|
77
|
+
end
|
78
|
+
|
79
|
+
def resume_fiber_with_readiness((token, readiness))
|
80
|
+
if (fiber = @resume_tokens.delete(token))
|
81
|
+
fiber.resume(readiness)
|
82
|
+
end
|
83
|
+
rescue StandardError => e
|
84
|
+
warn "Failed to resume fiber #{fiber}: #{e.message}"
|
85
|
+
end
|
86
|
+
|
87
|
+
def resume_blocked(fiber)
|
88
|
+
if (token = @token_map[fiber])
|
89
|
+
resume_fiber(token)
|
90
|
+
elsif fiber.alive?
|
91
|
+
fiber.resume
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def switch_unblock_batch
|
96
|
+
@unblocked_mux.synchronize do
|
97
|
+
current = @unblocked[@unblock_idx]
|
98
|
+
@unblock_idx = (@unblock_idx + 1) % 2
|
99
|
+
current
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Yields upwards to the scheduler, with an intention to
|
104
|
+
# resume the fiber that yielded ASAP.
|
105
|
+
def yield
|
106
|
+
kernel_sleep(0) if work?
|
107
|
+
end
|
108
|
+
|
109
|
+
# Keep running until we've got no timers we're awaiting, no pending IO, no temporary yields,
|
110
|
+
# no pending unblocks.
|
111
|
+
def work?
|
112
|
+
!@unblocked[@unblock_idx].empty? || !@join_waiters.empty? || has_pending_io?
|
113
|
+
end
|
114
|
+
|
115
|
+
# Run until no more work needs doing.
|
116
|
+
def run
|
117
|
+
tick while work?
|
118
|
+
debug "Exit Scheduler"
|
119
|
+
end
|
120
|
+
|
121
|
+
# Hook invoked at the end of the thread.
|
122
|
+
# Will start our scheduler's Reactor.
|
123
|
+
def scheduler_close
|
124
|
+
run
|
125
|
+
ensure
|
126
|
+
@closed ||= true
|
127
|
+
freeze
|
128
|
+
end
|
129
|
+
|
130
|
+
# Need to defer to Process::Status rather than our extension
|
131
|
+
# as we don't have a means of creating our own Process::Status.
|
132
|
+
def process_wait(pid, flags)
|
133
|
+
Thread.new do
|
134
|
+
Process::Status.wait(pid, flags)
|
135
|
+
end.value
|
136
|
+
end
|
137
|
+
|
138
|
+
def closed?
|
139
|
+
@closed
|
140
|
+
end
|
141
|
+
|
142
|
+
# Spin up a new fiber and immediately resume it.
|
143
|
+
def fiber(&blk)
|
144
|
+
Fiber.new(blocking: false, &blk).tap(&:resume)
|
145
|
+
end
|
10
146
|
end
|
11
147
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "minitest/reporters"
|
4
|
+
Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
|
5
|
+
|
6
|
+
require "itsi/scheduler"
|
7
|
+
require 'debug'
|
8
|
+
module Itsi::Scheduler::TestHelper
|
9
|
+
SchedulerClass = Itsi::Scheduler
|
10
|
+
|
11
|
+
def with_scheduler(join: true, report_on_exception: false)
|
12
|
+
Thread.new do
|
13
|
+
Thread.current.report_on_exception = report_on_exception
|
14
|
+
scheduler = SchedulerClass.new
|
15
|
+
Fiber.set_scheduler(scheduler)
|
16
|
+
Fiber.schedule do
|
17
|
+
yield scheduler
|
18
|
+
end
|
19
|
+
end.yield_self do |thread|
|
20
|
+
thread.join if join
|
21
|
+
thread
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_record"
|
4
|
+
|
5
|
+
class TestActiveRecordFiberScheduler < Minitest::Test
|
6
|
+
include Itsi::Scheduler::TestHelper
|
7
|
+
|
8
|
+
# Set up an ActiveRecord connection to your PostgreSQL test database.
|
9
|
+
# Adjust the connection parameters as needed.
|
10
|
+
def setup
|
11
|
+
ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
|
12
|
+
ActiveRecord::Base.establish_connection(
|
13
|
+
adapter: "postgresql",
|
14
|
+
database: "fiber_scheduler_test",
|
15
|
+
pool: 2, # use a small pool to test contention scenarios
|
16
|
+
checkout_timeout: 5
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Disconnect after each test.
|
21
|
+
def teardown
|
22
|
+
ActiveRecord::Base.connection_pool.disconnect!
|
23
|
+
end
|
24
|
+
|
25
|
+
# Test a basic query execution inside a fiber.
|
26
|
+
def test_basic_query
|
27
|
+
result = nil
|
28
|
+
|
29
|
+
with_scheduler do |_scheduler|
|
30
|
+
Fiber.schedule do
|
31
|
+
result = ActiveRecord::Base.connection.select_value("SELECT 1")
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# select_value returns a string from PG adapter so we compare with "1"
|
36
|
+
assert_equal "1", result.to_s
|
37
|
+
end
|
38
|
+
|
39
|
+
# Test running two queries concurrently in different fibers.
|
40
|
+
def test_concurrent_queries
|
41
|
+
results = []
|
42
|
+
|
43
|
+
with_scheduler do |_scheduler|
|
44
|
+
Fiber.schedule do
|
45
|
+
results << ActiveRecord::Base.connection.select_value("SELECT 1")
|
46
|
+
end
|
47
|
+
|
48
|
+
Fiber.schedule do
|
49
|
+
results << ActiveRecord::Base.connection.select_value("SELECT 2")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Ensure that both queries have executed and returned the expected values.
|
54
|
+
results = results.map(&:to_s)
|
55
|
+
assert_includes results, "1"
|
56
|
+
assert_includes results, "2"
|
57
|
+
end
|
58
|
+
|
59
|
+
# Test a query that involves a short delay using PostgreSQL's pg_sleep.
|
60
|
+
def test_query_with_delay
|
61
|
+
result = nil
|
62
|
+
|
63
|
+
with_scheduler do |_scheduler|
|
64
|
+
Fiber.schedule do
|
65
|
+
# Introduce a 0.1 second delay.
|
66
|
+
ActiveRecord::Base.connection.execute("SELECT pg_sleep(0.1)")
|
67
|
+
result = ActiveRecord::Base.connection.select_value("SELECT 3")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
assert_equal "3", result.to_s
|
72
|
+
end
|
73
|
+
|
74
|
+
# Test connection pool exhaustion by limiting the pool to one connection.
|
75
|
+
# Two fibers will attempt to get a connection concurrently.
|
76
|
+
def test_connection_pool_exhaustion
|
77
|
+
# Re-establish connection with a pool size of 1.
|
78
|
+
ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
|
79
|
+
ActiveRecord::Base.establish_connection(
|
80
|
+
adapter: "postgresql",
|
81
|
+
host: "localhost",
|
82
|
+
database: "fiber_scheduler_test",
|
83
|
+
pool: 1,
|
84
|
+
checkout_timeout: 0.25
|
85
|
+
)
|
86
|
+
# ActiveRecord::Base.connection_pool.disconnect!
|
87
|
+
|
88
|
+
results = []
|
89
|
+
|
90
|
+
with_scheduler do |_scheduler|
|
91
|
+
Fiber.schedule do
|
92
|
+
ActiveRecord::Base.connection_pool.with_connection(prevent_permanent_checkout: true) do
|
93
|
+
results << ActiveRecord::Base.connection.select_value("SELECT 1")
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
Fiber.schedule do
|
98
|
+
ActiveRecord::Base.connection_pool.with_connection(prevent_permanent_checkout: true) do
|
99
|
+
results << ActiveRecord::Base.connection.select_value("SELECT 2")
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Takes #{checkout_timeout} seconds between last
|
105
|
+
results = results.map(&:to_s)
|
106
|
+
assert_includes results, "1"
|
107
|
+
assert_includes results, "2"
|
108
|
+
end
|
109
|
+
|
110
|
+
# Test that after a Fiber finishes its work, its connection is automatically released.
|
111
|
+
def test_fiber_connection_release_after_completion
|
112
|
+
# Use the scheduler to run a fiber that checks out a connection and does a simple query.
|
113
|
+
|
114
|
+
with_scheduler do |_scheduler|
|
115
|
+
Fiber.schedule do
|
116
|
+
# This fiber checks out a connection to run a query.
|
117
|
+
ActiveRecord::Base.connection.select_value("SELECT 1")
|
118
|
+
# The fiber ends here.
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# After the scheduler finishes, the fiber should have completed and released its connection.
|
123
|
+
# Now we attempt to checkout a connection manually. If the previous fiber's connection
|
124
|
+
# was not released, this would either time out or raise an error.
|
125
|
+
connection = ActiveRecord::Base.connection_pool.checkout
|
126
|
+
assert connection, "Expected to obtain a connection after fiber completion"
|
127
|
+
ActiveRecord::Base.connection_pool.checkin(connection)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Test that a transaction works correctly when run inside a fiber.
|
131
|
+
# A temporary table is created, a record inserted and then queried before the transaction is rolled back.
|
132
|
+
def test_transaction_fiber
|
133
|
+
result = nil
|
134
|
+
|
135
|
+
with_scheduler do |_scheduler|
|
136
|
+
Fiber.schedule do
|
137
|
+
ActiveRecord::Base.transaction do
|
138
|
+
# Create a temporary table for testing.
|
139
|
+
ActiveRecord::Base.connection.execute(<<~SQL)
|
140
|
+
CREATE TEMP TABLE IF NOT EXISTS test_table (
|
141
|
+
id serial PRIMARY KEY,
|
142
|
+
name text
|
143
|
+
)
|
144
|
+
SQL
|
145
|
+
|
146
|
+
# Insert a record.
|
147
|
+
ActiveRecord::Base.connection.execute("INSERT INTO test_table (name) VALUES ('Alice')")
|
148
|
+
# Query the inserted record.
|
149
|
+
result = ActiveRecord::Base.connection.select_value("SELECT name FROM test_table LIMIT 1")
|
150
|
+
# Roll back the transaction to avoid leaving test data.
|
151
|
+
raise ActiveRecord::Rollback
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
assert_equal "Alice", result.to_s
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'debug'
|
3
|
+
|
4
|
+
|
5
|
+
class TestAddressResolve < Minitest::Test
|
6
|
+
include Itsi::Scheduler::TestHelper
|
7
|
+
|
8
|
+
def test_addess_resolve
|
9
|
+
results = []
|
10
|
+
|
11
|
+
with_scheduler do |_scheduler|
|
12
|
+
Fiber.schedule do
|
13
|
+
results << Addrinfo.getaddrinfo("www.ruby-lang.org", 80, nil, :STREAM)
|
14
|
+
end
|
15
|
+
Fiber.schedule do
|
16
|
+
results << Addrinfo.getaddrinfo("www.google.com", 80, nil, :STREAM)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
assert results.all?{|results| results.find(&:ipv4?) }
|
21
|
+
assert results.all?{|results| results.find(&:ipv6?) }
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,229 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
|
4
|
+
class TestBlockUnblock < Minitest::Test
|
5
|
+
include Itsi::Scheduler::TestHelper
|
6
|
+
|
7
|
+
def test_block_with_timeout
|
8
|
+
result = nil
|
9
|
+
start_time = Time.now
|
10
|
+
|
11
|
+
with_scheduler do |_scheduler|
|
12
|
+
Fiber.schedule do
|
13
|
+
# Simulate a blocking operation with a 0.1-second timeout.
|
14
|
+
Fiber.scheduler.block(nil, 0.1)
|
15
|
+
result = :resumed
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
elapsed_time = Time.now - start_time
|
20
|
+
|
21
|
+
assert_equal :resumed, result
|
22
|
+
assert_in_delta 0.1, elapsed_time, 0.01, "Fiber did not resume after the expected timeout"
|
23
|
+
end
|
24
|
+
|
25
|
+
# Test that a fiber blocked without a timeout can be manually unblocked.
|
26
|
+
def test_block_and_manual_unblock
|
27
|
+
result = nil
|
28
|
+
|
29
|
+
with_scheduler do |scheduler|
|
30
|
+
fiber = Fiber.schedule do
|
31
|
+
# This fiber blocks indefinitely until manually unblocked.
|
32
|
+
Fiber.scheduler.block(:self, nil)
|
33
|
+
result = :resumed
|
34
|
+
end
|
35
|
+
|
36
|
+
Fiber.schedule do
|
37
|
+
sleep 0.05
|
38
|
+
scheduler.unblock(:self, fiber)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
assert_equal :resumed, result
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_double_unblock
|
46
|
+
result = nil
|
47
|
+
with_scheduler do |sched|
|
48
|
+
fiber = Fiber.schedule do
|
49
|
+
# This fiber blocks indefinitely until manually unblocked.
|
50
|
+
Fiber.scheduler.block(:self, nil)
|
51
|
+
result = :resumed
|
52
|
+
end
|
53
|
+
|
54
|
+
fiber_2 = Fiber.schedule do
|
55
|
+
Fiber.scheduler.unblock(:self, fiber)
|
56
|
+
Fiber.scheduler.unblock(:self, fiber)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
assert_equal :resumed, result
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_timed_double_unblock
|
64
|
+
result = nil
|
65
|
+
|
66
|
+
fiber = scheduler = nil
|
67
|
+
|
68
|
+
Thread.new do
|
69
|
+
sleep 0.01
|
70
|
+
scheduler.unblock(:self, fiber)
|
71
|
+
scheduler.unblock(:self, fiber)
|
72
|
+
sleep 1
|
73
|
+
end
|
74
|
+
|
75
|
+
with_scheduler do |sched|
|
76
|
+
scheduler = sched
|
77
|
+
fiber = Fiber.schedule do
|
78
|
+
# This fiber blocks indefinitely until manually unblocked.
|
79
|
+
Fiber.scheduler.block(:self, 0.1)
|
80
|
+
Fiber.scheduler.block(:self, 0.1)
|
81
|
+
result = :resumed
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
assert_equal :resumed, result
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_condition_variable_signaling
|
89
|
+
result = nil
|
90
|
+
mutex = Mutex.new
|
91
|
+
cv = ConditionVariable.new
|
92
|
+
|
93
|
+
# Set a scheduler so that non-blocking fibers are active.
|
94
|
+
# (Without a scheduler, blocking operations run in the usual way.)
|
95
|
+
with_scheduler do
|
96
|
+
# Create a non-blocking fiber that waits on the condition variable.
|
97
|
+
fiber = Fiber.schedule do
|
98
|
+
mutex.synchronize do
|
99
|
+
# The call to cv.wait(mutex) internally triggers the scheduler’s block hook.
|
100
|
+
cv.wait(mutex)
|
101
|
+
result = :resumed
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# In a separate thread, signal the condition variable after a delay.
|
106
|
+
Thread.new do
|
107
|
+
sleep 0.05
|
108
|
+
mutex.synchronize do
|
109
|
+
cv.signal # This should cause Ruby to internally call the scheduler’s unblock hook.
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Wait until the fiber finishes.
|
114
|
+
Timeout.timeout(1) do
|
115
|
+
sleep 0.01 while fiber.alive?
|
116
|
+
end
|
117
|
+
end
|
118
|
+
assert_equal :resumed, result
|
119
|
+
end
|
120
|
+
|
121
|
+
def test_queue_pop_unblocks_fiber
|
122
|
+
result = nil
|
123
|
+
queue = Queue.new
|
124
|
+
|
125
|
+
# Set a scheduler so that non-blocking fiber operations are enabled.
|
126
|
+
with_scheduler do
|
127
|
+
# Schedule a fiber that waits on the queue.
|
128
|
+
fiber = Fiber.schedule do
|
129
|
+
# queue.pop will block until an item is pushed.
|
130
|
+
result = queue.pop
|
131
|
+
end
|
132
|
+
|
133
|
+
# In a separate thread, push an element into the queue after a short delay.
|
134
|
+
Thread.new do
|
135
|
+
queue.push(:hello)
|
136
|
+
end.join
|
137
|
+
|
138
|
+
# Wait until the fiber finishes execution.
|
139
|
+
sleep 0.1 while fiber.alive?
|
140
|
+
end
|
141
|
+
assert_equal :hello, result
|
142
|
+
end
|
143
|
+
|
144
|
+
# Test that unblocking a fiber that isn’t blocked is a no-op.
|
145
|
+
def test_unblock_non_blocked_fiber
|
146
|
+
with_scheduler do |scheduler|
|
147
|
+
fiber = Fiber.new do
|
148
|
+
# Do nothing special.
|
149
|
+
:finished
|
150
|
+
end
|
151
|
+
|
152
|
+
# Try to unblock a fiber that isn’t currently blocked.
|
153
|
+
scheduler.unblock(nil, fiber)
|
154
|
+
# The fiber should finish normally.
|
155
|
+
assert_equal :finished, fiber.resume
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def test_multiple_fibers_blocking_and_unblocking
|
160
|
+
results = {}
|
161
|
+
|
162
|
+
with_scheduler do |scheduler|
|
163
|
+
Fiber.schedule do
|
164
|
+
Fiber.scheduler.block(:resource1, 0.1)
|
165
|
+
results[:fiber1] = :resumed
|
166
|
+
end
|
167
|
+
|
168
|
+
fiber2 = Fiber.schedule do
|
169
|
+
Fiber.scheduler.block(:resource2, nil)
|
170
|
+
results[:fiber2] = :resumed
|
171
|
+
end
|
172
|
+
|
173
|
+
Fiber.schedule do
|
174
|
+
sleep 0.05
|
175
|
+
scheduler.unblock(:resource, fiber2)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
assert_equal :resumed, results[:fiber1], "Fiber1 did not resume after timeout"
|
180
|
+
assert_equal :resumed, results[:fiber2], "Fiber2 did not resume after manual unblock"
|
181
|
+
end
|
182
|
+
|
183
|
+
def test_block_with_immediate_unblock
|
184
|
+
result = nil
|
185
|
+
|
186
|
+
with_scheduler do |scheduler|
|
187
|
+
fiber = Fiber.schedule do
|
188
|
+
Fiber.scheduler.block(:resource, 0.1)
|
189
|
+
result = :resumed
|
190
|
+
end
|
191
|
+
|
192
|
+
Fiber.schedule do
|
193
|
+
scheduler.unblock(:resource, fiber)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
assert_equal :resumed, result
|
198
|
+
end
|
199
|
+
|
200
|
+
def test_block_with_no_unblock
|
201
|
+
result = nil
|
202
|
+
|
203
|
+
with_scheduler(join: false) do |_scheduler|
|
204
|
+
Fiber.schedule do
|
205
|
+
Fiber.scheduler.block(:resource, 0.1)
|
206
|
+
result = :resumed
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
assert_nil result, :resumed
|
211
|
+
end
|
212
|
+
|
213
|
+
def test_unblock_non_blocked_fiber_v2
|
214
|
+
result = :not_resumed
|
215
|
+
|
216
|
+
with_scheduler do |scheduler|
|
217
|
+
fiber = Fiber.schedule do
|
218
|
+
result = :resumed
|
219
|
+
end
|
220
|
+
|
221
|
+
Fiber.schedule do
|
222
|
+
sleep 0.05
|
223
|
+
scheduler.unblock(:resource, fiber)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
assert_equal :resumed, result, "Fiber should have been resumed normally"
|
228
|
+
end
|
229
|
+
end
|