itsi 0.1.0 → 0.1.3

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 +196 -134
  34. data/crates/itsi_server/src/server/lifecycle_event.rs +9 -0
  35. data/crates/itsi_server/src/server/listener.rs +184 -127
  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 +254 -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 +241 -0
  41. data/crates/itsi_server/src/server/signal.rs +70 -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 +244 -0
  79. data/gems/scheduler/ext/itsi_server/src/server/lifecycle_event.rs +9 -0
  80. data/gems/scheduler/ext/itsi_server/src/server/listener.rs +275 -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 +254 -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 +241 -0
  86. data/gems/scheduler/ext/itsi_server/src/server/signal.rs +70 -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 -3
  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 +196 -134
  135. data/gems/server/ext/itsi_server/src/server/lifecycle_event.rs +9 -0
  136. data/gems/server/ext/itsi_server/src/server/listener.rs +184 -127
  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 +254 -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 +241 -0
  142. data/gems/server/ext/itsi_server/src/server/signal.rs +70 -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 -5
  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 +82 -2
  153. data/gems/server/lib/itsi/signals.rb +23 -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 +18 -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
@@ -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
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ class TestFileIO < Minitest::Test
5
+ include Itsi::Scheduler::TestHelper
6
+
7
+ # Test that a fiber waiting to read 5 bytes from a pipe is resumed
8
+ # when another fiber writes "hello" to the pipe.
9
+ def test_io_read_resume
10
+ reader, writer = IO.pipe
11
+ result = nil
12
+
13
+ with_scheduler do |_scheduler|
14
+ Fiber.schedule do
15
+ result = reader.read(5)
16
+ end
17
+
18
+ Fiber.schedule do
19
+ sleep 0.05
20
+ writer.write("hello")
21
+ end
22
+ end
23
+
24
+ reader.close
25
+ writer.close
26
+
27
+ assert_equal "hello", result
28
+ end
29
+
30
+ # Test that IO.wait_readable times out correctly when no data arrives.
31
+ def test_io_wait_readable_timeout
32
+ reader, writer = IO.pipe
33
+
34
+ result = nil
35
+ with_scheduler do |_scheduler|
36
+ Fiber.schedule do
37
+ # When no data is available, wait_readable should return nil after the timeout.
38
+ result = reader.wait_readable(0.05)
39
+ puts "Awoken"
40
+ end
41
+ end
42
+
43
+ reader.close
44
+ writer.close
45
+
46
+ assert_nil result, "Expected nil on timeout when no data is available"
47
+ end
48
+
49
+ # Test that two different pipes can be read concurrently.
50
+ def test_multiple_io_reads
51
+ reader1, writer1 = IO.pipe
52
+ reader2, writer2 = IO.pipe
53
+ results = {}
54
+
55
+ with_scheduler do |_scheduler|
56
+ Fiber.schedule do
57
+ results[:first] = reader1.read(5)
58
+ end
59
+
60
+ Fiber.schedule do
61
+ results[:second] = reader2.read(6)
62
+ end
63
+
64
+ Fiber.schedule do
65
+ sleep 0.05
66
+ writer1.write("first")
67
+ writer2.write("second")
68
+ end
69
+ end
70
+
71
+ reader1.close
72
+ writer1.close
73
+ reader2.close
74
+ writer2.close
75
+
76
+ assert_equal "first", results[:first]
77
+ assert_equal "second", results[:second]
78
+ end
79
+
80
+ # Test an interleaved read/write operation.
81
+ # One fiber writes the data in chunks (with short sleeps in between),
82
+ # while another fiber reads in fixed-size chunks until EOF.
83
+ def test_io_interleaved_read_write
84
+ reader, writer = IO.pipe
85
+ data_read = "".dup
86
+
87
+ with_scheduler do |_scheduler|
88
+ Fiber.schedule do
89
+ loop do
90
+ chunk = reader.read(3)
91
+ break if chunk.nil? || chunk.empty?
92
+
93
+ data_read << chunk
94
+ end
95
+ end
96
+
97
+ Fiber.schedule do
98
+ ["Hel", "lo ", "Wor", "ld"].each do |part|
99
+ writer.write(part)
100
+ sleep 0.02
101
+ end
102
+ writer.close # signal EOF to the reader
103
+ end
104
+ end
105
+
106
+ reader.close
107
+
108
+ assert_equal "Hello World", data_read
109
+ end
110
+
111
+ # Test that a fiber using nonblocking I/O correctly waits for data.
112
+ def test_io_read_with_nonblocking_mode
113
+ reader, writer = IO.pipe
114
+ # Ensure the IOs are in synchronous mode.
115
+ reader.sync = true
116
+ writer.sync = true
117
+ result = nil
118
+
119
+ with_scheduler do |_scheduler|
120
+ Fiber.schedule do
121
+ # Use wait_readable to wait until data is available.
122
+ reader.wait_readable(0.2)
123
+ result = reader.read_nonblock(5)
124
+ end
125
+
126
+ Fiber.schedule do
127
+ sleep 0.05
128
+ writer.write("hello")
129
+ end
130
+ end
131
+
132
+ reader.close
133
+ writer.close
134
+
135
+ assert_equal "hello", result
136
+ end
137
+
138
+ # Test that writing to an IO (here a pipe) succeeds immediately
139
+ # when the pipe is ready for writing.
140
+ def test_io_write_immediate
141
+ reader, writer = IO.pipe
142
+ bytes_written = nil
143
+
144
+ with_scheduler do |_scheduler|
145
+ Fiber.schedule do
146
+ # If the pipe is empty, write should not block.
147
+ bytes_written = writer.write("test")
148
+ end
149
+ end
150
+
151
+ reader.close
152
+ writer.close
153
+
154
+ assert_equal 4, bytes_written
155
+ end
156
+
157
+ # Test what happens when two fibers are waiting on the same IO.
158
+ # With the current scheduler design, only one fiber will be resumed
159
+ # when data becomes available, and the other will eventually time out.
160
+ def test_multiple_fibers_waiting_on_same_fd
161
+ reader, writer = IO.pipe
162
+ results = []
163
+ data = "data"
164
+
165
+ with_scheduler do |_scheduler|
166
+ Fiber.schedule do
167
+ if res = reader.wait_readable(0.1)
168
+ reader.read(data.length)
169
+ end
170
+ results << (res ? "readable" : "timeout")
171
+ end
172
+
173
+ Fiber.schedule do
174
+ if res = reader.wait_readable(0.1)
175
+ reader.read(data.length)
176
+ end
177
+ results << (res ? "readable" : "timeout")
178
+ end
179
+
180
+ Fiber.schedule do
181
+ writer.write(data)
182
+ end
183
+ end
184
+
185
+ writer.close
186
+ reader.close
187
+
188
+ # One fiber should be resumed with "readable" while the other times out.
189
+ assert_equal 2, results.size
190
+ assert_includes results, "readable"
191
+ assert_includes results, "timeout"
192
+ end
193
+ end
@@ -1,9 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "test_helper"
4
3
 
5
4
  class TestItsiScheduler < Minitest::Test
5
+ include Itsi::Scheduler::TestHelper
6
+
6
7
  def test_that_it_has_a_version_number
7
8
  refute_nil ::Itsi::Scheduler::VERSION
8
9
  end
10
+
11
+ def test_errors
12
+ results = []
13
+ start_at = Time.now
14
+ # Run the scheduler in a dedicated thread to avoid interference with the
15
+ # main thread’s scheduler state.
16
+ total = 0
17
+ out, err = capture_subprocess_io do
18
+ with_scheduler do |_scheduler|
19
+ 9.times do |i|
20
+ Fiber.schedule do
21
+ sleep 0.05
22
+ raise i if i % 3 == 0
23
+ total += 1
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ assert_equal total, 6
30
+ assert_match /Failed to resume fiber /, out
31
+ end
9
32
  end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+
4
+ class TestKernelSleep < Minitest::Test
5
+ include Itsi::Scheduler::TestHelper
6
+
7
+ def test_that_it_has_a_version_number
8
+ refute_nil ::Itsi::Scheduler::VERSION
9
+ end
10
+
11
+ def test_it_can_sleep_concurrently
12
+ results = []
13
+ start_at = Time.now
14
+ # Run the scheduler in a dedicated thread to avoid interference with the
15
+ # main thread’s scheduler state.
16
+ with_scheduler do |_scheduler|
17
+ 5.times do
18
+ Fiber.schedule do
19
+ sleep 0.05
20
+ results << "first"
21
+ sleep 0.05
22
+ results << "second"
23
+ end
24
+ end
25
+ end
26
+
27
+ ends_at = Time.now
28
+ # We expect 10 sleep completions overall (2 per fiber).
29
+ assert_equal 10, results.size
30
+ # Because all sleeps run concurrently, the total elapsed time should be about 1 second.
31
+ assert_in_delta 0.1, ends_at - start_at, 0.02, "Total elapsed time should be close to 0.1 second"
32
+ end
33
+
34
+ def test_sleep_zero_duration
35
+ with_scheduler do |_scheduler|
36
+ start = Time.now
37
+ result = sleep(0)
38
+ finish = Time.now
39
+ assert result, "sleep(0) should return a truthy value"
40
+ # Expect near-immediate return.
41
+ assert_operator finish - start, :<, 0.01, "sleep(0) should not delay execution"
42
+ end
43
+ end
44
+
45
+ def test_multiple_sleeps_in_single_fiber
46
+ order = []
47
+ with_scheduler do |_scheduler|
48
+ # Schedule one fiber that sleeps twice.
49
+ Fiber.schedule do
50
+ order << "start"
51
+ sleep 0.2
52
+ order << "after first sleep"
53
+ sleep 0.3
54
+ order << "after second sleep"
55
+ end
56
+ end
57
+
58
+ assert_equal ["start", "after first sleep", "after second sleep"], order
59
+ end
60
+
61
+ def test_fibers_wake_in_correct_order
62
+ order = []
63
+ with_scheduler do |_scheduler|
64
+ # Schedule three fibers with different sleep durations.
65
+ Fiber.schedule do
66
+ sleep 0.3
67
+ order << "fiber1"
68
+ end
69
+ Fiber.schedule do
70
+ sleep 0.1
71
+ order << "fiber2"
72
+ end
73
+ Fiber.schedule do
74
+ sleep 0.2
75
+ order << "fiber3"
76
+ end
77
+ end
78
+
79
+ # Since the fibers sleep for 0.1, 0.2, and 0.3 seconds respectively,
80
+ # we expect the wake-up order to be: fiber2, then fiber3, then fiber1.
81
+ assert_equal %w[fiber2 fiber3 fiber1], order
82
+ end
83
+
84
+ def test_invalid_sleep_value
85
+ with_scheduler do |_scheduler|
86
+ assert_raises(TypeError) do
87
+ sleep("not a number")
88
+ end
89
+ end
90
+ end
91
+ end