raptor 0.6.0 → 0.8.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/CHANGELOG.md +23 -0
- data/README.md +148 -23
- data/lib/rackup/handler/raptor.rb +12 -2
- data/lib/raptor/binder.rb +122 -28
- data/lib/raptor/cli.rb +82 -21
- data/lib/raptor/cluster.rb +188 -32
- data/lib/raptor/http.rb +75 -0
- data/lib/raptor/{request.rb → http1.rb} +202 -81
- data/lib/raptor/http2.rb +149 -61
- data/lib/raptor/reactor.rb +22 -15
- data/lib/raptor/server.rb +57 -37
- 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 +72 -5
- data/sig/generated/raptor/cli.rbs +2 -3
- data/sig/generated/raptor/cluster.rbs +89 -13
- data/sig/generated/raptor/http.rbs +52 -0
- data/sig/generated/raptor/{request.rbs → http1.rbs} +107 -31
- data/sig/generated/raptor/http2.rbs +64 -14
- data/sig/generated/raptor/reactor.rbs +18 -11
- data/sig/generated/raptor/server.rbs +32 -18
- data/sig/generated/raptor/systemd.rbs +42 -0
- metadata +7 -3
data/lib/raptor/cli.rb
CHANGED
|
@@ -26,24 +26,41 @@ module Raptor
|
|
|
26
26
|
class CLI
|
|
27
27
|
DEFAULT_WORKER_COUNT = Etc.nprocessors
|
|
28
28
|
|
|
29
|
+
NESTED_OPTION_KEYS = [:connection, :http1, :http2].freeze
|
|
30
|
+
|
|
29
31
|
DEFAULT_OPTIONS = {
|
|
30
32
|
binds: ["tcp://0.0.0.0:9292"].freeze,
|
|
33
|
+
socket_backlog: 1024,
|
|
34
|
+
drain_accept_queue: false,
|
|
31
35
|
workers: DEFAULT_WORKER_COUNT,
|
|
32
36
|
ractors: 1,
|
|
33
37
|
threads: 3,
|
|
34
38
|
rackup: "config.ru",
|
|
35
|
-
|
|
39
|
+
chdir: nil,
|
|
40
|
+
environment: nil,
|
|
41
|
+
connection: {
|
|
36
42
|
first_data_timeout: 30,
|
|
37
43
|
chunk_data_timeout: 10,
|
|
38
|
-
|
|
44
|
+
write_timeout: 5,
|
|
39
45
|
max_body_size: nil,
|
|
40
46
|
body_spool_threshold: 1024 * 1024,
|
|
41
47
|
},
|
|
42
|
-
|
|
48
|
+
http1: {
|
|
49
|
+
persistent_data_timeout: 65,
|
|
50
|
+
max_keepalive_requests: 100,
|
|
51
|
+
},
|
|
52
|
+
http2: {
|
|
53
|
+
max_concurrent_streams: 100,
|
|
54
|
+
},
|
|
43
55
|
worker_boot_timeout: 60,
|
|
56
|
+
worker_timeout: 60,
|
|
57
|
+
worker_drain_timeout: 25,
|
|
44
58
|
worker_shutdown_timeout: 30,
|
|
45
59
|
stats_file: "tmp/raptor.json",
|
|
46
60
|
pid_file: nil,
|
|
61
|
+
stdout_file: nil,
|
|
62
|
+
stderr_file: nil,
|
|
63
|
+
access_log_file: nil,
|
|
47
64
|
}.freeze
|
|
48
65
|
|
|
49
66
|
DEFAULT_CONFIG_PATHS = ["raptor.rb", "config/raptor.rb"].freeze
|
|
@@ -102,14 +119,17 @@ module Raptor
|
|
|
102
119
|
#
|
|
103
120
|
# @rbs (Array[String] argv) -> void
|
|
104
121
|
def initialize(argv)
|
|
122
|
+
@options = DEFAULT_OPTIONS.dup
|
|
123
|
+
NESTED_OPTION_KEYS.each { |key| @options[key] = @options[key].dup }
|
|
124
|
+
@options[:launch_command] = $PROGRAM_NAME
|
|
125
|
+
@options[:launch_argv] = argv.dup
|
|
126
|
+
|
|
105
127
|
if argv.first == "stats"
|
|
106
128
|
argv.shift
|
|
107
129
|
@command = :stats
|
|
108
130
|
else
|
|
109
131
|
@command = :server
|
|
110
132
|
end
|
|
111
|
-
@options = DEFAULT_OPTIONS.dup
|
|
112
|
-
@options[:client] = @options[:client].dup
|
|
113
133
|
|
|
114
134
|
apply_config_file(extract_config_path(argv) || self.class.default_config_path)
|
|
115
135
|
|
|
@@ -178,9 +198,6 @@ module Raptor
|
|
|
178
198
|
|
|
179
199
|
# Loads a config file and merges it into `@options` over the defaults.
|
|
180
200
|
#
|
|
181
|
-
# Top-level keys replace defaults; the nested `:client` hash is merged
|
|
182
|
-
# key-by-key so a config file does not need to restate every client option.
|
|
183
|
-
#
|
|
184
201
|
# @param path [String, nil] path to the config file, or nil to no-op
|
|
185
202
|
# @return [void]
|
|
186
203
|
#
|
|
@@ -190,8 +207,8 @@ module Raptor
|
|
|
190
207
|
|
|
191
208
|
config = self.class.load_config_file(path)
|
|
192
209
|
config.each do |key, value|
|
|
193
|
-
if key
|
|
194
|
-
@options[
|
|
210
|
+
if NESTED_OPTION_KEYS.include?(key) && value.is_a?(Hash)
|
|
211
|
+
@options[key] = @options[key].merge(value)
|
|
195
212
|
else
|
|
196
213
|
@options[key] = value
|
|
197
214
|
end
|
|
@@ -219,46 +236,78 @@ module Raptor
|
|
|
219
236
|
end
|
|
220
237
|
end
|
|
221
238
|
|
|
239
|
+
opts.on("--socket-backlog NUM", Integer, "Socket listen backlog (default: 1024)") do |num|
|
|
240
|
+
@options[:socket_backlog] = num
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
opts.on("--[no-]drain-accept-queue", "Drain the kernel accept queue on shutdown (default: off)") do |bool|
|
|
244
|
+
@options[:drain_accept_queue] = bool
|
|
245
|
+
end
|
|
246
|
+
|
|
222
247
|
opts.on("-w", "--workers NUM", Integer, "Number of worker processes (default: #{DEFAULT_WORKER_COUNT})") do |num|
|
|
223
248
|
@options[:workers] = num
|
|
224
249
|
end
|
|
225
250
|
|
|
226
|
-
opts.on("-r", "--ractors NUM", Integer, "Number of ractors (default: 1)") do |num|
|
|
251
|
+
opts.on("-r", "--ractors NUM", Integer, "Number of pipeline ractors per worker (default: 1)") do |num|
|
|
227
252
|
@options[:ractors] = num
|
|
228
253
|
end
|
|
229
254
|
|
|
230
|
-
opts.on("-t", "--threads NUM", Integer, "Number of threads (default: 3)") do |num|
|
|
255
|
+
opts.on("-t", "--threads NUM", Integer, "Number of application threads per worker (default: 3)") do |num|
|
|
231
256
|
@options[:threads] = num
|
|
232
257
|
end
|
|
233
258
|
|
|
259
|
+
opts.on("-C", "--chdir PATH", String, "Change to PATH before loading the Rack application (default: none)") do |path|
|
|
260
|
+
@options[:chdir] = path
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
opts.on("-e", "--environment ENV", String, "Application environment label; falls back to $RAILS_ENV, then $RACK_ENV, then development") do |env|
|
|
264
|
+
@options[:environment] = env
|
|
265
|
+
end
|
|
266
|
+
|
|
234
267
|
opts.on("--first-data-timeout SECONDS", Integer, "First data timeout in seconds (default: 30)") do |timeout|
|
|
235
|
-
@options[:
|
|
268
|
+
@options[:connection][:first_data_timeout] = timeout
|
|
236
269
|
end
|
|
237
270
|
|
|
238
271
|
opts.on("--chunk-data-timeout SECONDS", Integer, "Chunk data timeout in seconds (default: 10)") do |timeout|
|
|
239
|
-
@options[:
|
|
272
|
+
@options[:connection][:chunk_data_timeout] = timeout
|
|
240
273
|
end
|
|
241
274
|
|
|
242
|
-
opts.on("--
|
|
243
|
-
@options[:
|
|
275
|
+
opts.on("--write-timeout SECONDS", Integer, "Per-write socket timeout in seconds (default: 5)") do |timeout|
|
|
276
|
+
@options[:connection][:write_timeout] = timeout
|
|
244
277
|
end
|
|
245
278
|
|
|
246
279
|
opts.on("--max-body-size BYTES", Integer, "Maximum request body size in bytes (default: unlimited)") do |bytes|
|
|
247
|
-
@options[:
|
|
280
|
+
@options[:connection][:max_body_size] = bytes
|
|
248
281
|
end
|
|
249
282
|
|
|
250
|
-
opts.on("--body-spool-threshold BYTES", Integer, "
|
|
251
|
-
@options[:
|
|
283
|
+
opts.on("--body-spool-threshold BYTES", Integer, "Request body spool threshold in bytes (default: #{1024 * 1024})") do |bytes|
|
|
284
|
+
@options[:connection][:body_spool_threshold] = bytes
|
|
252
285
|
end
|
|
253
286
|
|
|
254
|
-
opts.on("--
|
|
255
|
-
@options[:
|
|
287
|
+
opts.on("--http1-persistent-data-timeout SECONDS", Integer, "HTTP/1.1 keep-alive idle timeout in seconds (default: 65)") do |timeout|
|
|
288
|
+
@options[:http1][:persistent_data_timeout] = timeout
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
opts.on("--http1-max-keepalive-requests NUM", Integer, "Maximum HTTP/1.1 requests per keep-alive connection (default: 100)") do |num|
|
|
292
|
+
@options[:http1][:max_keepalive_requests] = num
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
opts.on("--http2-max-concurrent-streams NUM", Integer, "Maximum HTTP/2 concurrent streams per connection (default: 100)") do |num|
|
|
296
|
+
@options[:http2][:max_concurrent_streams] = num
|
|
256
297
|
end
|
|
257
298
|
|
|
258
299
|
opts.on("--worker-boot-timeout SECONDS", Integer, "Worker boot timeout in seconds (default: 60)") do |timeout|
|
|
259
300
|
@options[:worker_boot_timeout] = timeout
|
|
260
301
|
end
|
|
261
302
|
|
|
303
|
+
opts.on("--worker-timeout SECONDS", Integer, "Worker check-in timeout in seconds (default: 60)") do |timeout|
|
|
304
|
+
@options[:worker_timeout] = timeout
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
opts.on("--worker-drain-timeout SECONDS", Integer, "Worker request-drain timeout in seconds (default: 25)") do |timeout|
|
|
308
|
+
@options[:worker_drain_timeout] = timeout
|
|
309
|
+
end
|
|
310
|
+
|
|
262
311
|
opts.on("--worker-shutdown-timeout SECONDS", Integer, "Worker shutdown timeout in seconds (default: 30)") do |timeout|
|
|
263
312
|
@options[:worker_shutdown_timeout] = timeout
|
|
264
313
|
end
|
|
@@ -271,6 +320,18 @@ module Raptor
|
|
|
271
320
|
@options[:pid_file] = path
|
|
272
321
|
end
|
|
273
322
|
|
|
323
|
+
opts.on("--stdout-file PATH", String, "Redirect stdout to PATH; reopened on SIGHUP (default: none)") do |path|
|
|
324
|
+
@options[:stdout_file] = path
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
opts.on("--stderr-file PATH", String, "Redirect stderr to PATH; reopened on SIGHUP (default: none)") do |path|
|
|
328
|
+
@options[:stderr_file] = path
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
opts.on("--access-log-file PATH", String, "Write Common Log Format access logs to PATH; reopened on SIGHUP (default: none)") do |path|
|
|
332
|
+
@options[:access_log_file] = path
|
|
333
|
+
end
|
|
334
|
+
|
|
274
335
|
opts.on("--help", "Show this help") do
|
|
275
336
|
puts opts
|
|
276
337
|
exit
|
data/lib/raptor/cluster.rb
CHANGED
|
@@ -11,9 +11,10 @@ require_relative "log"
|
|
|
11
11
|
require_relative "binder"
|
|
12
12
|
require_relative "server"
|
|
13
13
|
require_relative "reactor"
|
|
14
|
-
require_relative "
|
|
14
|
+
require_relative "http1"
|
|
15
15
|
require_relative "http2"
|
|
16
16
|
require_relative "stats"
|
|
17
|
+
require_relative "systemd"
|
|
17
18
|
|
|
18
19
|
module Raptor
|
|
19
20
|
# Multi-process web server cluster with advanced concurrency architecture.
|
|
@@ -41,11 +42,13 @@ module Raptor
|
|
|
41
42
|
# workers: 4, ractors: 2, threads: 8,
|
|
42
43
|
# binds: ["tcp://0.0.0.0:3000"],
|
|
43
44
|
# rackup: "config.ru",
|
|
44
|
-
#
|
|
45
|
+
# connection: { first_data_timeout: 30, chunk_data_timeout: 10 }
|
|
45
46
|
# }
|
|
46
47
|
# Cluster.run(options)
|
|
47
48
|
#
|
|
48
49
|
class Cluster
|
|
50
|
+
INHERITED_FDS_ENV = "RAPTOR_INHERITED_FDS"
|
|
51
|
+
|
|
49
52
|
# Convenience method to create and run a cluster with the given options.
|
|
50
53
|
#
|
|
51
54
|
# @param options [Hash] cluster configuration options
|
|
@@ -56,15 +59,26 @@ module Raptor
|
|
|
56
59
|
new(options).run
|
|
57
60
|
end
|
|
58
61
|
|
|
62
|
+
# @rbs @drain_accept_queue: bool
|
|
59
63
|
# @rbs @worker_count: Integer
|
|
60
64
|
# @rbs @ractor_count: Integer
|
|
61
65
|
# @rbs @thread_count: Integer
|
|
62
|
-
# @rbs @
|
|
63
|
-
# @rbs @
|
|
66
|
+
# @rbs @environment: String
|
|
67
|
+
# @rbs @connection_options: Hash[Symbol, untyped]
|
|
68
|
+
# @rbs @http1_options: Hash[Symbol, untyped]
|
|
69
|
+
# @rbs @http2_options: Hash[Symbol, untyped]
|
|
64
70
|
# @rbs @worker_boot_timeout: Integer
|
|
71
|
+
# @rbs @worker_timeout: Integer
|
|
72
|
+
# @rbs @worker_drain_timeout: Integer
|
|
65
73
|
# @rbs @worker_shutdown_timeout: Integer
|
|
66
74
|
# @rbs @stats_file: String?
|
|
67
75
|
# @rbs @pid_file: String?
|
|
76
|
+
# @rbs @stdout_file: String?
|
|
77
|
+
# @rbs @stderr_file: String?
|
|
78
|
+
# @rbs @access_log_file: String?
|
|
79
|
+
# @rbs @access_log_io: IO?
|
|
80
|
+
# @rbs @launch_command: String?
|
|
81
|
+
# @rbs @launch_argv: Array[String]?
|
|
68
82
|
# @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
69
83
|
# @rbs @binder: Binder
|
|
70
84
|
# @rbs @server_port: Integer
|
|
@@ -76,6 +90,7 @@ module Raptor
|
|
|
76
90
|
# @rbs @phase: Integer
|
|
77
91
|
# @rbs @phased_restart_requested: bool
|
|
78
92
|
# @rbs @phased_restarting: bool
|
|
93
|
+
# @rbs @hot_restart_requested: bool
|
|
79
94
|
|
|
80
95
|
# Creates a new Cluster with the specified configuration.
|
|
81
96
|
#
|
|
@@ -85,34 +100,67 @@ module Raptor
|
|
|
85
100
|
#
|
|
86
101
|
# @param options [Hash] cluster configuration options
|
|
87
102
|
# @option options [Array<String>] :binds array of bind URIs
|
|
103
|
+
# @option options [Integer] :socket_backlog kernel listen() queue depth for TCP/SSL listeners
|
|
104
|
+
# @option options [Boolean] :drain_accept_queue whether to drain the kernel accept queue on shutdown
|
|
88
105
|
# @option options [Integer] :workers number of worker processes
|
|
89
106
|
# @option options [Integer] :ractors number of ractors per worker process
|
|
90
107
|
# @option options [Integer] :threads number of threads per worker process
|
|
91
108
|
# @option options [#call] :app pre-built Rack application
|
|
92
109
|
# @option options [String] :rackup path to Rack configuration file
|
|
93
|
-
# @option options [
|
|
94
|
-
# @option options [
|
|
110
|
+
# @option options [String, nil] :chdir directory to change to before loading the Rack application, or nil to leave the working directory unchanged
|
|
111
|
+
# @option options [String, nil] :environment Raptor's application environment label; falls back to `$RAILS_ENV`, then `$RACK_ENV`, then `"development"`
|
|
112
|
+
# @option options [Hash] :connection per-connection settings shared across protocols
|
|
113
|
+
# @option options [Hash] :http1 HTTP/1.1-specific settings
|
|
114
|
+
# @option options [Hash] :http2 HTTP/2-specific settings
|
|
95
115
|
# @option options [Integer] :worker_boot_timeout seconds to wait for a worker to finish booting before killing it
|
|
116
|
+
# @option options [Integer] :worker_timeout seconds to wait for a booted worker to check in before killing it
|
|
117
|
+
# @option options [Integer] :worker_drain_timeout seconds a worker waits for in-flight requests during shutdown before force-killing app threads
|
|
96
118
|
# @option options [Integer] :worker_shutdown_timeout seconds to wait for graceful worker exit before force-killing
|
|
97
119
|
# @option options [String, nil] :stats_file path to write per-worker stats JSON, or nil to disable
|
|
98
120
|
# @option options [String, nil] :pid_file path to write the master PID to, or nil to disable
|
|
99
|
-
# @option options [
|
|
121
|
+
# @option options [String, nil] :stdout_file path to redirect stdout to, reopened on SIGHUP, or nil to disable
|
|
122
|
+
# @option options [String, nil] :stderr_file path to redirect stderr to, reopened on SIGHUP, or nil to disable
|
|
123
|
+
# @option options [String, nil] :access_log_file path to write Common Log Format access logs to, reopened on SIGHUP, or nil to disable
|
|
124
|
+
# @option options [String, nil] :launch_command path of the program to re-exec on hot restart, or nil to disable
|
|
125
|
+
# @option options [Array<String>, nil] :launch_argv command-line arguments for the hot-restart exec, or nil to disable
|
|
126
|
+
# @option options [#call, nil] :on_error callback invoked with (env, exception) when the Rack app raises
|
|
100
127
|
# @return [void]
|
|
101
128
|
#
|
|
102
129
|
# @rbs (Hash[Symbol, untyped] options) -> void
|
|
103
130
|
def initialize(options)
|
|
131
|
+
@drain_accept_queue = options[:drain_accept_queue]
|
|
104
132
|
@worker_count = options[:workers]
|
|
105
133
|
@ractor_count = options[:ractors]
|
|
106
134
|
@thread_count = options[:threads]
|
|
107
|
-
@
|
|
108
|
-
@
|
|
135
|
+
@environment = options[:environment] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
|
|
136
|
+
@connection_options = options[:connection]
|
|
137
|
+
@http1_options = options[:http1]
|
|
138
|
+
@http2_options = options[:http2]
|
|
109
139
|
@worker_boot_timeout = options[:worker_boot_timeout]
|
|
140
|
+
@worker_timeout = options[:worker_timeout]
|
|
141
|
+
@worker_drain_timeout = options[:worker_drain_timeout]
|
|
110
142
|
@worker_shutdown_timeout = options[:worker_shutdown_timeout]
|
|
111
143
|
@stats_file = options[:stats_file]
|
|
112
144
|
@pid_file = options[:pid_file]
|
|
145
|
+
@stdout_file = options[:stdout_file]
|
|
146
|
+
@stderr_file = options[:stderr_file]
|
|
147
|
+
@access_log_file = options[:access_log_file]
|
|
148
|
+
@access_log_io = nil
|
|
149
|
+
@launch_command = options[:launch_command]
|
|
150
|
+
@launch_argv = options[:launch_argv]
|
|
113
151
|
@on_error = options[:on_error]
|
|
114
152
|
|
|
115
|
-
|
|
153
|
+
Dir.chdir(options[:chdir]) if options[:chdir]
|
|
154
|
+
|
|
155
|
+
inherited_fds = if raw = ENV.delete(INHERITED_FDS_ENV)
|
|
156
|
+
JSON.parse(raw)
|
|
157
|
+
elsif (systemd_fds = Systemd.listen_fds).any?
|
|
158
|
+
Systemd.clear_listen_env
|
|
159
|
+
pair_systemd_fds(options[:binds], systemd_fds)
|
|
160
|
+
else
|
|
161
|
+
{}
|
|
162
|
+
end
|
|
163
|
+
@binder = Binder.new(options[:binds], socket_backlog: options[:socket_backlog], inherited_fds: inherited_fds)
|
|
116
164
|
@server_port = @binder.server_port
|
|
117
165
|
@app = options[:app] || Rack::Builder.parse_file(options[:rackup])
|
|
118
166
|
log_initialization
|
|
@@ -124,14 +172,15 @@ module Raptor
|
|
|
124
172
|
@phase = 0
|
|
125
173
|
@phased_restart_requested = false
|
|
126
174
|
@phased_restarting = false
|
|
175
|
+
@hot_restart_requested = false
|
|
127
176
|
end
|
|
128
177
|
|
|
129
178
|
# Starts the multi-process cluster and manages worker processes.
|
|
130
179
|
#
|
|
131
180
|
# Forks the configured number of worker processes and monitors them,
|
|
132
181
|
# restarting any that exit unexpectedly or stop checking in. Handles
|
|
133
|
-
# graceful shutdown via INT or TERM signals,
|
|
134
|
-
# and
|
|
182
|
+
# graceful shutdown via INT or TERM signals, phased restart via USR1,
|
|
183
|
+
# and hot restart via USR2.
|
|
135
184
|
#
|
|
136
185
|
# Each worker process includes:
|
|
137
186
|
# - 1 server thread (continuously accepts connections with backpressure control)
|
|
@@ -145,10 +194,13 @@ module Raptor
|
|
|
145
194
|
#
|
|
146
195
|
# @rbs () -> void
|
|
147
196
|
def run
|
|
197
|
+
reopen_logs
|
|
198
|
+
|
|
148
199
|
trap("INT") { shutdown }
|
|
149
200
|
trap("TERM") { shutdown }
|
|
150
|
-
trap("
|
|
151
|
-
trap("
|
|
201
|
+
trap("HUP") { reopen_logs_and_signal_workers }
|
|
202
|
+
trap("USR1") { @phased_restart_requested = true }
|
|
203
|
+
trap("USR2") { @hot_restart_requested = true }
|
|
152
204
|
|
|
153
205
|
File.open(@pid_file, File::CREAT | File::EXCL | File::WRONLY) { |file| file.write(Process.pid.to_s) } if @pid_file
|
|
154
206
|
|
|
@@ -162,15 +214,19 @@ module Raptor
|
|
|
162
214
|
end
|
|
163
215
|
end
|
|
164
216
|
|
|
217
|
+
Systemd.notify("READY=1\nMAINPID=#{Process.pid}")
|
|
218
|
+
|
|
165
219
|
until @shutdown
|
|
166
220
|
break if reap_workers == :no_children
|
|
167
221
|
|
|
222
|
+
perform_hot_restart if @hot_restart_requested
|
|
168
223
|
perform_phased_restart if @phased_restart_requested && !@phased_restarting
|
|
169
224
|
timeout_hung_workers
|
|
170
225
|
|
|
171
226
|
sleep 0.1
|
|
172
227
|
end
|
|
173
228
|
|
|
229
|
+
Systemd.notify("STOPPING=1")
|
|
174
230
|
stop_workers
|
|
175
231
|
stats_file_thread&.join
|
|
176
232
|
File.delete(@stats_file) rescue nil if @stats_file
|
|
@@ -190,6 +246,25 @@ module Raptor
|
|
|
190
246
|
|
|
191
247
|
private
|
|
192
248
|
|
|
249
|
+
# Returns the inherited-FDs hash for a systemd socket-activation handoff,
|
|
250
|
+
# pairing each bind URI with the FD systemd passed at the same index.
|
|
251
|
+
# The activation is skipped (with a warning) when the FD count doesn't
|
|
252
|
+
# match the number of bind URIs.
|
|
253
|
+
#
|
|
254
|
+
# @param bind_uris [Array<String>] the configured bind URIs
|
|
255
|
+
# @param filenos [Array<Integer>] file descriptors passed by systemd
|
|
256
|
+
# @return [Hash{String => Array<Integer>}]
|
|
257
|
+
#
|
|
258
|
+
# @rbs (Array[String] bind_uris, Array[Integer] filenos) -> Hash[String, Array[Integer]]
|
|
259
|
+
def pair_systemd_fds(bind_uris, filenos)
|
|
260
|
+
if bind_uris.length != filenos.length
|
|
261
|
+
Log.warn "Ignoring socket activation: #{filenos.length} fd(s) from systemd, #{bind_uris.length} bind(s) configured"
|
|
262
|
+
return {}
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
bind_uris.zip(filenos).to_h { |bind_uri, fileno| [bind_uri, [fileno]] }
|
|
266
|
+
end
|
|
267
|
+
|
|
193
268
|
# Forks a new worker process and registers it at the given index.
|
|
194
269
|
# The worker inherits the cluster's current phase.
|
|
195
270
|
#
|
|
@@ -278,7 +353,7 @@ module Raptor
|
|
|
278
353
|
end
|
|
279
354
|
|
|
280
355
|
# Replaces each worker process one at a time, waiting for the new
|
|
281
|
-
# worker to boot before moving on to the next.
|
|
356
|
+
# worker to boot before moving on to the next.
|
|
282
357
|
#
|
|
283
358
|
# @return [void]
|
|
284
359
|
#
|
|
@@ -315,6 +390,36 @@ module Raptor
|
|
|
315
390
|
end
|
|
316
391
|
end
|
|
317
392
|
|
|
393
|
+
# Re-execs the master process with a fresh boot of the same Raptor
|
|
394
|
+
# invocation, handing the new master its listening sockets so accepted
|
|
395
|
+
# connections continue to be served across the swap.
|
|
396
|
+
#
|
|
397
|
+
# @return [void]
|
|
398
|
+
#
|
|
399
|
+
# @rbs () -> void
|
|
400
|
+
def perform_hot_restart
|
|
401
|
+
@hot_restart_requested = false
|
|
402
|
+
|
|
403
|
+
unless @launch_command && @launch_argv
|
|
404
|
+
Log.warn "Hot restart unavailable: launch command not captured"
|
|
405
|
+
return
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
Log.info "Hot restart starting"
|
|
409
|
+
monotonic_usec = (Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1_000_000).to_i
|
|
410
|
+
Systemd.notify("RELOADING=1\nMONOTONIC_USEC=#{monotonic_usec}")
|
|
411
|
+
@shutdown = true
|
|
412
|
+
stop_workers
|
|
413
|
+
@binder.clear_close_on_exec
|
|
414
|
+
ENV[INHERITED_FDS_ENV] = JSON.generate(@binder.inheritable_fds)
|
|
415
|
+
File.delete(@stats_file) rescue nil if @stats_file
|
|
416
|
+
File.delete(@pid_file) rescue nil if @pid_file
|
|
417
|
+
@stats.unmap
|
|
418
|
+
$stdout.flush
|
|
419
|
+
$stderr.flush
|
|
420
|
+
exec(@launch_command, *@launch_argv)
|
|
421
|
+
end
|
|
422
|
+
|
|
318
423
|
# Runs the full server stack inside a worker process.
|
|
319
424
|
#
|
|
320
425
|
# Sets up and coordinates the reactor, server, ractor pool, thread pool,
|
|
@@ -330,6 +435,9 @@ module Raptor
|
|
|
330
435
|
shutdown_requested = false
|
|
331
436
|
trap("INT") { shutdown_requested = true }
|
|
332
437
|
trap("TERM") { shutdown_requested = true }
|
|
438
|
+
trap("HUP") { reopen_logs }
|
|
439
|
+
trap("USR1", "IGNORE")
|
|
440
|
+
trap("USR2", "IGNORE")
|
|
333
441
|
|
|
334
442
|
started_at = Process.clock_gettime(Process::CLOCK_REALTIME)
|
|
335
443
|
request_count = 0
|
|
@@ -354,27 +462,46 @@ module Raptor
|
|
|
354
462
|
@app.call(env)
|
|
355
463
|
}
|
|
356
464
|
thread_pool = AtomicThreadPool.new(size: @thread_count)
|
|
357
|
-
|
|
358
|
-
|
|
465
|
+
http1 = Http1.new(
|
|
466
|
+
counting_app,
|
|
467
|
+
@server_port,
|
|
468
|
+
connection_options: @connection_options,
|
|
469
|
+
http1_options: @http1_options,
|
|
470
|
+
access_log_io: @access_log_io,
|
|
471
|
+
on_error: @on_error
|
|
472
|
+
)
|
|
473
|
+
http2 = Http2.new(
|
|
474
|
+
counting_app,
|
|
475
|
+
@server_port,
|
|
476
|
+
connection_options: @connection_options,
|
|
477
|
+
http2_options: @http2_options,
|
|
478
|
+
access_log_io: @access_log_io,
|
|
479
|
+
on_error: @on_error
|
|
480
|
+
)
|
|
359
481
|
ractor_pool = RactorPool.new(
|
|
360
482
|
size: @ractor_count,
|
|
361
|
-
worker:
|
|
483
|
+
worker: http1.http_parser_worker
|
|
362
484
|
) do |parsed_result|
|
|
363
485
|
begin
|
|
364
486
|
if parsed_result[:protocol] == :http2
|
|
365
487
|
http2.handle_parsed_request(parsed_result, reactor, thread_pool)
|
|
366
488
|
else
|
|
367
|
-
|
|
489
|
+
http1.handle_parsed_request(parsed_result, reactor, thread_pool)
|
|
368
490
|
end
|
|
369
491
|
rescue => error
|
|
370
492
|
Log.rescued_error(error)
|
|
371
493
|
end
|
|
372
494
|
end
|
|
373
495
|
|
|
374
|
-
reactor = Reactor.new(
|
|
496
|
+
reactor = Reactor.new(
|
|
497
|
+
ractor_pool,
|
|
498
|
+
thread_pool,
|
|
499
|
+
connection_options: @connection_options,
|
|
500
|
+
http1_options: @http1_options
|
|
501
|
+
)
|
|
375
502
|
reactor_thread = reactor.run
|
|
376
503
|
|
|
377
|
-
server = Server.new(@binder, reactor, thread_pool,
|
|
504
|
+
server = Server.new(@binder, reactor, thread_pool, http1, http2, connection_options: @connection_options, drain_accept_queue: @drain_accept_queue)
|
|
378
505
|
server_thread = server.run
|
|
379
506
|
|
|
380
507
|
Log.info "Worker #{index} booted"
|
|
@@ -412,11 +539,28 @@ module Raptor
|
|
|
412
539
|
reactor.shutdown
|
|
413
540
|
reactor_thread.join
|
|
414
541
|
ractor_pool.shutdown
|
|
415
|
-
|
|
416
|
-
thread_pool
|
|
542
|
+
http1.shutdown
|
|
543
|
+
drain_thread_pool(thread_pool)
|
|
417
544
|
stats_thread.join
|
|
418
545
|
end
|
|
419
546
|
|
|
547
|
+
# Shuts down the worker's application thread pool, force-killing the
|
|
548
|
+
# underlying threads if in-flight requests have not finished within
|
|
549
|
+
# `worker_drain_timeout` seconds.
|
|
550
|
+
#
|
|
551
|
+
# @param thread_pool [AtomicThreadPool] the worker's thread pool
|
|
552
|
+
# @return [void]
|
|
553
|
+
#
|
|
554
|
+
# @rbs (AtomicThreadPool thread_pool) -> void
|
|
555
|
+
def drain_thread_pool(thread_pool)
|
|
556
|
+
drain = Thread.new { thread_pool.shutdown }
|
|
557
|
+
return if drain.join(@worker_drain_timeout)
|
|
558
|
+
|
|
559
|
+
Log.warn "Force-killing in-flight app threads after #{@worker_drain_timeout}s drain timeout"
|
|
560
|
+
thread_pool.instance_variable_get(:@threads).each(&:kill)
|
|
561
|
+
drain.join
|
|
562
|
+
end
|
|
563
|
+
|
|
420
564
|
# Returns a human-readable description of how a process exited.
|
|
421
565
|
#
|
|
422
566
|
# @param status [Process::Status] the exit status of the process
|
|
@@ -454,6 +598,7 @@ module Raptor
|
|
|
454
598
|
Log.info "Cluster initializing:"
|
|
455
599
|
Log.info "├─ Version: #{VERSION}"
|
|
456
600
|
Log.info "├─ Ruby Version: #{RUBY_DESCRIPTION}"
|
|
601
|
+
Log.info "├─ Environment: #{@environment}"
|
|
457
602
|
Log.info "├─ Master PID: #{Process.pid}"
|
|
458
603
|
Log.info "│ └─ #{@worker_count} worker process#{"es" if @worker_count > 1}"
|
|
459
604
|
Log.info "│ ├─ 1 server thread"
|
|
@@ -465,20 +610,31 @@ module Raptor
|
|
|
465
610
|
Log.info "└─ Listening on #{@binder.addresses.join(", ")}"
|
|
466
611
|
end
|
|
467
612
|
|
|
468
|
-
#
|
|
613
|
+
# Redirects `$stdout`, `$stderr`, and the access log to their configured
|
|
614
|
+
# paths. No-op for any stream whose target path is nil.
|
|
469
615
|
#
|
|
470
|
-
#
|
|
616
|
+
# @return [void]
|
|
617
|
+
#
|
|
618
|
+
# @rbs () -> void
|
|
619
|
+
def reopen_logs
|
|
620
|
+
$stdout.reopen(@stdout_file, "a").sync = true if @stdout_file
|
|
621
|
+
$stderr.reopen(@stderr_file, "a").sync = true if @stderr_file
|
|
622
|
+
return unless @access_log_file
|
|
623
|
+
|
|
624
|
+
@access_log_io ||= File.open(@access_log_file, "a")
|
|
625
|
+
@access_log_io.reopen(@access_log_file, "a")
|
|
626
|
+
@access_log_io.sync = true
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
# Reopens the master's log files and forwards SIGHUP to each worker so
|
|
630
|
+
# they reopen their own inherited file descriptors.
|
|
471
631
|
#
|
|
472
632
|
# @return [void]
|
|
473
633
|
#
|
|
474
634
|
# @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
|
|
635
|
+
def reopen_logs_and_signal_workers
|
|
636
|
+
reopen_logs
|
|
637
|
+
@workers.values.each { |pid| Process.kill("HUP", pid) rescue nil }
|
|
482
638
|
end
|
|
483
639
|
|
|
484
640
|
# Writes the stats file on a 1-second interval until shutdown.
|
data/lib/raptor/http.rb
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "rack"
|
|
5
|
+
|
|
6
|
+
require_relative "version"
|
|
7
|
+
|
|
8
|
+
module Raptor
|
|
9
|
+
# Shared HTTP utilities used by both the HTTP/1.x and HTTP/2 handlers:
|
|
10
|
+
# Rack env keys that aren't provided by Rack itself, low-level socket
|
|
11
|
+
# writing, and Common Log Format access-log formatting.
|
|
12
|
+
#
|
|
13
|
+
module Http
|
|
14
|
+
WRITE_TIMEOUT = 5
|
|
15
|
+
|
|
16
|
+
CONTENT_LENGTH = "CONTENT_LENGTH"
|
|
17
|
+
CONTENT_TYPE = "CONTENT_TYPE"
|
|
18
|
+
HTTP_VERSION = "HTTP_VERSION"
|
|
19
|
+
REMOTE_ADDR = "REMOTE_ADDR"
|
|
20
|
+
SERVER_SOFTWARE = "SERVER_SOFTWARE"
|
|
21
|
+
SERVER_SOFTWARE_VALUE = "Raptor/#{Raptor::VERSION}".freeze
|
|
22
|
+
|
|
23
|
+
class WriteError < StandardError
|
|
24
|
+
# @rbs () -> String
|
|
25
|
+
def message = "could not write response"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Writes `string` in full, retrying on partial writes. Bounded by
|
|
29
|
+
# `timeout` so a slow client can't pin the writing thread.
|
|
30
|
+
#
|
|
31
|
+
# @param socket [TCPSocket] the socket to write to
|
|
32
|
+
# @param string [String] the data to write
|
|
33
|
+
# @param timeout [Integer] seconds to wait for the socket to become writable on each partial write
|
|
34
|
+
# @return [void]
|
|
35
|
+
# @raise [WriteError] if the socket is not writable within the timeout or raises IOError
|
|
36
|
+
#
|
|
37
|
+
# @rbs (TCPSocket socket, String string, ?timeout: Integer) -> void
|
|
38
|
+
def self.socket_write(socket, string, timeout: WRITE_TIMEOUT)
|
|
39
|
+
bytes = 0
|
|
40
|
+
byte_size = string.bytesize
|
|
41
|
+
|
|
42
|
+
while bytes < byte_size
|
|
43
|
+
begin
|
|
44
|
+
bytes += socket.write_nonblock(bytes.zero? ? string : string.byteslice(bytes..-1))
|
|
45
|
+
rescue IO::WaitWritable
|
|
46
|
+
raise WriteError unless socket.wait_writable(timeout)
|
|
47
|
+
retry
|
|
48
|
+
rescue IOError
|
|
49
|
+
raise WriteError
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Writes a Common Log Format entry to `io`. Write failures are silently
|
|
55
|
+
# ignored.
|
|
56
|
+
#
|
|
57
|
+
# @param io [IO] the destination IO
|
|
58
|
+
# @param env [Hash] the Rack environment
|
|
59
|
+
# @param status [Integer] the response status code
|
|
60
|
+
# @param size [String] the response body size in bytes, or `-` if unknown
|
|
61
|
+
# @param remote_addr [String] the client IP address
|
|
62
|
+
# @return [void]
|
|
63
|
+
#
|
|
64
|
+
# @rbs (IO io, Hash[String, untyped] env, Integer status, String size, String remote_addr) -> void
|
|
65
|
+
def self.write_access_log(io, env, status, size, remote_addr)
|
|
66
|
+
timestamp = Time.now.strftime("%d/%b/%Y:%H:%M:%S %z")
|
|
67
|
+
method = env[Rack::REQUEST_METHOD]
|
|
68
|
+
query = env[Rack::QUERY_STRING]
|
|
69
|
+
path = query.empty? ? env[Rack::PATH_INFO] : "#{env[Rack::PATH_INFO]}?#{query}"
|
|
70
|
+
protocol = env[Rack::SERVER_PROTOCOL]
|
|
71
|
+
|
|
72
|
+
io.puts(%(#{remote_addr} - - [#{timestamp}] "#{method} #{path} #{protocol}" #{status} #{size})) rescue nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|