raptor 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.buildkite/pipeline.yml +90 -32
- data/.dockerignore +5 -0
- data/CHANGELOG.md +30 -0
- data/Dockerfile +28 -0
- data/README.md +159 -27
- data/Rakefile +19 -1
- data/docs/raptor-vs-puma.md +557 -0
- data/ext/raptor_bpf/reuseport_select.bpf.c +63 -0
- data/ext/raptor_http/extconf.rb +1 -1
- data/ext/raptor_native/extconf.rb +7 -0
- data/ext/raptor_native/raptor_native.c +99 -0
- data/lib/rackup/handler/raptor.rb +12 -2
- data/lib/raptor/binder.rb +139 -31
- data/lib/raptor/cli.rb +83 -22
- data/lib/raptor/cluster.rb +214 -33
- data/lib/raptor/http.rb +116 -0
- data/lib/raptor/{request.rb → http1.rb} +228 -99
- data/lib/raptor/http2.rb +85 -40
- data/lib/raptor/reactor.rb +22 -15
- data/lib/raptor/reuseport_bpf.rb +108 -0
- data/lib/raptor/server.rb +92 -43
- data/lib/raptor/stats.rb +1 -1
- data/lib/raptor/systemd.rb +69 -0
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/binder.rbs +82 -5
- data/sig/generated/raptor/cli.rbs +2 -3
- data/sig/generated/raptor/cluster.rbs +91 -13
- data/sig/generated/raptor/http.rbs +65 -0
- data/sig/generated/raptor/{request.rbs → http1.rbs} +122 -35
- data/sig/generated/raptor/http2.rbs +46 -13
- data/sig/generated/raptor/reactor.rbs +18 -11
- data/sig/generated/raptor/reuseport_bpf.rbs +56 -0
- data/sig/generated/raptor/server.rbs +49 -21
- data/sig/generated/raptor/systemd.rbs +42 -0
- metadata +30 -3
data/lib/raptor/cluster.rb
CHANGED
|
@@ -9,11 +9,13 @@ require "ractor-pool"
|
|
|
9
9
|
|
|
10
10
|
require_relative "log"
|
|
11
11
|
require_relative "binder"
|
|
12
|
-
require_relative "server"
|
|
13
12
|
require_relative "reactor"
|
|
14
|
-
require_relative "
|
|
13
|
+
require_relative "server"
|
|
14
|
+
require_relative "reuseport_bpf"
|
|
15
|
+
require_relative "http1"
|
|
15
16
|
require_relative "http2"
|
|
16
17
|
require_relative "stats"
|
|
18
|
+
require_relative "systemd"
|
|
17
19
|
|
|
18
20
|
module Raptor
|
|
19
21
|
# Multi-process web server cluster with advanced concurrency architecture.
|
|
@@ -41,11 +43,13 @@ module Raptor
|
|
|
41
43
|
# workers: 4, ractors: 2, threads: 8,
|
|
42
44
|
# binds: ["tcp://0.0.0.0:3000"],
|
|
43
45
|
# rackup: "config.ru",
|
|
44
|
-
#
|
|
46
|
+
# connection: { first_data_timeout: 30, chunk_data_timeout: 10 }
|
|
45
47
|
# }
|
|
46
48
|
# Cluster.run(options)
|
|
47
49
|
#
|
|
48
50
|
class Cluster
|
|
51
|
+
INHERITED_FDS_ENV = "RAPTOR_INHERITED_FDS"
|
|
52
|
+
|
|
49
53
|
# Convenience method to create and run a cluster with the given options.
|
|
50
54
|
#
|
|
51
55
|
# @param options [Hash] cluster configuration options
|
|
@@ -56,15 +60,26 @@ module Raptor
|
|
|
56
60
|
new(options).run
|
|
57
61
|
end
|
|
58
62
|
|
|
63
|
+
# @rbs @drain_accept_queue: bool
|
|
59
64
|
# @rbs @worker_count: Integer
|
|
60
65
|
# @rbs @ractor_count: Integer
|
|
61
66
|
# @rbs @thread_count: Integer
|
|
62
|
-
# @rbs @
|
|
63
|
-
# @rbs @
|
|
67
|
+
# @rbs @environment: String
|
|
68
|
+
# @rbs @connection_options: Hash[Symbol, untyped]
|
|
69
|
+
# @rbs @http1_options: Hash[Symbol, untyped]
|
|
70
|
+
# @rbs @http2_options: Hash[Symbol, untyped]
|
|
64
71
|
# @rbs @worker_boot_timeout: Integer
|
|
72
|
+
# @rbs @worker_timeout: Integer
|
|
73
|
+
# @rbs @worker_drain_timeout: Integer
|
|
65
74
|
# @rbs @worker_shutdown_timeout: Integer
|
|
66
75
|
# @rbs @stats_file: String?
|
|
67
76
|
# @rbs @pid_file: String?
|
|
77
|
+
# @rbs @stdout_file: String?
|
|
78
|
+
# @rbs @stderr_file: String?
|
|
79
|
+
# @rbs @access_log_file: String?
|
|
80
|
+
# @rbs @access_log_io: IO?
|
|
81
|
+
# @rbs @launch_command: String?
|
|
82
|
+
# @rbs @launch_argv: Array[String]?
|
|
68
83
|
# @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
69
84
|
# @rbs @binder: Binder
|
|
70
85
|
# @rbs @server_port: Integer
|
|
@@ -76,6 +91,8 @@ module Raptor
|
|
|
76
91
|
# @rbs @phase: Integer
|
|
77
92
|
# @rbs @phased_restart_requested: bool
|
|
78
93
|
# @rbs @phased_restarting: bool
|
|
94
|
+
# @rbs @hot_restart_requested: bool
|
|
95
|
+
# @rbs @bpf_active: bool
|
|
79
96
|
|
|
80
97
|
# Creates a new Cluster with the specified configuration.
|
|
81
98
|
#
|
|
@@ -85,34 +102,67 @@ module Raptor
|
|
|
85
102
|
#
|
|
86
103
|
# @param options [Hash] cluster configuration options
|
|
87
104
|
# @option options [Array<String>] :binds array of bind URIs
|
|
105
|
+
# @option options [Integer] :socket_backlog kernel listen() queue depth for TCP/SSL listeners
|
|
106
|
+
# @option options [Boolean] :drain_accept_queue whether to drain the kernel accept queue on shutdown
|
|
88
107
|
# @option options [Integer] :workers number of worker processes
|
|
89
108
|
# @option options [Integer] :ractors number of ractors per worker process
|
|
90
109
|
# @option options [Integer] :threads number of threads per worker process
|
|
91
110
|
# @option options [#call] :app pre-built Rack application
|
|
92
111
|
# @option options [String] :rackup path to Rack configuration file
|
|
93
|
-
# @option options [
|
|
94
|
-
# @option options [
|
|
112
|
+
# @option options [String, nil] :chdir directory to change to before loading the Rack application, or nil to leave the working directory unchanged
|
|
113
|
+
# @option options [String, nil] :environment Raptor's application environment label; falls back to `$RAILS_ENV`, then `$RACK_ENV`, then `"development"`
|
|
114
|
+
# @option options [Hash] :connection per-connection settings shared across protocols
|
|
115
|
+
# @option options [Hash] :http1 HTTP/1.1-specific settings
|
|
116
|
+
# @option options [Hash] :http2 HTTP/2-specific settings
|
|
95
117
|
# @option options [Integer] :worker_boot_timeout seconds to wait for a worker to finish booting before killing it
|
|
118
|
+
# @option options [Integer] :worker_timeout seconds to wait for a booted worker to check in before killing it
|
|
119
|
+
# @option options [Integer] :worker_drain_timeout seconds a worker waits for in-flight requests during shutdown before force-killing app threads
|
|
96
120
|
# @option options [Integer] :worker_shutdown_timeout seconds to wait for graceful worker exit before force-killing
|
|
97
121
|
# @option options [String, nil] :stats_file path to write per-worker stats JSON, or nil to disable
|
|
98
122
|
# @option options [String, nil] :pid_file path to write the master PID to, or nil to disable
|
|
99
|
-
# @option options [
|
|
123
|
+
# @option options [String, nil] :stdout_file path to redirect stdout to, reopened on SIGHUP, or nil to disable
|
|
124
|
+
# @option options [String, nil] :stderr_file path to redirect stderr to, reopened on SIGHUP, or nil to disable
|
|
125
|
+
# @option options [String, nil] :access_log_file path to write Common Log Format access logs to, reopened on SIGHUP, or nil to disable
|
|
126
|
+
# @option options [String, nil] :launch_command path of the program to re-exec on hot restart, or nil to disable
|
|
127
|
+
# @option options [Array<String>, nil] :launch_argv command-line arguments for the hot-restart exec, or nil to disable
|
|
128
|
+
# @option options [#call, nil] :on_error callback invoked with (env, exception) when the Rack app raises
|
|
100
129
|
# @return [void]
|
|
101
130
|
#
|
|
102
131
|
# @rbs (Hash[Symbol, untyped] options) -> void
|
|
103
132
|
def initialize(options)
|
|
133
|
+
@drain_accept_queue = options[:drain_accept_queue]
|
|
104
134
|
@worker_count = options[:workers]
|
|
105
135
|
@ractor_count = options[:ractors]
|
|
106
136
|
@thread_count = options[:threads]
|
|
107
|
-
@
|
|
108
|
-
@
|
|
137
|
+
@environment = options[:environment] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
|
|
138
|
+
@connection_options = options[:connection]
|
|
139
|
+
@http1_options = options[:http1]
|
|
140
|
+
@http2_options = options[:http2]
|
|
109
141
|
@worker_boot_timeout = options[:worker_boot_timeout]
|
|
142
|
+
@worker_timeout = options[:worker_timeout]
|
|
143
|
+
@worker_drain_timeout = options[:worker_drain_timeout]
|
|
110
144
|
@worker_shutdown_timeout = options[:worker_shutdown_timeout]
|
|
111
145
|
@stats_file = options[:stats_file]
|
|
112
146
|
@pid_file = options[:pid_file]
|
|
147
|
+
@stdout_file = options[:stdout_file]
|
|
148
|
+
@stderr_file = options[:stderr_file]
|
|
149
|
+
@access_log_file = options[:access_log_file]
|
|
150
|
+
@access_log_io = nil
|
|
151
|
+
@launch_command = options[:launch_command]
|
|
152
|
+
@launch_argv = options[:launch_argv]
|
|
113
153
|
@on_error = options[:on_error]
|
|
114
154
|
|
|
115
|
-
|
|
155
|
+
Dir.chdir(options[:chdir]) if options[:chdir]
|
|
156
|
+
|
|
157
|
+
inherited_fds = if raw = ENV.delete(INHERITED_FDS_ENV)
|
|
158
|
+
JSON.parse(raw)
|
|
159
|
+
elsif (systemd_fds = Systemd.listen_fds).any?
|
|
160
|
+
Systemd.clear_listen_env
|
|
161
|
+
pair_systemd_fds(options[:binds], systemd_fds)
|
|
162
|
+
else
|
|
163
|
+
{}
|
|
164
|
+
end
|
|
165
|
+
@binder = Binder.new(options[:binds], socket_backlog: options[:socket_backlog], inherited_fds: inherited_fds)
|
|
116
166
|
@server_port = @binder.server_port
|
|
117
167
|
@app = options[:app] || Rack::Builder.parse_file(options[:rackup])
|
|
118
168
|
log_initialization
|
|
@@ -124,14 +174,15 @@ module Raptor
|
|
|
124
174
|
@phase = 0
|
|
125
175
|
@phased_restart_requested = false
|
|
126
176
|
@phased_restarting = false
|
|
177
|
+
@hot_restart_requested = false
|
|
127
178
|
end
|
|
128
179
|
|
|
129
180
|
# Starts the multi-process cluster and manages worker processes.
|
|
130
181
|
#
|
|
131
182
|
# Forks the configured number of worker processes and monitors them,
|
|
132
183
|
# restarting any that exit unexpectedly or stop checking in. Handles
|
|
133
|
-
# graceful shutdown via INT or TERM signals,
|
|
134
|
-
# and
|
|
184
|
+
# graceful shutdown via INT or TERM signals, phased restart via USR1,
|
|
185
|
+
# and hot restart via USR2.
|
|
135
186
|
#
|
|
136
187
|
# Each worker process includes:
|
|
137
188
|
# - 1 server thread (continuously accepts connections with backpressure control)
|
|
@@ -145,13 +196,18 @@ module Raptor
|
|
|
145
196
|
#
|
|
146
197
|
# @rbs () -> void
|
|
147
198
|
def run
|
|
199
|
+
reopen_logs
|
|
200
|
+
|
|
148
201
|
trap("INT") { shutdown }
|
|
149
202
|
trap("TERM") { shutdown }
|
|
150
|
-
trap("
|
|
151
|
-
trap("
|
|
203
|
+
trap("HUP") { reopen_logs_and_signal_workers }
|
|
204
|
+
trap("USR1") { @phased_restart_requested = true }
|
|
205
|
+
trap("USR2") { @hot_restart_requested = true }
|
|
152
206
|
|
|
153
207
|
File.open(@pid_file, File::CREAT | File::EXCL | File::WRONLY) { |file| file.write(Process.pid.to_s) } if @pid_file
|
|
154
208
|
|
|
209
|
+
@bpf_active = ReuseportBPF.setup(@worker_count)
|
|
210
|
+
|
|
155
211
|
@worker_count.times { |index| spawn_worker(index) }
|
|
156
212
|
|
|
157
213
|
stats_file_thread = if @stats_file
|
|
@@ -162,20 +218,25 @@ module Raptor
|
|
|
162
218
|
end
|
|
163
219
|
end
|
|
164
220
|
|
|
221
|
+
Systemd.notify("READY=1\nMAINPID=#{Process.pid}")
|
|
222
|
+
|
|
165
223
|
until @shutdown
|
|
166
224
|
break if reap_workers == :no_children
|
|
167
225
|
|
|
226
|
+
perform_hot_restart if @hot_restart_requested
|
|
168
227
|
perform_phased_restart if @phased_restart_requested && !@phased_restarting
|
|
169
228
|
timeout_hung_workers
|
|
170
229
|
|
|
171
230
|
sleep 0.1
|
|
172
231
|
end
|
|
173
232
|
|
|
233
|
+
Systemd.notify("STOPPING=1")
|
|
174
234
|
stop_workers
|
|
175
235
|
stats_file_thread&.join
|
|
176
236
|
File.delete(@stats_file) rescue nil if @stats_file
|
|
177
237
|
File.delete(@pid_file) rescue nil if @pid_file
|
|
178
238
|
@stats.unmap
|
|
239
|
+
@binder.close
|
|
179
240
|
end
|
|
180
241
|
|
|
181
242
|
# Returns stats for all worker processes.
|
|
@@ -190,6 +251,25 @@ module Raptor
|
|
|
190
251
|
|
|
191
252
|
private
|
|
192
253
|
|
|
254
|
+
# Returns the inherited-FDs hash for a systemd socket-activation handoff,
|
|
255
|
+
# pairing each bind URI with the FD systemd passed at the same index.
|
|
256
|
+
# The activation is skipped (with a warning) when the FD count doesn't
|
|
257
|
+
# match the number of bind URIs.
|
|
258
|
+
#
|
|
259
|
+
# @param bind_uris [Array<String>] the configured bind URIs
|
|
260
|
+
# @param filenos [Array<Integer>] file descriptors passed by systemd
|
|
261
|
+
# @return [Hash{String => Array<Integer>}]
|
|
262
|
+
#
|
|
263
|
+
# @rbs (Array[String] bind_uris, Array[Integer] filenos) -> Hash[String, Array[Integer]]
|
|
264
|
+
def pair_systemd_fds(bind_uris, filenos)
|
|
265
|
+
if bind_uris.length != filenos.length
|
|
266
|
+
Log.warn "Ignoring socket activation: #{filenos.length} fd(s) from systemd, #{bind_uris.length} bind(s) configured"
|
|
267
|
+
return {}
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
bind_uris.zip(filenos).to_h { |bind_uri, fileno| [bind_uri, [fileno]] }
|
|
271
|
+
end
|
|
272
|
+
|
|
193
273
|
# Forks a new worker process and registers it at the given index.
|
|
194
274
|
# The worker inherits the cluster's current phase.
|
|
195
275
|
#
|
|
@@ -278,7 +358,7 @@ module Raptor
|
|
|
278
358
|
end
|
|
279
359
|
|
|
280
360
|
# Replaces each worker process one at a time, waiting for the new
|
|
281
|
-
# worker to boot before moving on to the next.
|
|
361
|
+
# worker to boot before moving on to the next.
|
|
282
362
|
#
|
|
283
363
|
# @return [void]
|
|
284
364
|
#
|
|
@@ -315,6 +395,36 @@ module Raptor
|
|
|
315
395
|
end
|
|
316
396
|
end
|
|
317
397
|
|
|
398
|
+
# Re-execs the master process with a fresh boot of the same Raptor
|
|
399
|
+
# invocation, handing the new master its listening sockets so accepted
|
|
400
|
+
# connections continue to be served across the swap.
|
|
401
|
+
#
|
|
402
|
+
# @return [void]
|
|
403
|
+
#
|
|
404
|
+
# @rbs () -> void
|
|
405
|
+
def perform_hot_restart
|
|
406
|
+
@hot_restart_requested = false
|
|
407
|
+
|
|
408
|
+
unless @launch_command && @launch_argv
|
|
409
|
+
Log.warn "Hot restart unavailable: launch command not captured"
|
|
410
|
+
return
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
Log.info "Hot restart starting"
|
|
414
|
+
monotonic_usec = (Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1_000_000).to_i
|
|
415
|
+
Systemd.notify("RELOADING=1\nMONOTONIC_USEC=#{monotonic_usec}")
|
|
416
|
+
@shutdown = true
|
|
417
|
+
stop_workers
|
|
418
|
+
@binder.clear_close_on_exec
|
|
419
|
+
ENV[INHERITED_FDS_ENV] = JSON.generate(@binder.inheritable_fds)
|
|
420
|
+
File.delete(@stats_file) rescue nil if @stats_file
|
|
421
|
+
File.delete(@pid_file) rescue nil if @pid_file
|
|
422
|
+
@stats.unmap
|
|
423
|
+
$stdout.flush
|
|
424
|
+
$stderr.flush
|
|
425
|
+
exec(@launch_command, *@launch_argv)
|
|
426
|
+
end
|
|
427
|
+
|
|
318
428
|
# Runs the full server stack inside a worker process.
|
|
319
429
|
#
|
|
320
430
|
# Sets up and coordinates the reactor, server, ractor pool, thread pool,
|
|
@@ -330,6 +440,11 @@ module Raptor
|
|
|
330
440
|
shutdown_requested = false
|
|
331
441
|
trap("INT") { shutdown_requested = true }
|
|
332
442
|
trap("TERM") { shutdown_requested = true }
|
|
443
|
+
trap("HUP") { reopen_logs }
|
|
444
|
+
trap("USR1", "IGNORE")
|
|
445
|
+
trap("USR2", "IGNORE")
|
|
446
|
+
|
|
447
|
+
Raptor::CPU.pin(index) if Raptor::CPU.count >= @worker_count
|
|
333
448
|
|
|
334
449
|
started_at = Process.clock_gettime(Process::CLOCK_REALTIME)
|
|
335
450
|
request_count = 0
|
|
@@ -354,27 +469,64 @@ module Raptor
|
|
|
354
469
|
@app.call(env)
|
|
355
470
|
}
|
|
356
471
|
thread_pool = AtomicThreadPool.new(size: @thread_count)
|
|
357
|
-
|
|
358
|
-
|
|
472
|
+
http1 = Http1.new(
|
|
473
|
+
counting_app,
|
|
474
|
+
@server_port,
|
|
475
|
+
connection_options: @connection_options,
|
|
476
|
+
http1_options: @http1_options,
|
|
477
|
+
access_log_io: @access_log_io,
|
|
478
|
+
on_error: @on_error
|
|
479
|
+
)
|
|
480
|
+
http2 = Http2.new(
|
|
481
|
+
counting_app,
|
|
482
|
+
@server_port,
|
|
483
|
+
connection_options: @connection_options,
|
|
484
|
+
http2_options: @http2_options,
|
|
485
|
+
access_log_io: @access_log_io,
|
|
486
|
+
on_error: @on_error
|
|
487
|
+
)
|
|
359
488
|
ractor_pool = RactorPool.new(
|
|
360
489
|
size: @ractor_count,
|
|
361
|
-
worker:
|
|
490
|
+
worker: http1.http_parser_worker
|
|
362
491
|
) do |parsed_result|
|
|
363
492
|
begin
|
|
364
493
|
if parsed_result[:protocol] == :http2
|
|
365
494
|
http2.handle_parsed_request(parsed_result, reactor, thread_pool)
|
|
366
495
|
else
|
|
367
|
-
|
|
496
|
+
http1.handle_parsed_request(parsed_result, reactor, thread_pool)
|
|
368
497
|
end
|
|
369
498
|
rescue => error
|
|
370
499
|
Log.rescued_error(error)
|
|
371
500
|
end
|
|
372
501
|
end
|
|
373
502
|
|
|
374
|
-
reactor = Reactor.new(
|
|
503
|
+
reactor = Reactor.new(
|
|
504
|
+
ractor_pool,
|
|
505
|
+
thread_pool,
|
|
506
|
+
connection_options: @connection_options,
|
|
507
|
+
http1_options: @http1_options
|
|
508
|
+
)
|
|
375
509
|
reactor_thread = reactor.run
|
|
376
510
|
|
|
377
|
-
|
|
511
|
+
worker_listeners = if @bpf_active
|
|
512
|
+
bpf_listeners = ReuseportBPF.create_worker_listeners(@binder.bind_uris, index, @binder.socket_backlog)
|
|
513
|
+
non_tcp_listeners = @binder.listeners.reject { |listener| listener.is_a?(TCPServer) }
|
|
514
|
+
bpf_listeners + non_tcp_listeners
|
|
515
|
+
else
|
|
516
|
+
@binder.listeners
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
server = Server.new(
|
|
520
|
+
@binder,
|
|
521
|
+
reactor,
|
|
522
|
+
thread_pool,
|
|
523
|
+
http1,
|
|
524
|
+
http2,
|
|
525
|
+
connection_options: @connection_options,
|
|
526
|
+
drain_accept_queue: @drain_accept_queue,
|
|
527
|
+
listeners: worker_listeners,
|
|
528
|
+
worker_index: (index if @bpf_active)
|
|
529
|
+
)
|
|
378
530
|
server_thread = server.run
|
|
379
531
|
|
|
380
532
|
Log.info "Worker #{index} booted"
|
|
@@ -412,11 +564,28 @@ module Raptor
|
|
|
412
564
|
reactor.shutdown
|
|
413
565
|
reactor_thread.join
|
|
414
566
|
ractor_pool.shutdown
|
|
415
|
-
|
|
416
|
-
thread_pool
|
|
567
|
+
http1.shutdown
|
|
568
|
+
drain_thread_pool(thread_pool)
|
|
417
569
|
stats_thread.join
|
|
418
570
|
end
|
|
419
571
|
|
|
572
|
+
# Shuts down the worker's application thread pool, force-killing the
|
|
573
|
+
# underlying threads if in-flight requests have not finished within
|
|
574
|
+
# `worker_drain_timeout` seconds.
|
|
575
|
+
#
|
|
576
|
+
# @param thread_pool [AtomicThreadPool] the worker's thread pool
|
|
577
|
+
# @return [void]
|
|
578
|
+
#
|
|
579
|
+
# @rbs (AtomicThreadPool thread_pool) -> void
|
|
580
|
+
def drain_thread_pool(thread_pool)
|
|
581
|
+
drain = Thread.new { thread_pool.shutdown }
|
|
582
|
+
return if drain.join(@worker_drain_timeout)
|
|
583
|
+
|
|
584
|
+
Log.warn "Force-killing in-flight app threads after #{@worker_drain_timeout}s drain timeout"
|
|
585
|
+
thread_pool.instance_variable_get(:@threads).each(&:kill)
|
|
586
|
+
drain.join
|
|
587
|
+
end
|
|
588
|
+
|
|
420
589
|
# Returns a human-readable description of how a process exited.
|
|
421
590
|
#
|
|
422
591
|
# @param status [Process::Status] the exit status of the process
|
|
@@ -454,6 +623,7 @@ module Raptor
|
|
|
454
623
|
Log.info "Cluster initializing:"
|
|
455
624
|
Log.info "├─ Version: #{VERSION}"
|
|
456
625
|
Log.info "├─ Ruby Version: #{RUBY_DESCRIPTION}"
|
|
626
|
+
Log.info "├─ Environment: #{@environment}"
|
|
457
627
|
Log.info "├─ Master PID: #{Process.pid}"
|
|
458
628
|
Log.info "│ └─ #{@worker_count} worker process#{"es" if @worker_count > 1}"
|
|
459
629
|
Log.info "│ ├─ 1 server thread"
|
|
@@ -465,20 +635,31 @@ module Raptor
|
|
|
465
635
|
Log.info "└─ Listening on #{@binder.addresses.join(", ")}"
|
|
466
636
|
end
|
|
467
637
|
|
|
468
|
-
#
|
|
638
|
+
# Redirects `$stdout`, `$stderr`, and the access log to their configured
|
|
639
|
+
# paths. No-op for any stream whose target path is nil.
|
|
469
640
|
#
|
|
470
|
-
#
|
|
641
|
+
# @return [void]
|
|
642
|
+
#
|
|
643
|
+
# @rbs () -> void
|
|
644
|
+
def reopen_logs
|
|
645
|
+
$stdout.reopen(@stdout_file, "a").sync = true if @stdout_file
|
|
646
|
+
$stderr.reopen(@stderr_file, "a").sync = true if @stderr_file
|
|
647
|
+
return unless @access_log_file
|
|
648
|
+
|
|
649
|
+
@access_log_io ||= File.open(@access_log_file, "a")
|
|
650
|
+
@access_log_io.reopen(@access_log_file, "a")
|
|
651
|
+
@access_log_io.sync = true
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
# Reopens the master's log files and forwards SIGHUP to each worker so
|
|
655
|
+
# they reopen their own inherited file descriptors.
|
|
471
656
|
#
|
|
472
657
|
# @return [void]
|
|
473
658
|
#
|
|
474
659
|
# @rbs () -> void
|
|
475
|
-
def
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
Log.info "Worker #{stat[:index]} (phase #{stat[:phase]}): pid=#{stat[:pid]}, requests=#{stat[:requests]}, " \
|
|
479
|
-
"busy=#{stat[:busy_threads]}/#{stat[:thread_capacity]}, backlog=#{stat[:backlog]}, " \
|
|
480
|
-
"#{status}, last_checkin=#{Time.at(stat[:last_checkin]).strftime("%H:%M:%S")}"
|
|
481
|
-
end
|
|
660
|
+
def reopen_logs_and_signal_workers
|
|
661
|
+
reopen_logs
|
|
662
|
+
@workers.values.each { |pid| Process.kill("HUP", pid) rescue nil }
|
|
482
663
|
end
|
|
483
664
|
|
|
484
665
|
# Writes the stats file on a 1-second interval until shutdown.
|
data/lib/raptor/http.rb
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "rack"
|
|
5
|
+
|
|
6
|
+
require_relative "version"
|
|
7
|
+
require_relative "raptor_native"
|
|
8
|
+
|
|
9
|
+
module Raptor
|
|
10
|
+
# Shared HTTP utilities used by both the HTTP/1.x and HTTP/2 handlers:
|
|
11
|
+
# Rack env keys that aren't provided by Rack itself, low-level socket
|
|
12
|
+
# writing, and Common Log Format access-log formatting.
|
|
13
|
+
#
|
|
14
|
+
module Http
|
|
15
|
+
WRITE_TIMEOUT = 5
|
|
16
|
+
|
|
17
|
+
CONTENT_LENGTH = "CONTENT_LENGTH"
|
|
18
|
+
CONTENT_TYPE = "CONTENT_TYPE"
|
|
19
|
+
HTTP_VERSION = "HTTP_VERSION"
|
|
20
|
+
REMOTE_ADDR = "REMOTE_ADDR"
|
|
21
|
+
SERVER_SOFTWARE = "SERVER_SOFTWARE"
|
|
22
|
+
SERVER_SOFTWARE_VALUE = "Raptor/#{Raptor::VERSION}".freeze
|
|
23
|
+
|
|
24
|
+
class WriteError < StandardError
|
|
25
|
+
# @rbs () -> String
|
|
26
|
+
def message = "could not write response"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Writes `string` in full, retrying on partial writes. Bounded by
|
|
30
|
+
# `timeout` so a slow client can't pin the writing thread.
|
|
31
|
+
#
|
|
32
|
+
# @param socket [TCPSocket] the socket to write to
|
|
33
|
+
# @param string [String] the data to write
|
|
34
|
+
# @param timeout [Integer] seconds to wait for the socket to become writable on each partial write
|
|
35
|
+
# @return [void]
|
|
36
|
+
# @raise [WriteError] if the socket is not writable within the timeout or raises IOError
|
|
37
|
+
#
|
|
38
|
+
# @rbs (TCPSocket socket, String string, ?timeout: Integer) -> void
|
|
39
|
+
def self.socket_write(socket, string, timeout: WRITE_TIMEOUT)
|
|
40
|
+
bytes = 0
|
|
41
|
+
byte_size = string.bytesize
|
|
42
|
+
|
|
43
|
+
while bytes < byte_size
|
|
44
|
+
begin
|
|
45
|
+
bytes += socket.write_nonblock(bytes.zero? ? string : string.byteslice(bytes..-1))
|
|
46
|
+
rescue IO::WaitWritable
|
|
47
|
+
raise WriteError unless socket.wait_writable(timeout)
|
|
48
|
+
retry
|
|
49
|
+
rescue IOError
|
|
50
|
+
raise WriteError
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Writes `strings` in full via a single `writev(2)` syscall when possible,
|
|
56
|
+
# falling back to per-string writes on partial results. Bounded by
|
|
57
|
+
# `timeout` so a slow client can't pin the writing thread.
|
|
58
|
+
#
|
|
59
|
+
# @param socket [TCPSocket] the socket to write to
|
|
60
|
+
# @param strings [Array<String>] the buffers to write in order
|
|
61
|
+
# @param timeout [Integer] seconds to wait for the socket to become writable on each retry
|
|
62
|
+
# @return [void]
|
|
63
|
+
# @raise [WriteError] if the socket is not writable within the timeout or raises IOError
|
|
64
|
+
#
|
|
65
|
+
# @rbs (TCPSocket socket, Array[String] strings, ?timeout: Integer) -> void
|
|
66
|
+
def self.socket_writev(socket, strings, timeout: WRITE_TIMEOUT)
|
|
67
|
+
total = strings.sum(&:bytesize)
|
|
68
|
+
return if total.zero?
|
|
69
|
+
|
|
70
|
+
begin
|
|
71
|
+
written = Raptor::VectorIO.writev_nonblock(socket, strings)
|
|
72
|
+
rescue IO::WaitWritable
|
|
73
|
+
raise WriteError unless socket.wait_writable(timeout)
|
|
74
|
+
retry
|
|
75
|
+
rescue IOError
|
|
76
|
+
raise WriteError
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
return if written == total
|
|
80
|
+
|
|
81
|
+
offset = 0
|
|
82
|
+
strings.each do |string|
|
|
83
|
+
size = string.bytesize
|
|
84
|
+
if written >= offset + size
|
|
85
|
+
offset += size
|
|
86
|
+
else
|
|
87
|
+
start = written - offset
|
|
88
|
+
socket_write(socket, start.zero? ? string : string.byteslice(start..-1), timeout: timeout)
|
|
89
|
+
offset += size
|
|
90
|
+
written = offset
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Writes a Common Log Format entry to `io`. Write failures are silently
|
|
96
|
+
# ignored.
|
|
97
|
+
#
|
|
98
|
+
# @param io [IO] the destination IO
|
|
99
|
+
# @param env [Hash] the Rack environment
|
|
100
|
+
# @param status [Integer] the response status code
|
|
101
|
+
# @param size [String] the response body size in bytes, or `-` if unknown
|
|
102
|
+
# @param remote_addr [String] the client IP address
|
|
103
|
+
# @return [void]
|
|
104
|
+
#
|
|
105
|
+
# @rbs (IO io, Hash[String, untyped] env, Integer status, String size, String remote_addr) -> void
|
|
106
|
+
def self.write_access_log(io, env, status, size, remote_addr)
|
|
107
|
+
timestamp = Time.now.strftime("%d/%b/%Y:%H:%M:%S %z")
|
|
108
|
+
method = env[Rack::REQUEST_METHOD]
|
|
109
|
+
query = env[Rack::QUERY_STRING]
|
|
110
|
+
path = query.empty? ? env[Rack::PATH_INFO] : "#{env[Rack::PATH_INFO]}?#{query}"
|
|
111
|
+
protocol = env[Rack::SERVER_PROTOCOL]
|
|
112
|
+
|
|
113
|
+
io.puts(%(#{remote_addr} - - [#{timestamp}] "#{method} #{path} #{protocol}" #{status} #{size})) rescue nil
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|