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
@@ -0,0 +1,274 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "timeout"
5
+ require "debug"
6
+
7
+ class TestNetworkIO < Minitest::Test
8
+ include Itsi::Scheduler::TestHelper
9
+
10
+ def test_tcp_echo
11
+ message = "Hello, Itsi!"
12
+ response = nil
13
+
14
+ with_scheduler do |_scheduler|
15
+ server = TCPServer.new("127.0.0.1", 0)
16
+ port = server.addr[1]
17
+
18
+ # Server fiber: accept one connection and echo data.
19
+ Fiber.schedule do
20
+ client = server.accept
21
+ data = client.read(message.size)
22
+ client.write(data)
23
+ client.close
24
+ server.close
25
+ end
26
+
27
+ # Client fiber: connect, send message, and read echo.
28
+ Fiber.schedule do
29
+ client = TCPSocket.new("127.0.0.1", port)
30
+ client.write(message)
31
+ response = client.read(message.size)
32
+ client.close
33
+ end
34
+ end
35
+
36
+ assert_equal message, response
37
+ end
38
+
39
+ def test_concurrent_tcp_clients
40
+ server = TCPServer.new("127.0.0.1", 0)
41
+ port = server.addr[1]
42
+ messages = %w[first second third fourth]
43
+ responses = {}
44
+
45
+ with_scheduler do |_scheduler|
46
+ # Server fiber: accept several connections.
47
+ Fiber.schedule do
48
+ messages.size.times do
49
+ client = server.accept
50
+ # Spawn a fiber for each connection to echo the data.
51
+ Fiber.schedule do
52
+ data = client.readpartial(1024)
53
+ client.write(data)
54
+ client.close
55
+ end
56
+ end
57
+ server.close
58
+ end
59
+
60
+ # Client fibers: connect concurrently and send messages.
61
+ messages.each do |msg|
62
+ Fiber.schedule do
63
+ sleep rand(0.01..0.05) # random delay for interleaving
64
+ client = TCPSocket.new("127.0.0.1", port)
65
+ client.write(msg)
66
+ responses[msg] = client.read(msg.size)
67
+ client.close
68
+ end
69
+ end
70
+ end
71
+
72
+ messages.each do |msg|
73
+ assert_equal msg, responses[msg]
74
+ end
75
+ end
76
+
77
+ def test_interleaved_network_and_sleep
78
+ server = TCPServer.new("127.0.0.1", 0)
79
+ port = server.addr[1]
80
+ responses = {}
81
+
82
+ with_scheduler do |_scheduler|
83
+ # Echo server that delays between chunks.
84
+ Fiber.schedule do
85
+ client = server.accept
86
+ data = "".dup
87
+ # Read in chunks until we have received 12 bytes.
88
+ while data.size < 12
89
+ begin
90
+ chunk = client.readpartial(3)
91
+ rescue EOFError
92
+ break
93
+ end
94
+ data << chunk
95
+ sleep 0.02 # delay between chunks
96
+ end
97
+ # Echo back the reversed data.
98
+ client.write(data.reverse)
99
+ client.close
100
+ server.close
101
+ end
102
+
103
+ # Client fiber: send data in small chunks.
104
+ Fiber.schedule do
105
+ sleep 0.05 # allow server to start
106
+ client = TCPSocket.new("127.0.0.1", port)
107
+ "HelloWorld!!".chars.each_slice(3) do |slice|
108
+ client.write(slice.join)
109
+ sleep 0.01
110
+ end
111
+ responses[:result] = client.read(12)
112
+ client.close
113
+ end
114
+ end
115
+
116
+ expected = "HelloWorld!!".reverse[0, 12]
117
+ assert_equal expected, responses[:result]
118
+ end
119
+
120
+ def test_tcp_timeout
121
+ server = TCPServer.new("127.0.0.1", 0)
122
+ port = server.addr[1]
123
+ result = nil
124
+
125
+ with_scheduler do |_scheduler|
126
+ # Server fiber: accept the connection but delay sending.
127
+ Fiber.schedule do
128
+ client = server.accept
129
+ sleep 0.2 # delay long enough to force a timeout on the client
130
+ client.write("late data")
131
+ client.close
132
+ server.close
133
+ end
134
+
135
+ # Client fiber: connect and wait for data with a short timeout.
136
+ Fiber.schedule do
137
+ sleep 0.05
138
+ client = TCPSocket.new("127.0.0.1", port)
139
+ result = if client.wait_readable(0.1)
140
+ client.readpartial(1024)
141
+ else
142
+ nil
143
+ end
144
+ client.close
145
+ end
146
+ end
147
+
148
+ # The client should time out (i.e. result remains nil) because the server waits too long.
149
+ assert_nil result
150
+ end
151
+
152
+ def test_multiple_fibers_on_same_socket
153
+ server = TCPServer.new("127.0.0.1", 0)
154
+ port = server.addr[1]
155
+ results = []
156
+
157
+ with_scheduler do |_scheduler|
158
+ # Server fiber: accept a connection and send a short message.
159
+ Fiber.schedule do
160
+ client1 = server.accept
161
+ client2 = server.accept
162
+ client1.write("network")
163
+ client2.write("network")
164
+ client1.close
165
+ client2.close
166
+ server.close
167
+ end
168
+
169
+ # Two separate client fibers using separate connections.
170
+ # (In many schedulers, if two fibers wait on the same IO object,
171
+ # only one may be resumed when data becomes available.)
172
+ 2.times do |i|
173
+ Fiber.schedule do
174
+ sleep 0.001
175
+ client = TCPSocket.new("127.0.0.1", port)
176
+ res = client.wait_readable(0.05) ? "readable" : "timeout"
177
+ results << res
178
+ client.close
179
+ end
180
+ end
181
+ end
182
+
183
+ assert_equal 2, results.size
184
+ assert_includes results, "readable"
185
+ end
186
+
187
+ def test_two_fibers_only_one_connects
188
+ server = TCPServer.new("127.0.0.1", 0)
189
+ port = server.addr[1]
190
+ results = []
191
+
192
+ with_scheduler do |_scheduler|
193
+ # Server fiber: accept a connection and send a short message.
194
+ Fiber.schedule do
195
+ client1 = server.accept
196
+ client1.write("network")
197
+ client1.close
198
+ sleep 0.1
199
+ server.close
200
+ end
201
+
202
+ # Two separate client fibers using separate connections.
203
+ # (In many schedulers, if two fibers wait on the same IO object,
204
+ # only one may be resumed when data becomes available.)
205
+ 2.times do |i|
206
+ Fiber.schedule do
207
+ sleep 0.001
208
+ client = TCPSocket.new("127.0.0.1", port)
209
+ res = client.wait_readable(0.05) ? "readable" : "timeout"
210
+ results << res
211
+ client.close
212
+ end
213
+ end
214
+ end
215
+
216
+ assert_equal 2, results.size
217
+ assert_includes results, "readable"
218
+ assert_includes results, "timeout"
219
+ end
220
+
221
+ def test_udp_and_tcp_interleaving
222
+ # Set up a UDP server.
223
+ udp_server = UDPSocket.new
224
+ udp_server.bind("127.0.0.1", 0)
225
+ udp_port = udp_server.addr[1]
226
+
227
+ # Set up a TCP server.
228
+ tcp_server = TCPServer.new("127.0.0.1", 0)
229
+ tcp_port = tcp_server.addr[1]
230
+
231
+ udp_result = nil
232
+ tcp_result = nil
233
+
234
+ with_scheduler do |_scheduler|
235
+ # UDP server fiber: wait for a datagram.
236
+ Fiber.schedule do
237
+ udp_result = udp_server.recvfrom(1024)[0]
238
+ udp_server.close
239
+ end
240
+
241
+ # TCP server fiber: accept a connection and read data.
242
+ Fiber.schedule do
243
+ client = tcp_server.accept
244
+ tcp_result = client.readpartial(1024)
245
+ client.close
246
+ tcp_server.close
247
+ end
248
+
249
+ # UDP client fiber: send a datagram.
250
+ Fiber.schedule do
251
+ sleep 0.02
252
+ udp_client = UDPSocket.new
253
+ udp_client.send("udp data", 0, "127.0.0.1", udp_port)
254
+ udp_client.close
255
+ end
256
+
257
+ # TCP client fiber: connect and send data.
258
+ Fiber.schedule do
259
+ sleep 0.02
260
+ client = TCPSocket.new("127.0.0.1", tcp_port)
261
+ client.write("tcp data")
262
+ client.close
263
+ end
264
+
265
+ # Additional fiber to interleave a sleep.
266
+ Fiber.schedule do
267
+ sleep 0.05
268
+ end
269
+ end
270
+
271
+ assert_equal "udp data", udp_result
272
+ assert_equal "tcp data", tcp_result
273
+ end
274
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+ require 'debug'
3
+
4
+
5
+ class TestProcessWait < Minitest::Test
6
+ include Itsi::Scheduler::TestHelper
7
+
8
+ def test_process_wait
9
+ start_time = Time.now
10
+ pids = []
11
+
12
+ with_scheduler do |_scheduler|
13
+ 3.times do
14
+ pids << Process.spawn("sleep 0.25")
15
+ end
16
+ 3.times do |i|
17
+ Fiber.schedule do
18
+ Process.wait(pids[i])
19
+ end
20
+ end
21
+ end
22
+ end_time = Time.now
23
+ assert_in_delta(0.25, end_time - start_time, 0.1)
24
+
25
+ end
26
+ end
data/gems/server/exe/itsi CHANGED
@@ -1,27 +1,32 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
-
4
3
  require "optparse"
5
- require "rack"
6
- require "etc"
7
4
 
8
5
  # Default options used when starting Osprey from the CLI using `osprey`
9
6
  DEFAULT_OPTIONS = {
10
7
  # Number of workers
11
- workers: Etc.nprocessors,
8
+ workers: 1,
12
9
  # Number of threads per worker
13
10
  threads: 1,
14
11
  # Graceful shutdown timeout
15
- shutdown_timeout: 0.3,
12
+ shutdown_timeout: 5,
16
13
  # Binds
17
- binds: ['http://0.0.0.0:3000']
14
+ binds: ["http://0.0.0.0:3000"],
15
+ # Preload
16
+ preload: true,
17
+ # Rackup file
18
+ rackup_file: "config.ru",
19
+ # Scheduler class
20
+ scheduler_class: nil,
21
+ # Whether to stream the body or not
22
+ stream_body: false
18
23
  }
19
24
 
20
25
  options = DEFAULT_OPTIONS.to_a.select(&:last).to_h
21
26
 
22
27
  # Define the option parser
23
28
  OptionParser.new do |opts|
24
- opts.banner = "Usage: script.rb [options]"
29
+ opts.banner = "Usage: itsi [options]"
25
30
 
26
31
  opts.on("-w", "--workers WORKERS", Integer, "Number of workers (default: #{options[:workers]})") do |w|
27
32
  options[:workers] = w
@@ -31,29 +36,64 @@ OptionParser.new do |opts|
31
36
  options[:threads] = t
32
37
  end
33
38
 
34
- opts.on("-h", "--host HOST", String, "Host to bind to (default: #{options[:host]})") do |h|
35
- options[:host] = h
39
+ opts.on("-r", "--rackup_file FILE", String, "Rackup file to use (default: #{options[:rackup_file]})") do |rf|
40
+ options[:rackup_file] = rf
36
41
  end
37
42
 
38
- opts.on("-p", "--port PORT", Integer, "Port for the application (default: #{options[:port]})") do |p|
39
- options[:port] = p
43
+ opts.on("--worker-memory-limit MEMORY_LIMIT", Integer,
44
+ "Memory limit for each worker (default: #{options[:worker_memory_limit] || 'None'}). If this limit is breached the worker is gracefully restarted") do |ml|
45
+ options[:worker_memory_limit] = ml
40
46
  end
41
47
 
42
- opts.on("-f", "--use_fiber_scheduler PORT", TrueClass,
43
- "Port for the application (default: #{options[:use_scheduler]})") do |p|
44
- options[:use_scheduler] = p
48
+ opts.on("-f", "--fiber_scheduler [CLASS_NAME]", [String], "Scheduler class to use (default: nil). Provide blank or true to use Itsi::Scheduler, or a classname to use an alternative scheduler") do |scheduler_class|
49
+ if scheduler_class.nil? || scheduler_class == "true"
50
+ options[:scheduler_class] = "Itsi::Scheduler"
51
+ elsif scheduler_class == "false"
52
+ options.delete(:scheduler_class)
53
+ else
54
+ options[:scheduler_class] = scheduler_class
55
+ end
45
56
  end
46
57
 
47
- opts.on("--http_port HTTP_PORT", Integer, "HTTP port for the application (default: #{options[:http_port]})") do |hp|
48
- options[:http_port] = hp
58
+ opts.on("--preload [true, false, :bundle_group_name]", String, " Toggle preloading the application") do |preload|
59
+ if preload == "true"
60
+ options[:preload] = true
61
+ elsif preload == "false"
62
+ options[:preload] = false
63
+ else
64
+ # Not supported yet
65
+ end
49
66
  end
50
67
 
51
- opts.on("-c", "--cert_path CERT_PATH", String, "Path to the SSL certificate file") do |cp|
52
- options[:cert_path] = cp
68
+
69
+ opts.on("-b", "--bind BIND", String, "Bind address (default: #{options[:binds].join(", ")}). You can specify this flag multiple times to bind to multiple addresses.") do |bind|
70
+ options[:binds].pop if options[:binds].first.frozen?
71
+ options[:binds] << bind
53
72
  end
54
73
 
55
- opts.on("-k", "--key_path KEY_PATH", String, "Path to the SSL key file") do |kp|
56
- options[:key_path] = kp
74
+ opts.on("-c", "--cert_path CERT_PATH", String,
75
+ "Path to the SSL certificate file (must follow a --bind option). You can specify this flag multiple times.") do |cp|
76
+ raise OptionParser::InvalidOption, "--cert_path must follow a --bind" if options[:binds].empty?
77
+ require "uri"
78
+
79
+ # Modify the last bind entry to add/update the cert query parameter
80
+ uri = URI.parse("http://#{options[:binds].last}") # Ensure valid URI parsing
81
+ params = URI.decode_www_form(uri.query.to_s).to_h
82
+ params["cert"] = cp
83
+ query_string = params.map { |k, v| "#{k}=#{v}" }.join("&")
84
+ options[:binds][-1] = "#{uri.host}?#{query_string}"
85
+ end
86
+
87
+ opts.on("-k", "--key_path KEY_PATH", String, "Path to the SSL key file (must follow a --bind option). You can specify this flag multiple times.") do |kp|
88
+ raise OptionParser::InvalidOption, "--key_path must follow a --bind" if options[:binds].empty?
89
+ require "uri"
90
+
91
+ # Modify the last bind entry to add/update the key query parameter
92
+ uri = URI.parse("http://#{options[:binds].last}") # Ensure valid URI parsing
93
+ params = URI.decode_www_form(uri.query.to_s).to_h
94
+ params["key"] = kp
95
+ query_string = params.map { |k, v| "#{k}=#{v}" }.join("&")
96
+ options[:binds][-1] = "#{uri.host}?#{query_string}"
57
97
  end
58
98
 
59
99
  opts.on("--shutdown_timeout SHUTDOWN_TIMEOUT", String,
@@ -65,20 +105,40 @@ OptionParser.new do |opts|
65
105
  options[:script_name] = script_name
66
106
  end
67
107
 
68
- opts.on("--help", "Show this help message") do
108
+ opts.on("--stream-body", TrueClass, "Stream body frames (default: false for best compatibility)") do |stream_body|
109
+ options[:stream_body] = stream_body
110
+ end
111
+
112
+ opts.on("-h", "--help", "Show this help message") do
69
113
  puts opts
70
114
  exit
71
115
  end
72
116
  end.parse!
73
117
 
74
- # Parse the Rack application
75
- app, _ = Rack::Builder.parse_file("config.ru")
118
+ # Rack app loader, invoked per worker.
119
+ # This is a no-op if preloading is enabled (we just return the preloaded app).
120
+ preloader = \
121
+ if options[:preload]
122
+ require "rack"
123
+
124
+ app = Array(Rack::Builder.parse_file(options[:rackup_file])).first
125
+ require_relative "../lib/itsi/server/scheduler_mode" if options[:scheduler_class]
126
+ app.method(:itself).to_proc
127
+ else
128
+ lambda do
129
+ require "rack"
130
+
131
+ app = Array(Rack::Builder.parse_file(options[:rackup_file])).first
132
+ require_relative "../lib/itsi/server/scheduler_mode" if options[:scheduler_class]
133
+ app
134
+ end
135
+ end
76
136
 
77
- puts "App is #{app}"
78
- # Make sure osprey is loaded, if not already loaded by the rack_app above.
79
- # Start the Osprey server
137
+ # Make sure Itsi is loaded, if not already loaded by the rack_app above.
138
+ # Start the Itsi server
80
139
  require "itsi/server"
140
+
81
141
  Itsi::Server.new(
82
- app: app,
83
- **options
142
+ app: preloader,
143
+ **options.except(:preload, :rackup_file)
84
144
  ).start
@@ -7,3 +7,5 @@ edition = "2024"
7
7
  thiserror = "2.0.11"
8
8
  magnus = { version = "0.7.1" }
9
9
  rcgen = "0.13.2"
10
+ nix = "0.29.0"
11
+ httparse = "1.10.1"
@@ -0,0 +1,70 @@
1
+ use crate::ItsiError;
2
+ use std::ffi::NulError;
3
+
4
+ pub static CLIENT_CONNECTION_CLOSED: &str = "Client disconnected";
5
+
6
+ impl From<httparse::Error> for ItsiError {
7
+ fn from(err: httparse::Error) -> Self {
8
+ ItsiError::ArgumentError(err.to_string())
9
+ }
10
+ }
11
+
12
+ impl From<nix::errno::Errno> for ItsiError {
13
+ fn from(err: nix::errno::Errno) -> Self {
14
+ ItsiError::ArgumentError(err.to_string())
15
+ }
16
+ }
17
+
18
+ impl From<std::io::Error> for ItsiError {
19
+ fn from(err: std::io::Error) -> Self {
20
+ ItsiError::ArgumentError(err.to_string())
21
+ }
22
+ }
23
+
24
+ impl From<rcgen::Error> for ItsiError {
25
+ fn from(err: rcgen::Error) -> Self {
26
+ ItsiError::ArgumentError(err.to_string())
27
+ }
28
+ }
29
+
30
+ impl From<NulError> for ItsiError {
31
+ fn from(err: NulError) -> Self {
32
+ ItsiError::ArgumentError(err.to_string())
33
+ }
34
+ }
35
+
36
+ impl From<magnus::Error> for ItsiError {
37
+ fn from(err: magnus::Error) -> Self {
38
+ match err.error_type() {
39
+ magnus::error::ErrorType::Jump(tag) => ItsiError::Jump(tag.to_string()),
40
+ magnus::error::ErrorType::Error(_exception_class, cow) => {
41
+ ItsiError::ArgumentError(cow.to_string())
42
+ }
43
+ magnus::error::ErrorType::Exception(exception) => {
44
+ ItsiError::ArgumentError(exception.to_string())
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ impl From<ItsiError> for magnus::Error {
51
+ fn from(err: ItsiError) -> Self {
52
+ match err {
53
+ ItsiError::InvalidInput(msg) => magnus::Error::new(magnus::exception::arg_error(), msg),
54
+ ItsiError::InternalServerError(msg) => {
55
+ magnus::Error::new(magnus::exception::exception(), msg)
56
+ }
57
+ ItsiError::UnsupportedProtocol(msg) => {
58
+ magnus::Error::new(magnus::exception::arg_error(), msg)
59
+ }
60
+ ItsiError::ArgumentError(msg) => {
61
+ magnus::Error::new(magnus::exception::arg_error(), msg)
62
+ }
63
+ ItsiError::Jump(msg) => magnus::Error::new(magnus::exception::local_jump_error(), msg),
64
+ ItsiError::Break() => magnus::Error::new(magnus::exception::interrupt(), "Break"),
65
+ ItsiError::ClientConnectionClosed => {
66
+ magnus::Error::new(magnus::exception::eof_error(), CLIENT_CONNECTION_CLOSED)
67
+ }
68
+ }
69
+ }
70
+ }
@@ -1,49 +1,22 @@
1
+ pub mod from;
1
2
  use thiserror::Error;
2
3
 
3
4
  pub type Result<T> = std::result::Result<T, ItsiError>;
4
5
 
5
6
  #[derive(Error, Debug)]
6
7
  pub enum ItsiError {
7
- #[error("Invalid input")]
8
+ #[error("Invalid input {0}")]
8
9
  InvalidInput(String),
9
- #[error("Internal server error")]
10
- InternalServerError,
11
- #[error("Unsupported protocol")]
10
+ #[error("Internal server error {0}")]
11
+ InternalServerError(String),
12
+ #[error("Unsupported protocol {0}")]
12
13
  UnsupportedProtocol(String),
13
- #[error("Argument error")]
14
+ #[error("Argument error: {0}")]
14
15
  ArgumentError(String),
16
+ #[error("Client Connection Closed")]
17
+ ClientConnectionClosed,
15
18
  #[error("Jump")]
16
19
  Jump(String),
17
- }
18
-
19
- impl From<ItsiError> for magnus::Error {
20
- fn from(err: ItsiError) -> Self {
21
- magnus::Error::new(magnus::exception::runtime_error(), err.to_string())
22
- }
23
- }
24
-
25
- impl From<std::io::Error> for ItsiError {
26
- fn from(err: std::io::Error) -> Self {
27
- ItsiError::ArgumentError(err.to_string())
28
- }
29
- }
30
-
31
- impl From<rcgen::Error> for ItsiError {
32
- fn from(err: rcgen::Error) -> Self {
33
- ItsiError::ArgumentError(err.to_string())
34
- }
35
- }
36
-
37
- impl From<magnus::Error> for ItsiError {
38
- fn from(err: magnus::Error) -> Self {
39
- match err.error_type() {
40
- magnus::error::ErrorType::Jump(tag) => ItsiError::Jump(tag.to_string()),
41
- magnus::error::ErrorType::Error(_exception_class, cow) => {
42
- ItsiError::ArgumentError(cow.to_string())
43
- }
44
- magnus::error::ErrorType::Exception(exception) => {
45
- ItsiError::ArgumentError(exception.to_string())
46
- }
47
- }
48
- }
20
+ #[error("Break")]
21
+ Break(),
49
22
  }
@@ -0,0 +1,15 @@
1
+ [package]
2
+ name = "itsi_instrument_entry"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ authors = ["Wouter Coppieters <wc@pico.net.nz>"]
6
+ license = "MIT"
7
+ publish = false
8
+
9
+
10
+ [lib]
11
+ proc-macro = true
12
+ [dependencies]
13
+ proc-macro2 = "1.0"
14
+ quote = "1.0"
15
+ syn = { version = "1.0", features = ["full"] }
@@ -0,0 +1,31 @@
1
+ use proc_macro::TokenStream;
2
+ use proc_macro2::TokenStream as TokenStream2;
3
+ use quote::quote;
4
+ use syn::{parse_macro_input, ItemFn};
5
+
6
+ #[proc_macro_attribute]
7
+ pub fn instrument_with_entry(attr: TokenStream, item: TokenStream) -> TokenStream {
8
+ let attr_tokens = TokenStream2::from(attr);
9
+ let input_fn = parse_macro_input!(item as ItemFn);
10
+ let attrs = input_fn.attrs;
11
+ let vis = input_fn.vis;
12
+ let sig = input_fn.sig;
13
+ let block = input_fn.block;
14
+ let output = quote! {
15
+ #[cfg(debug_assertions)]
16
+ #[tracing::instrument(#attr_tokens)]
17
+ #(#attrs)*
18
+ #vis #sig {
19
+ tracing::trace!("");
20
+ #block
21
+ }
22
+
23
+ #[cfg(not(debug_assertions))]
24
+ #(#attrs)*
25
+ #vis #sig {
26
+ #block
27
+ }
28
+ };
29
+
30
+ output.into()
31
+ }
@@ -4,5 +4,7 @@ version = "0.1.0"
4
4
  edition = "2024"
5
5
 
6
6
  [dependencies]
7
+ cfg-if = "1.0.0"
7
8
  magnus = { version = "0.7.1", features = ["rb-sys", "bytes"] }
9
+ nix = "0.29.0"
8
10
  rb-sys = "0.9.105"