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.
Files changed (164) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +524 -44
  3. data/Rakefile +22 -33
  4. data/crates/itsi_error/Cargo.toml +2 -0
  5. data/crates/itsi_error/src/from.rs +70 -0
  6. data/crates/itsi_error/src/lib.rs +10 -37
  7. data/crates/itsi_instrument_entry/Cargo.toml +15 -0
  8. data/crates/itsi_instrument_entry/src/lib.rs +31 -0
  9. data/crates/itsi_rb_helpers/Cargo.toml +2 -0
  10. data/crates/itsi_rb_helpers/src/heap_value.rs +121 -0
  11. data/crates/itsi_rb_helpers/src/lib.rs +90 -10
  12. data/crates/itsi_scheduler/Cargo.toml +9 -1
  13. data/crates/itsi_scheduler/extconf.rb +1 -1
  14. data/crates/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
  15. data/crates/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
  16. data/crates/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
  17. data/crates/itsi_scheduler/src/itsi_scheduler.rs +308 -0
  18. data/crates/itsi_scheduler/src/lib.rs +31 -10
  19. data/crates/itsi_server/Cargo.toml +14 -2
  20. data/crates/itsi_server/extconf.rb +1 -1
  21. data/crates/itsi_server/src/body_proxy/big_bytes.rs +104 -0
  22. data/crates/itsi_server/src/body_proxy/itsi_body_proxy.rs +122 -0
  23. data/crates/itsi_server/src/body_proxy/mod.rs +2 -0
  24. data/crates/itsi_server/src/lib.rs +58 -7
  25. data/crates/itsi_server/src/request/itsi_request.rs +238 -104
  26. data/crates/itsi_server/src/response/itsi_response.rs +347 -0
  27. data/crates/itsi_server/src/response/mod.rs +1 -0
  28. data/crates/itsi_server/src/server/bind.rs +50 -20
  29. data/crates/itsi_server/src/server/bind_protocol.rs +37 -0
  30. data/crates/itsi_server/src/server/io_stream.rs +104 -0
  31. data/crates/itsi_server/src/server/itsi_ca/itsi_ca.crt +11 -30
  32. data/crates/itsi_server/src/server/itsi_ca/itsi_ca.key +3 -50
  33. data/crates/itsi_server/src/server/itsi_server.rs +181 -133
  34. data/crates/itsi_server/src/server/lifecycle_event.rs +8 -0
  35. data/crates/itsi_server/src/server/listener.rs +169 -128
  36. data/crates/itsi_server/src/server/mod.rs +7 -1
  37. data/crates/itsi_server/src/server/process_worker.rs +196 -0
  38. data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +253 -0
  39. data/crates/itsi_server/src/server/serve_strategy/mod.rs +27 -0
  40. data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +238 -0
  41. data/crates/itsi_server/src/server/signal.rs +57 -0
  42. data/crates/itsi_server/src/server/thread_worker.rs +368 -0
  43. data/crates/itsi_server/src/server/tls.rs +42 -28
  44. data/crates/itsi_tracing/Cargo.toml +4 -0
  45. data/crates/itsi_tracing/src/lib.rs +36 -6
  46. data/gems/scheduler/Cargo.lock +219 -23
  47. data/gems/scheduler/Rakefile +7 -1
  48. data/gems/scheduler/ext/itsi_error/Cargo.toml +2 -0
  49. data/gems/scheduler/ext/itsi_error/src/from.rs +70 -0
  50. data/gems/scheduler/ext/itsi_error/src/lib.rs +10 -37
  51. data/gems/scheduler/ext/itsi_instrument_entry/Cargo.toml +15 -0
  52. data/gems/scheduler/ext/itsi_instrument_entry/src/lib.rs +31 -0
  53. data/gems/scheduler/ext/itsi_rb_helpers/Cargo.toml +2 -0
  54. data/gems/scheduler/ext/itsi_rb_helpers/src/heap_value.rs +121 -0
  55. data/gems/scheduler/ext/itsi_rb_helpers/src/lib.rs +90 -10
  56. data/gems/scheduler/ext/itsi_scheduler/Cargo.toml +9 -1
  57. data/gems/scheduler/ext/itsi_scheduler/extconf.rb +1 -1
  58. data/gems/scheduler/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
  59. data/gems/scheduler/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
  60. data/gems/scheduler/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
  61. data/gems/scheduler/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
  62. data/gems/scheduler/ext/itsi_scheduler/src/lib.rs +31 -10
  63. data/gems/scheduler/ext/itsi_server/Cargo.toml +41 -0
  64. data/gems/scheduler/ext/itsi_server/extconf.rb +6 -0
  65. data/gems/scheduler/ext/itsi_server/src/body_proxy/big_bytes.rs +104 -0
  66. data/gems/scheduler/ext/itsi_server/src/body_proxy/itsi_body_proxy.rs +122 -0
  67. data/gems/scheduler/ext/itsi_server/src/body_proxy/mod.rs +2 -0
  68. data/gems/scheduler/ext/itsi_server/src/lib.rs +103 -0
  69. data/gems/scheduler/ext/itsi_server/src/request/itsi_request.rs +277 -0
  70. data/gems/scheduler/ext/itsi_server/src/request/mod.rs +1 -0
  71. data/gems/scheduler/ext/itsi_server/src/response/itsi_response.rs +347 -0
  72. data/gems/scheduler/ext/itsi_server/src/response/mod.rs +1 -0
  73. data/gems/scheduler/ext/itsi_server/src/server/bind.rs +168 -0
  74. data/gems/scheduler/ext/itsi_server/src/server/bind_protocol.rs +37 -0
  75. data/gems/scheduler/ext/itsi_server/src/server/io_stream.rs +104 -0
  76. data/gems/scheduler/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +13 -0
  77. data/gems/scheduler/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +5 -0
  78. data/gems/scheduler/ext/itsi_server/src/server/itsi_server.rs +230 -0
  79. data/gems/scheduler/ext/itsi_server/src/server/lifecycle_event.rs +8 -0
  80. data/gems/scheduler/ext/itsi_server/src/server/listener.rs +259 -0
  81. data/gems/scheduler/ext/itsi_server/src/server/mod.rs +11 -0
  82. data/gems/scheduler/ext/itsi_server/src/server/process_worker.rs +196 -0
  83. data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +253 -0
  84. data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/mod.rs +27 -0
  85. data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/single_mode.rs +238 -0
  86. data/gems/scheduler/ext/itsi_server/src/server/signal.rs +57 -0
  87. data/gems/scheduler/ext/itsi_server/src/server/thread_worker.rs +368 -0
  88. data/gems/scheduler/ext/itsi_server/src/server/tls.rs +152 -0
  89. data/gems/scheduler/ext/itsi_tracing/Cargo.toml +4 -0
  90. data/gems/scheduler/ext/itsi_tracing/src/lib.rs +36 -6
  91. data/gems/scheduler/itsi-scheduler.gemspec +2 -2
  92. data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
  93. data/gems/scheduler/lib/itsi/scheduler.rb +137 -1
  94. data/gems/scheduler/test/helpers/test_helper.rb +24 -0
  95. data/gems/scheduler/test/test_active_record.rb +158 -0
  96. data/gems/scheduler/test/test_address_resolve.rb +23 -0
  97. data/gems/scheduler/test/test_block_unblock.rb +229 -0
  98. data/gems/scheduler/test/test_file_io.rb +193 -0
  99. data/gems/scheduler/test/test_itsi_scheduler.rb +24 -1
  100. data/gems/scheduler/test/test_kernel_sleep.rb +91 -0
  101. data/gems/scheduler/test/test_nested_fibers.rb +286 -0
  102. data/gems/scheduler/test/test_network_io.rb +274 -0
  103. data/gems/scheduler/test/test_process_wait.rb +26 -0
  104. data/gems/server/exe/itsi +88 -28
  105. data/gems/server/ext/itsi_error/Cargo.toml +2 -0
  106. data/gems/server/ext/itsi_error/src/from.rs +70 -0
  107. data/gems/server/ext/itsi_error/src/lib.rs +10 -37
  108. data/gems/server/ext/itsi_instrument_entry/Cargo.toml +15 -0
  109. data/gems/server/ext/itsi_instrument_entry/src/lib.rs +31 -0
  110. data/gems/server/ext/itsi_rb_helpers/Cargo.toml +2 -0
  111. data/gems/server/ext/itsi_rb_helpers/src/heap_value.rs +121 -0
  112. data/gems/server/ext/itsi_rb_helpers/src/lib.rs +90 -10
  113. data/gems/server/ext/itsi_scheduler/Cargo.toml +24 -0
  114. data/gems/server/ext/itsi_scheduler/extconf.rb +6 -0
  115. data/gems/server/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
  116. data/gems/server/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
  117. data/gems/server/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
  118. data/gems/server/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
  119. data/gems/server/ext/itsi_scheduler/src/lib.rs +38 -0
  120. data/gems/server/ext/itsi_server/Cargo.toml +14 -2
  121. data/gems/server/ext/itsi_server/extconf.rb +1 -1
  122. data/gems/server/ext/itsi_server/src/body_proxy/big_bytes.rs +104 -0
  123. data/gems/server/ext/itsi_server/src/body_proxy/itsi_body_proxy.rs +122 -0
  124. data/gems/server/ext/itsi_server/src/body_proxy/mod.rs +2 -0
  125. data/gems/server/ext/itsi_server/src/lib.rs +58 -7
  126. data/gems/server/ext/itsi_server/src/request/itsi_request.rs +238 -104
  127. data/gems/server/ext/itsi_server/src/response/itsi_response.rs +347 -0
  128. data/gems/server/ext/itsi_server/src/response/mod.rs +1 -0
  129. data/gems/server/ext/itsi_server/src/server/bind.rs +50 -20
  130. data/gems/server/ext/itsi_server/src/server/bind_protocol.rs +37 -0
  131. data/gems/server/ext/itsi_server/src/server/io_stream.rs +104 -0
  132. data/gems/server/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +11 -30
  133. data/gems/server/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +3 -50
  134. data/gems/server/ext/itsi_server/src/server/itsi_server.rs +181 -133
  135. data/gems/server/ext/itsi_server/src/server/lifecycle_event.rs +8 -0
  136. data/gems/server/ext/itsi_server/src/server/listener.rs +169 -128
  137. data/gems/server/ext/itsi_server/src/server/mod.rs +7 -1
  138. data/gems/server/ext/itsi_server/src/server/process_worker.rs +196 -0
  139. data/gems/server/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +253 -0
  140. data/gems/server/ext/itsi_server/src/server/serve_strategy/mod.rs +27 -0
  141. data/gems/server/ext/itsi_server/src/server/serve_strategy/single_mode.rs +238 -0
  142. data/gems/server/ext/itsi_server/src/server/signal.rs +57 -0
  143. data/gems/server/ext/itsi_server/src/server/thread_worker.rs +368 -0
  144. data/gems/server/ext/itsi_server/src/server/tls.rs +42 -28
  145. data/gems/server/ext/itsi_tracing/Cargo.toml +4 -0
  146. data/gems/server/ext/itsi_tracing/src/lib.rs +36 -6
  147. data/gems/server/itsi-server.gemspec +4 -4
  148. data/gems/server/lib/itsi/request.rb +30 -14
  149. data/gems/server/lib/itsi/server/rack/handler/itsi.rb +25 -0
  150. data/gems/server/lib/itsi/server/scheduler_mode.rb +6 -0
  151. data/gems/server/lib/itsi/server/version.rb +1 -1
  152. data/gems/server/lib/itsi/server.rb +68 -2
  153. data/gems/server/lib/itsi/signals.rb +18 -0
  154. data/gems/server/lib/itsi/stream_io.rb +38 -0
  155. data/gems/server/test/test_helper.rb +2 -0
  156. data/gems/server/test/test_itsi_server.rb +1 -1
  157. data/lib/itsi/version.rb +1 -1
  158. data/tasks.txt +17 -0
  159. metadata +102 -12
  160. data/crates/itsi_server/src/server/transfer_protocol.rs +0 -23
  161. data/crates/itsi_server/src/stream_writer/mod.rs +0 -21
  162. data/gems/scheduler/test/test_helper.rb +0 -6
  163. data/gems/server/ext/itsi_server/src/server/transfer_protocol.rs +0 -23
  164. 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
- # Your code goes here...
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