itsi-server 0.1.1 → 0.1.11
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.
Potentially problematic release.
This version of itsi-server might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Cargo.lock +2926 -0
- data/Cargo.toml +7 -0
- data/Rakefile +8 -1
- data/exe/itsi +119 -29
- data/ext/itsi_error/Cargo.toml +2 -0
- data/ext/itsi_error/src/from.rs +68 -0
- data/ext/itsi_error/src/lib.rs +13 -38
- data/ext/itsi_instrument_entry/Cargo.toml +15 -0
- data/ext/itsi_instrument_entry/src/lib.rs +31 -0
- data/ext/itsi_rb_helpers/Cargo.toml +2 -0
- data/ext/itsi_rb_helpers/src/heap_value.rs +121 -0
- data/ext/itsi_rb_helpers/src/lib.rs +112 -9
- data/ext/itsi_scheduler/Cargo.toml +24 -0
- data/ext/itsi_scheduler/extconf.rb +6 -0
- data/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
- data/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
- data/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
- data/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
- data/ext/itsi_scheduler/src/lib.rs +38 -0
- data/ext/itsi_server/Cargo.lock +2956 -0
- data/ext/itsi_server/Cargo.toml +25 -4
- data/ext/itsi_server/extconf.rb +1 -1
- data/ext/itsi_server/src/body_proxy/big_bytes.rs +104 -0
- data/ext/itsi_server/src/body_proxy/itsi_body_proxy.rs +122 -0
- data/ext/itsi_server/src/body_proxy/mod.rs +2 -0
- data/ext/itsi_server/src/env.rs +43 -0
- data/ext/itsi_server/src/lib.rs +136 -8
- data/ext/itsi_server/src/request/itsi_request.rs +258 -103
- data/ext/itsi_server/src/response/itsi_response.rs +357 -0
- data/ext/itsi_server/src/response/mod.rs +1 -0
- data/ext/itsi_server/src/server/bind.rs +65 -29
- data/ext/itsi_server/src/server/bind_protocol.rs +37 -0
- data/ext/itsi_server/src/server/io_stream.rs +104 -0
- data/ext/itsi_server/src/server/itsi_server.rs +245 -139
- data/ext/itsi_server/src/server/lifecycle_event.rs +9 -0
- data/ext/itsi_server/src/server/listener.rs +237 -137
- data/ext/itsi_server/src/server/mod.rs +7 -1
- data/ext/itsi_server/src/server/process_worker.rs +203 -0
- data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +260 -0
- data/ext/itsi_server/src/server/serve_strategy/mod.rs +27 -0
- data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +276 -0
- data/ext/itsi_server/src/server/signal.rs +74 -0
- data/ext/itsi_server/src/server/thread_worker.rs +399 -0
- data/ext/itsi_server/src/server/tls/locked_dir_cache.rs +132 -0
- data/ext/itsi_server/src/server/tls.rs +187 -60
- data/ext/itsi_tracing/Cargo.toml +4 -0
- data/ext/itsi_tracing/src/lib.rs +53 -6
- data/lib/itsi/index.html +91 -0
- data/lib/itsi/request.rb +38 -14
- data/lib/itsi/server/Itsi.rb +127 -0
- data/lib/itsi/server/config.rb +36 -0
- data/lib/itsi/server/options_dsl.rb +401 -0
- data/lib/itsi/server/rack/handler/itsi.rb +36 -0
- data/lib/itsi/server/rack_interface.rb +75 -0
- data/lib/itsi/server/scheduler_interface.rb +21 -0
- data/lib/itsi/server/scheduler_mode.rb +6 -0
- data/lib/itsi/server/signal_trap.rb +23 -0
- data/lib/itsi/server/version.rb +1 -1
- data/lib/itsi/server.rb +79 -9
- data/lib/itsi/stream_io.rb +38 -0
- metadata +49 -27
- data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -32
- data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -52
- data/ext/itsi_server/src/server/transfer_protocol.rs +0 -23
- data/ext/itsi_server/src/stream_writer/mod.rs +0 -21
@@ -0,0 +1,36 @@
|
|
1
|
+
module Itsi
|
2
|
+
class Server
|
3
|
+
module Config
|
4
|
+
module_function
|
5
|
+
|
6
|
+
ITSI_DEFAULT_CONFIG_FILE = "Itsi.rb"
|
7
|
+
|
8
|
+
def load(options)
|
9
|
+
options[:config_file] ||= \
|
10
|
+
if File.exist?(ITSI_DEFAULT_CONFIG_FILE)
|
11
|
+
ITSI_DEFAULT_CONFIG_FILE
|
12
|
+
elsif File.exist?("config/#{ITSI_DEFAULT_CONFIG_FILE}")
|
13
|
+
"config/#{ITSI_DEFAULT_CONFIG_FILE}"
|
14
|
+
end
|
15
|
+
|
16
|
+
# Options simply pass through unless we've specified a config file
|
17
|
+
return options unless options[:config_file]
|
18
|
+
|
19
|
+
require_relative "options_dsl"
|
20
|
+
OptionsDSL.evaluate(options[:config_file]).merge(options)
|
21
|
+
end
|
22
|
+
|
23
|
+
def write_default
|
24
|
+
if File.exist?(ITSI_DEFAULT_CONFIG_FILE)
|
25
|
+
puts "#{ITSI_DEFAULT_CONFIG_FILE} already exists."
|
26
|
+
return
|
27
|
+
end
|
28
|
+
|
29
|
+
puts "Writing default configuration..."
|
30
|
+
File.open(ITSI_DEFAULT_CONFIG_FILE, "w") do |file|
|
31
|
+
file.write(IO.read("#{__dir__}/Itsi.rb"))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,401 @@
|
|
1
|
+
module Itsi
|
2
|
+
class Server
|
3
|
+
class OptionsDSL
|
4
|
+
attr_reader :parent, :children, :filters, :endpoint_defs, :controller_class
|
5
|
+
|
6
|
+
def self.evaluate(filename)
|
7
|
+
new do
|
8
|
+
instance_eval(IO.read(filename))
|
9
|
+
end.to_options
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(parent = nil, route_specs = [], &block)
|
13
|
+
@parent = parent
|
14
|
+
@children = []
|
15
|
+
@filters = {}
|
16
|
+
@endpoint_defs = [] # Each is [subpath, *endpoint_args]
|
17
|
+
@controller_class = nil
|
18
|
+
@options = {}
|
19
|
+
|
20
|
+
# We'll store our array of route specs (strings or a single Regexp).
|
21
|
+
@route_specs = Array(route_specs).flatten
|
22
|
+
|
23
|
+
validate_path_specs!(@route_specs)
|
24
|
+
instance_exec(&block)
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_options
|
28
|
+
@options.merge(
|
29
|
+
{
|
30
|
+
routes: flatten_routes
|
31
|
+
}
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def workers(workers)
|
36
|
+
raise "Workers must be set at the root" unless @parent.nil?
|
37
|
+
|
38
|
+
@options[:workers] = [workers.to_i, 1].max
|
39
|
+
end
|
40
|
+
|
41
|
+
def threads(threads)
|
42
|
+
raise "Threads must be set at the root" unless @parent.nil?
|
43
|
+
|
44
|
+
@options[:threads] = [threads.to_i, 1].max
|
45
|
+
end
|
46
|
+
|
47
|
+
def rackup_file(rackup_file)
|
48
|
+
raise "Rackup file must be set at the root" unless @parent.nil?
|
49
|
+
raise "rackup_file already set" if @options[:rackup_file]
|
50
|
+
raise "Cannot provide a rackup_file if app is defined" if @options[:app]
|
51
|
+
|
52
|
+
if rackup_file.is_a?(File) && rackup_file.exist?
|
53
|
+
@options[:rackup_file] = file_path
|
54
|
+
else
|
55
|
+
file_path = rackup_file
|
56
|
+
@options[:rackup_file] = file_path if File.exist?(file_path)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def oob_gc_responses_threshold(threshold)
|
61
|
+
raise "OOB GC responses threshold must be set at the root" unless @parent.nil?
|
62
|
+
|
63
|
+
@options[:oob_gc_responses_threshold] = threshold.to_i
|
64
|
+
end
|
65
|
+
|
66
|
+
def log_level(level)
|
67
|
+
raise "Log level must be set at the root" unless @parent.nil?
|
68
|
+
|
69
|
+
ENV["ITSI_LOG"] = level.to_s
|
70
|
+
end
|
71
|
+
|
72
|
+
def log_format(format)
|
73
|
+
raise "Log format must be set at the root" unless @parent.nil?
|
74
|
+
|
75
|
+
case format.to_s
|
76
|
+
when "auto"
|
77
|
+
when "ansi" then ENV['ITSI_LOG_ANSI'] = "true"
|
78
|
+
when "json", "plain" then ENV['ITSI_LOG_PLAIN'] = "true"
|
79
|
+
else raise "Invalid log format '#{format}'"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def run(app)
|
84
|
+
raise "App must be set at the root" unless @parent.nil?
|
85
|
+
raise "App already set" if @options[:app]
|
86
|
+
raise "Cannot provide an app if rackup_file is defined" if @options[:rackup_file]
|
87
|
+
|
88
|
+
@options[:app] = app
|
89
|
+
end
|
90
|
+
|
91
|
+
def bind(bind_str)
|
92
|
+
raise "Bind must be set at the root" unless @parent.nil?
|
93
|
+
|
94
|
+
@options[:binds] ||= []
|
95
|
+
@options[:binds] << bind_str.to_s
|
96
|
+
end
|
97
|
+
|
98
|
+
def after_fork(&block)
|
99
|
+
raise "After fork must be set at the root" unless @parent.nil?
|
100
|
+
|
101
|
+
@options[:hooks] ||= {}
|
102
|
+
@options[:hooks][:after_fork] = block
|
103
|
+
end
|
104
|
+
|
105
|
+
def before_fork(&block)
|
106
|
+
raise "Before fork must be set at the root" unless @parent.nil?
|
107
|
+
|
108
|
+
@options[:hooks] ||= {}
|
109
|
+
@options[:hooks][:before_fork] = block
|
110
|
+
end
|
111
|
+
|
112
|
+
def after_memory_threshold_reached(&block)
|
113
|
+
raise "Before fork must be set at the root" unless @parent.nil?
|
114
|
+
|
115
|
+
@options[:hooks] ||= {}
|
116
|
+
@options[:hooks][:after_memory_threshold_reached] = block
|
117
|
+
end
|
118
|
+
|
119
|
+
def worker_memory_limit(memory_limit)
|
120
|
+
raise "Worker memory limit must be set at the root" unless @parent.nil?
|
121
|
+
|
122
|
+
@options[:worker_memory_limit] = memory_limit
|
123
|
+
end
|
124
|
+
|
125
|
+
def fiber_scheduler(klass_name)
|
126
|
+
raise "Fiber scheduler must be set at the root" unless @parent.nil?
|
127
|
+
|
128
|
+
@options[:scheduler_class] = klass_name if klass_name
|
129
|
+
end
|
130
|
+
|
131
|
+
def preload(preload)
|
132
|
+
raise "Preload must be set at the root" unless @parent.nil?
|
133
|
+
|
134
|
+
@options[:preload] = preload
|
135
|
+
end
|
136
|
+
|
137
|
+
def shutdown_timeout(shutdown_timeout)
|
138
|
+
raise "Shutdown timeout must be set at the root" unless @parent.nil?
|
139
|
+
|
140
|
+
@options[:shutdown_timeout] = shutdown_timeout.to_f
|
141
|
+
end
|
142
|
+
|
143
|
+
def script_name(script_name)
|
144
|
+
raise "Script name must be set at the root" unless @parent.nil?
|
145
|
+
|
146
|
+
@options[:script_name] = script_name.to_s
|
147
|
+
end
|
148
|
+
|
149
|
+
def stream_body(stream_body)
|
150
|
+
raise "Stream body must be set at the root" unless @parent.nil?
|
151
|
+
|
152
|
+
@options[:stream_body] = !!stream_body
|
153
|
+
end
|
154
|
+
|
155
|
+
def location(*route_specs, &block)
|
156
|
+
route_specs = route_specs.flatten
|
157
|
+
child = OptionsDSL.new(self, route_specs, &block)
|
158
|
+
@children << child
|
159
|
+
end
|
160
|
+
|
161
|
+
# define endpoints
|
162
|
+
def endpoint(subpath, *args)
|
163
|
+
raise "Endpoint must be set inside a location block" if @parent.is_nil?
|
164
|
+
|
165
|
+
@endpoint_defs << [subpath, *args]
|
166
|
+
end
|
167
|
+
|
168
|
+
def controller(klass)
|
169
|
+
raise "Endpoint must be set inside a location block" if @parent.is_nil?
|
170
|
+
|
171
|
+
@controller_class = klass
|
172
|
+
end
|
173
|
+
|
174
|
+
# define some filters
|
175
|
+
def basic_auth(**args)
|
176
|
+
raise "Endpoint must be set inside a location block" if @parent.is_nil?
|
177
|
+
|
178
|
+
@filters[:basic_auth] = args
|
179
|
+
end
|
180
|
+
|
181
|
+
# define some filters
|
182
|
+
def redirect(**args)
|
183
|
+
raise "Endpoint must be set inside a location block" if @parent.is_nil?
|
184
|
+
|
185
|
+
@filters[:redirect] = args
|
186
|
+
end
|
187
|
+
|
188
|
+
def jwt_auth(**args)
|
189
|
+
raise "Endpoint must be set inside a location block" if @parent.is_nil?
|
190
|
+
|
191
|
+
@filters[:jwt_auth] = args
|
192
|
+
end
|
193
|
+
|
194
|
+
def api_key_auth(**args)
|
195
|
+
raise "Endpoint must be set inside a location block" if @parent.is_nil?
|
196
|
+
|
197
|
+
@filters[:api_key_auth] = args
|
198
|
+
end
|
199
|
+
|
200
|
+
def compress(**args)
|
201
|
+
raise "Endpoint must be set inside a location block" if @parent.is_nil?
|
202
|
+
|
203
|
+
@filters[:compress] = args
|
204
|
+
end
|
205
|
+
|
206
|
+
def rate_limit(name, **args)
|
207
|
+
raise "Endpoint must be set inside a location block" if @parent.is_nil?
|
208
|
+
|
209
|
+
@filters[:rate_limit] = { name: name }.merge(args)
|
210
|
+
end
|
211
|
+
|
212
|
+
def cors(**args)
|
213
|
+
raise "Endpoint must be set inside a location block" if @parent.is_nil?
|
214
|
+
|
215
|
+
@filters[:cors] = args
|
216
|
+
end
|
217
|
+
|
218
|
+
def file_server(**args)
|
219
|
+
raise "Endpoint must be set inside a location block" if @parent.is_nil?
|
220
|
+
|
221
|
+
@filters[:file_server] = args
|
222
|
+
end
|
223
|
+
|
224
|
+
def flatten_routes
|
225
|
+
child_routes = @children.flat_map(&:flatten_routes)
|
226
|
+
base_expansions = combined_paths_from_parent
|
227
|
+
endpoint_routes = @endpoint_defs.map do |(endpoint_subpath, *endpoint_args)|
|
228
|
+
ep_expansions = expand_single_subpath(endpoint_subpath)
|
229
|
+
final_regex_str = or_pattern_for(cartesian_combine(base_expansions, ep_expansions))
|
230
|
+
|
231
|
+
{
|
232
|
+
route: Regexp.new("^#{final_regex_str}$"),
|
233
|
+
filters: effective_filters_with_endpoint(endpoint_args)
|
234
|
+
}
|
235
|
+
end
|
236
|
+
|
237
|
+
location_route = unless @route_specs.empty?
|
238
|
+
pattern_str = or_pattern_for(base_expansions) # the expansions themselves
|
239
|
+
{
|
240
|
+
route: Regexp.new("^#{pattern_str}$"),
|
241
|
+
filters: effective_filters
|
242
|
+
}
|
243
|
+
end
|
244
|
+
|
245
|
+
result = []
|
246
|
+
result.concat(child_routes)
|
247
|
+
result.concat(endpoint_routes)
|
248
|
+
result << location_route if location_route
|
249
|
+
result
|
250
|
+
end
|
251
|
+
|
252
|
+
def validate_path_specs!(specs)
|
253
|
+
regexes = specs.select { |s| s.is_a?(Regexp) }
|
254
|
+
return unless regexes.size > 1
|
255
|
+
|
256
|
+
raise ArgumentError, "Cannot have multiple raw Regex route specs in a single location."
|
257
|
+
end
|
258
|
+
|
259
|
+
# Called by flatten_routes to get expansions from the parent's expansions combined with mine
|
260
|
+
def combined_paths_from_parent
|
261
|
+
if parent
|
262
|
+
pex = parent.combined_paths_from_parent_for_children
|
263
|
+
cartesian_combine(pex, expansions_for(@route_specs))
|
264
|
+
else
|
265
|
+
expansions_for(@route_specs)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def combined_paths_from_parent_for_children
|
270
|
+
if parent
|
271
|
+
pex = parent.combined_paths_from_parent_for_children
|
272
|
+
cartesian_combine(pex, expansions_for(@route_specs))
|
273
|
+
else
|
274
|
+
expansions_for(@route_specs)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
def expand_single_subpath(subpath)
|
279
|
+
expansions_for([subpath]) # just treat it as a mini specs array
|
280
|
+
end
|
281
|
+
|
282
|
+
def expansions_for(specs)
|
283
|
+
return [] if specs.empty?
|
284
|
+
|
285
|
+
if specs.any? { |s| s.is_a? Regexp }
|
286
|
+
raise "Cannot combine a raw Regexp with other strings in the same location." if specs.size > 1
|
287
|
+
|
288
|
+
[[:raw_regex, specs.first]]
|
289
|
+
else
|
290
|
+
specs.map do |string_spec|
|
291
|
+
string_spec = string_spec.sub(%r{^/}, "")
|
292
|
+
string_spec
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
def cartesian_combine(parent_exps, child_exps)
|
298
|
+
return child_exps if parent_exps.empty?
|
299
|
+
return parent_exps if child_exps.empty?
|
300
|
+
|
301
|
+
if parent_exps.size == 1 && parent_exps.first.is_a?(Array) && parent_exps.first.first == :raw_regex
|
302
|
+
raise "Cannot nest under a raw Regexp route."
|
303
|
+
end
|
304
|
+
|
305
|
+
if child_exps.size == 1 && child_exps.first.is_a?(Array) && child_exps.first.first == :raw_regex
|
306
|
+
raise "Cannot nest a raw Regexp route under a parent string route."
|
307
|
+
end
|
308
|
+
|
309
|
+
results = []
|
310
|
+
parent_exps.each do |p|
|
311
|
+
child_exps.each do |c|
|
312
|
+
joined = [p, c].reject(&:empty?).join("/")
|
313
|
+
results << joined
|
314
|
+
end
|
315
|
+
end
|
316
|
+
results
|
317
|
+
end
|
318
|
+
|
319
|
+
def or_pattern_for(expansions)
|
320
|
+
return "" if expansions.empty?
|
321
|
+
|
322
|
+
if expansions.size == 1 && expansions.first.is_a?(Array) && expansions.first.first == :raw_regex
|
323
|
+
raw = expansions.first.last
|
324
|
+
return raw.source # Use the raw Regexp's source
|
325
|
+
end
|
326
|
+
|
327
|
+
pattern_pieces = expansions.map do |exp|
|
328
|
+
if exp.empty?
|
329
|
+
"" # => means top-level "/"
|
330
|
+
else
|
331
|
+
segment_to_regex_with_slash(exp)
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
joined = pattern_pieces.join("|")
|
336
|
+
|
337
|
+
"(?:#{joined})"
|
338
|
+
end
|
339
|
+
|
340
|
+
def segment_to_regex_with_slash(path_str)
|
341
|
+
return "" if path_str == ""
|
342
|
+
|
343
|
+
segments = path_str.split("/")
|
344
|
+
|
345
|
+
converted = segments.map do |seg|
|
346
|
+
# wildcard?
|
347
|
+
next ".*" if seg == "*"
|
348
|
+
|
349
|
+
# :param(...)?
|
350
|
+
if seg =~ /^:([A-Za-z_]\w*)(?:\(([^)]*)\))?$/
|
351
|
+
param_name = Regexp.last_match(1)
|
352
|
+
custom = Regexp.last_match(2)
|
353
|
+
if custom && !custom.empty?
|
354
|
+
"(?<#{param_name}>#{custom})"
|
355
|
+
else
|
356
|
+
"(?<#{param_name}>[^/]+)"
|
357
|
+
end
|
358
|
+
else
|
359
|
+
Regexp.escape(seg)
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
converted.join("/")
|
364
|
+
end
|
365
|
+
|
366
|
+
def effective_filters
|
367
|
+
# gather from root -> self, overriding duplicates
|
368
|
+
merged = merge_ancestor_filters
|
369
|
+
# turn into array
|
370
|
+
merged.map { |k, v| { type: k, params: v } }
|
371
|
+
end
|
372
|
+
|
373
|
+
def effective_filters_with_endpoint(endpoint_args)
|
374
|
+
arr = effective_filters
|
375
|
+
# endpoint filter last
|
376
|
+
ep_filter_params = endpoint_args.dup
|
377
|
+
ep_filter_params << @controller_class if @controller_class
|
378
|
+
arr << { type: :endpoint, params: ep_filter_params }
|
379
|
+
arr
|
380
|
+
end
|
381
|
+
|
382
|
+
def merge_ancestor_filters
|
383
|
+
chain = []
|
384
|
+
node = self
|
385
|
+
while node
|
386
|
+
chain << node
|
387
|
+
node = node.parent
|
388
|
+
end
|
389
|
+
chain.reverse!
|
390
|
+
|
391
|
+
merged = {}
|
392
|
+
chain.each do |n|
|
393
|
+
n.filters.each do |k, v|
|
394
|
+
merged[k] = v
|
395
|
+
end
|
396
|
+
end
|
397
|
+
merged
|
398
|
+
end
|
399
|
+
end
|
400
|
+
end
|
401
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
return unless defined?(::Rackup::Handler) || defined?(Rack::Handler)
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module Handler
|
5
|
+
module Itsi
|
6
|
+
def self.run(app, options = {})
|
7
|
+
::Itsi::Server.start(
|
8
|
+
**Itsi::Server::Config.load(
|
9
|
+
{
|
10
|
+
app: app,
|
11
|
+
binds: [
|
12
|
+
"http://#{
|
13
|
+
options.fetch(
|
14
|
+
:host,
|
15
|
+
"127.0.0.1"
|
16
|
+
)}:#{
|
17
|
+
options.fetch(
|
18
|
+
:Port,
|
19
|
+
3001
|
20
|
+
)}"
|
21
|
+
]
|
22
|
+
}
|
23
|
+
)
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
if defined?(Rackup)
|
31
|
+
::Rackup::Handler.register("itsi", Rack::Handler::Itsi)
|
32
|
+
::Rackup::Handler.register("Itsi", Rack::Handler::Itsi)
|
33
|
+
elsif defined?(Rack)
|
34
|
+
::Rack::Handler.register("itsi", Rack::Handler::Itsi)
|
35
|
+
::Rack::Handler.register("Itsi", Rack::Handler::Itsi)
|
36
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Itsi
|
2
|
+
class Server
|
3
|
+
module RackInterface
|
4
|
+
# Interface to Rack applications.
|
5
|
+
# Here we build the env, and invoke the Rack app's call method.
|
6
|
+
# We then turn the Rack response into something Itsi server understands.
|
7
|
+
def call(app, request)
|
8
|
+
respond request, app.call(request.to_rack_env)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Itsi responses are asynchronous and can be streamed.
|
12
|
+
# Response chunks are sent using response.send_frame
|
13
|
+
# and the response is finished using response.close_write.
|
14
|
+
# If only a single chunk is written, you can use the #send_and_close method.
|
15
|
+
def respond(request, (status, headers, body))
|
16
|
+
response = request.response
|
17
|
+
|
18
|
+
# Don't try and respond if we've been hijacked.
|
19
|
+
# The hijacker is now responsible for this.
|
20
|
+
return if request.hijacked
|
21
|
+
|
22
|
+
# 1. Set Status
|
23
|
+
response.status = status
|
24
|
+
|
25
|
+
# 2. Set Headers
|
26
|
+
body_streamer = streaming_body?(body) ? body : headers.delete("rack.hijack")
|
27
|
+
headers.each do |key, value|
|
28
|
+
next response.add_header(key, value) unless value.is_a?(Array)
|
29
|
+
|
30
|
+
value.each do |v|
|
31
|
+
response.add_header(key, v)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# 3. Set Body
|
36
|
+
# As soon as we start setting the response
|
37
|
+
# the server will begin to stream it to the client.
|
38
|
+
|
39
|
+
# If we're partially hijacked or returned a streaming body,
|
40
|
+
# stream this response.
|
41
|
+
|
42
|
+
if body_streamer
|
43
|
+
body_streamer.call(StreamIO.new(response))
|
44
|
+
|
45
|
+
# If we're enumerable with more than one chunk
|
46
|
+
# also stream, otherwise write in a single chunk
|
47
|
+
elsif body.respond_to?(:each) || body.respond_to?(:to_ary)
|
48
|
+
unless body.respond_to?(:each)
|
49
|
+
body = body.to_ary
|
50
|
+
raise "Body #to_ary didn't return an array" unless body.is_a?(Array)
|
51
|
+
end
|
52
|
+
# We offset this iteration intentionally,
|
53
|
+
# to optimize for the case where there's only one chunk.
|
54
|
+
buffer = nil
|
55
|
+
body.each do |part|
|
56
|
+
response.send_frame(buffer.to_s) if buffer
|
57
|
+
buffer = part
|
58
|
+
end
|
59
|
+
|
60
|
+
response.send_and_close(buffer.to_s)
|
61
|
+
else
|
62
|
+
response.send_and_close(body.to_s)
|
63
|
+
end
|
64
|
+
ensure
|
65
|
+
response.close_write
|
66
|
+
body.close if body.respond_to?(:close)
|
67
|
+
end
|
68
|
+
|
69
|
+
# A streaming body is one that responds to #call and not #each.
|
70
|
+
def streaming_body?(body)
|
71
|
+
body.respond_to?(:call) && !body.respond_to?(:each)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Itsi
|
2
|
+
class Server
|
3
|
+
module SchedulerInterface
|
4
|
+
# Simple wrapper to instantiate a scheduler, start it,
|
5
|
+
# and immediate have it invoke a scheduler proc
|
6
|
+
def start_scheduler_loop(scheduler_class, scheduler_task)
|
7
|
+
scheduler = scheduler_class.new
|
8
|
+
Fiber.set_scheduler(scheduler)
|
9
|
+
[scheduler, Fiber.schedule(&scheduler_task)]
|
10
|
+
end
|
11
|
+
|
12
|
+
# When running in scheduler mode,
|
13
|
+
# each request is wrapped in a Fiber.
|
14
|
+
def schedule(app, request)
|
15
|
+
Fiber.schedule do
|
16
|
+
call(app, request)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,6 @@
|
|
1
|
+
if defined?(ActiveSupport::IsolatedExecutionState) && !ENV["ITSI_DISABLE_AS_AUTO_FIBER_ISOLATION_LEVEL"]
|
2
|
+
Itsi.log_info \
|
3
|
+
"ActiveSupport Isolated Execution state detected. Automatically switching to :fiber mode. "\
|
4
|
+
"Set ITSI_DISABLE_AS_AUTO_FIBER_ISOLATION_LEVEL to disable this behavior"
|
5
|
+
ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
|
6
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Itsi
|
2
|
+
module SignalTrap
|
3
|
+
|
4
|
+
DEFAULT_SIGNALS = ["DEFAULT", "", nil].freeze
|
5
|
+
INTERCEPTED_SIGNALS = ["INT"].freeze
|
6
|
+
|
7
|
+
def trap(signal, *args, &block)
|
8
|
+
unless INTERCEPTED_SIGNALS.include?(signal.to_s) && block.nil? && Itsi::Server.running?
|
9
|
+
return super(signal, *args, &block)
|
10
|
+
end
|
11
|
+
Itsi::Server.reset_signal_handlers
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
[Kernel, Signal].each do |receiver|
|
18
|
+
receiver.singleton_class.prepend(Itsi::SignalTrap)
|
19
|
+
end
|
20
|
+
|
21
|
+
[Object].each do |receiver|
|
22
|
+
receiver.include(Itsi::SignalTrap)
|
23
|
+
end
|