raptor 0.18.0 → 0.20.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 +17 -0
- data/README.md +67 -45
- data/docs/brisrails-talk.md +19 -16
- data/docs/raptor-vs-puma.md +43 -34
- data/lib/rackup/handler/raptor.rb +10 -4
- data/lib/raptor/binder.rb +9 -0
- data/lib/raptor/cli.rb +57 -14
- data/lib/raptor/cluster.rb +95 -18
- data/lib/raptor/control_server.rb +164 -0
- data/lib/raptor/http1.rb +32 -12
- data/lib/raptor/http2.rb +22 -2
- data/lib/raptor/reactor.rb +1 -1
- data/lib/raptor/thread_locals.rb +38 -0
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/binder.rbs +7 -0
- data/sig/generated/raptor/cli.rbs +15 -5
- data/sig/generated/raptor/cluster.rbs +43 -17
- data/sig/generated/raptor/control_server.rbs +70 -0
- data/sig/generated/raptor/http1.rbs +16 -6
- data/sig/generated/raptor/http2.rbs +13 -2
- data/sig/generated/raptor/reactor.rbs +1 -1
- data/sig/generated/raptor/thread_locals.rbs +24 -0
- metadata +5 -1
|
@@ -44,7 +44,7 @@ module Rackup
|
|
|
44
44
|
"Port=PORT" => "Port to listen on (default: #{DEFAULT_OPTIONS[:Port]})",
|
|
45
45
|
"Workers=NUM" => "Number of worker processes (default: available processor count)",
|
|
46
46
|
"Threads=NUM" => "Number of threads per worker (default: 3)",
|
|
47
|
-
"MaxThreads=NUM" => "Maximum threads per worker (`unlimited` for no limit; default:
|
|
47
|
+
"MaxThreads=NUM" => "Maximum threads per worker (`unlimited` for no limit; default: unlimited)",
|
|
48
48
|
"Config=PATH" => "Load additional configuration from PATH"
|
|
49
49
|
}
|
|
50
50
|
end
|
|
@@ -71,6 +71,8 @@ module Rackup
|
|
|
71
71
|
cli_defaults = ::Raptor::CLI::DEFAULT_OPTIONS
|
|
72
72
|
config_path = options[:Config] || ::Raptor::CLI.default_config_path
|
|
73
73
|
config = config_path ? ::Raptor::CLI.load_config_file(config_path) : {}
|
|
74
|
+
threads = (options[:Threads] || ENV["RAPTOR_THREADS"] || config[:threads] || cli_defaults[:threads]).to_i
|
|
75
|
+
max_threads = options[:MaxThreads] || ENV["RAPTOR_MAX_THREADS"] || config.fetch(:max_threads, cli_defaults[:max_threads])
|
|
74
76
|
|
|
75
77
|
result = {
|
|
76
78
|
binds: if options[:Host] || options[:Port]
|
|
@@ -80,9 +82,12 @@ module Rackup
|
|
|
80
82
|
end,
|
|
81
83
|
socket_backlog: (config[:socket_backlog] || cli_defaults[:socket_backlog]).to_i,
|
|
82
84
|
drain_accept_queue: config.key?(:drain_accept_queue) ? config[:drain_accept_queue] : cli_defaults[:drain_accept_queue],
|
|
83
|
-
workers: (options[:Workers] || config[:workers] || Concurrent.available_processor_count).to_i,
|
|
84
|
-
threads:
|
|
85
|
-
max_threads: ::Raptor::CLI.parse_max_threads(
|
|
85
|
+
workers: (options[:Workers] || ENV["RAPTOR_WORKERS"] || config[:workers] || Concurrent.available_processor_count).to_i,
|
|
86
|
+
threads: threads,
|
|
87
|
+
max_threads: ::Raptor::CLI.parse_max_threads(max_threads, threads: threads),
|
|
88
|
+
cpu_affinity: config.key?(:cpu_affinity) ? config[:cpu_affinity] : cli_defaults[:cpu_affinity],
|
|
89
|
+
clean_thread_locals: config.key?(:clean_thread_locals) ? config[:clean_thread_locals] : cli_defaults[:clean_thread_locals],
|
|
90
|
+
clean_fiber_locals: config.key?(:clean_fiber_locals) ? config[:clean_fiber_locals] : cli_defaults[:clean_fiber_locals],
|
|
86
91
|
app: app
|
|
87
92
|
}
|
|
88
93
|
result[:rackup] = config[:rackup] if config.key?(:rackup)
|
|
@@ -96,6 +101,7 @@ module Rackup
|
|
|
96
101
|
result[:worker_drain_timeout] = (config[:worker_drain_timeout] || cli_defaults[:worker_drain_timeout]).to_i
|
|
97
102
|
result[:worker_shutdown_timeout] = (config[:worker_shutdown_timeout] || cli_defaults[:worker_shutdown_timeout]).to_i
|
|
98
103
|
result[:stats_file] = config.key?(:stats_file) ? config[:stats_file] : cli_defaults[:stats_file]
|
|
104
|
+
result[:control_url] = config[:control_url] if config.key?(:control_url)
|
|
99
105
|
result[:pid_file] = config[:pid_file] if config.key?(:pid_file)
|
|
100
106
|
result[:stdout_file] = config[:stdout_file] if config.key?(:stdout_file)
|
|
101
107
|
result[:stderr_file] = config[:stderr_file] if config.key?(:stderr_file)
|
data/lib/raptor/binder.rb
CHANGED
|
@@ -106,6 +106,15 @@ module Raptor
|
|
|
106
106
|
tcp_listener.local_address.ip_port
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
+
# Returns whether any listener can negotiate HTTP/2.
|
|
110
|
+
#
|
|
111
|
+
# @return [Boolean]
|
|
112
|
+
#
|
|
113
|
+
# @rbs () -> bool
|
|
114
|
+
def http2?
|
|
115
|
+
@listeners.any? { |listener| listener.is_a?(SslListener) }
|
|
116
|
+
end
|
|
117
|
+
|
|
109
118
|
# Closes all listening sockets.
|
|
110
119
|
#
|
|
111
120
|
# @return [void]
|
data/lib/raptor/cli.rb
CHANGED
|
@@ -34,7 +34,10 @@ module Raptor
|
|
|
34
34
|
drain_accept_queue: false,
|
|
35
35
|
workers: DEFAULT_WORKER_COUNT,
|
|
36
36
|
threads: 3,
|
|
37
|
-
max_threads:
|
|
37
|
+
max_threads: Float::INFINITY,
|
|
38
|
+
cpu_affinity: false,
|
|
39
|
+
clean_thread_locals: true,
|
|
40
|
+
clean_fiber_locals: false,
|
|
38
41
|
rackup: "config.ru",
|
|
39
42
|
chdir: nil,
|
|
40
43
|
environment: nil,
|
|
@@ -48,7 +51,7 @@ module Raptor
|
|
|
48
51
|
http1: {
|
|
49
52
|
ractors: nil,
|
|
50
53
|
persistent_data_timeout: 65,
|
|
51
|
-
max_keepalive_requests:
|
|
54
|
+
max_keepalive_requests: 1000,
|
|
52
55
|
},
|
|
53
56
|
http2: {
|
|
54
57
|
ractors: nil,
|
|
@@ -64,6 +67,7 @@ module Raptor
|
|
|
64
67
|
before_worker_shutdown: [].freeze,
|
|
65
68
|
before_refork: [].freeze,
|
|
66
69
|
stats_file: "tmp/raptor.json",
|
|
70
|
+
control_url: nil,
|
|
67
71
|
pid_file: nil,
|
|
68
72
|
stdout_file: nil,
|
|
69
73
|
stderr_file: nil,
|
|
@@ -101,17 +105,27 @@ module Raptor
|
|
|
101
105
|
|
|
102
106
|
# Parses a maximum thread count from a CLI, config, or Rack handler value.
|
|
103
107
|
#
|
|
104
|
-
# @param value [Integer, Float, String
|
|
105
|
-
# @
|
|
106
|
-
#
|
|
108
|
+
# @param value [Integer, Float, String] maximum thread count
|
|
109
|
+
# @param threads [Integer] baseline thread count
|
|
110
|
+
# @return [Integer, Float] parsed thread count, or `Float::INFINITY` for
|
|
111
|
+
# `"unlimited"`
|
|
112
|
+
# @raise [ArgumentError] if the maximum is nil or less than the baseline
|
|
107
113
|
#
|
|
108
|
-
# @rbs (Integer | Float | String
|
|
109
|
-
def self.parse_max_threads(value)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
114
|
+
# @rbs (Integer | Float | String value, threads: Integer) -> (Integer | Float)
|
|
115
|
+
def self.parse_max_threads(value, threads:)
|
|
116
|
+
raise ArgumentError, "max_threads cannot be nil" unless value
|
|
117
|
+
|
|
118
|
+
max_threads = if value == "unlimited" || value == Float::INFINITY
|
|
119
|
+
Float::INFINITY
|
|
120
|
+
elsif value.is_a?(Integer)
|
|
121
|
+
value
|
|
122
|
+
else
|
|
123
|
+
Integer(value, 10)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
raise ArgumentError, "max_threads must be greater than or equal to threads" if max_threads < threads
|
|
113
127
|
|
|
114
|
-
|
|
128
|
+
max_threads
|
|
115
129
|
end
|
|
116
130
|
|
|
117
131
|
# @rbs @command: Symbol
|
|
@@ -141,12 +155,13 @@ module Raptor
|
|
|
141
155
|
end
|
|
142
156
|
|
|
143
157
|
apply_config_file(extract_config_path(argv) || self.class.default_config_path)
|
|
158
|
+
apply_environment
|
|
144
159
|
|
|
145
160
|
@parser = create_parser
|
|
146
161
|
@parser.parse!(argv)
|
|
147
162
|
|
|
148
163
|
@options[:rackup] = argv.first if @command == :server && argv.first
|
|
149
|
-
@options[:max_threads] = self.class.parse_max_threads(@options[:max_threads])
|
|
164
|
+
@options[:max_threads] = self.class.parse_max_threads(@options[:max_threads], threads: @options[:threads])
|
|
150
165
|
end
|
|
151
166
|
|
|
152
167
|
# Runs the requested command.
|
|
@@ -222,6 +237,18 @@ module Raptor
|
|
|
222
237
|
end
|
|
223
238
|
end
|
|
224
239
|
|
|
240
|
+
# Applies worker and thread settings from the environment. Explicit CLI
|
|
241
|
+
# options are parsed afterwards and take precedence.
|
|
242
|
+
#
|
|
243
|
+
# @return [void]
|
|
244
|
+
#
|
|
245
|
+
# @rbs () -> void
|
|
246
|
+
def apply_environment
|
|
247
|
+
@options[:workers] = Integer(ENV["RAPTOR_WORKERS"], 10) if ENV["RAPTOR_WORKERS"]
|
|
248
|
+
@options[:threads] = Integer(ENV["RAPTOR_THREADS"], 10) if ENV["RAPTOR_THREADS"]
|
|
249
|
+
@options[:max_threads] = ENV["RAPTOR_MAX_THREADS"] if ENV["RAPTOR_MAX_THREADS"]
|
|
250
|
+
end
|
|
251
|
+
|
|
225
252
|
# Creates the OptionParser instance with all supported command-line options.
|
|
226
253
|
#
|
|
227
254
|
# @return [OptionParser] configured option parser
|
|
@@ -259,10 +286,22 @@ module Raptor
|
|
|
259
286
|
@options[:threads] = num
|
|
260
287
|
end
|
|
261
288
|
|
|
262
|
-
opts.on("--max-threads NUM", "Maximum application threads per worker (`unlimited` for no limit; default:
|
|
289
|
+
opts.on("--max-threads NUM", "Maximum application threads per worker (`unlimited` for no limit; default: unlimited)") do |num|
|
|
263
290
|
@options[:max_threads] = num
|
|
264
291
|
end
|
|
265
292
|
|
|
293
|
+
opts.on("--[no-]cpu-affinity", "Pin each worker process to a CPU (default: off)") do |bool|
|
|
294
|
+
@options[:cpu_affinity] = bool
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
opts.on("--[no-]clean-thread-locals", "Clear application thread locals after each request (default: on)") do |bool|
|
|
298
|
+
@options[:clean_thread_locals] = bool
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
opts.on("--[no-]clean-fiber-locals", "Process each request in a fresh Fiber (default: off)") do |bool|
|
|
302
|
+
@options[:clean_fiber_locals] = bool
|
|
303
|
+
end
|
|
304
|
+
|
|
266
305
|
opts.on("-C", "--chdir PATH", String, "Change to PATH before loading the Rack application (default: none)") do |path|
|
|
267
306
|
@options[:chdir] = path
|
|
268
307
|
end
|
|
@@ -299,7 +338,7 @@ module Raptor
|
|
|
299
338
|
@options[:http1][:persistent_data_timeout] = timeout
|
|
300
339
|
end
|
|
301
340
|
|
|
302
|
-
opts.on("--http1-max-keepalive-requests NUM", Integer, "Maximum HTTP/1.1 requests per keep-alive connection (default:
|
|
341
|
+
opts.on("--http1-max-keepalive-requests NUM", Integer, "Maximum HTTP/1.1 requests per keep-alive connection (default: 1000)") do |num|
|
|
303
342
|
@options[:http1][:max_keepalive_requests] = num
|
|
304
343
|
end
|
|
305
344
|
|
|
@@ -335,6 +374,10 @@ module Raptor
|
|
|
335
374
|
@options[:stats_file] = path
|
|
336
375
|
end
|
|
337
376
|
|
|
377
|
+
opts.on("--control-url URI", String, "Serve cluster stats on a unix:// URI (default: off)") do |uri|
|
|
378
|
+
@options[:control_url] = uri
|
|
379
|
+
end
|
|
380
|
+
|
|
338
381
|
opts.on("--pid-file PATH", String, "PID file path (default: none)") do |path|
|
|
339
382
|
@options[:pid_file] = path
|
|
340
383
|
end
|
data/lib/raptor/cluster.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
require "concurrent/utility/processor_counter"
|
|
5
5
|
require "json"
|
|
6
|
+
require "time"
|
|
6
7
|
|
|
7
8
|
require "atomic-ruby/atomic_thread_pool"
|
|
8
9
|
require "rack/builder"
|
|
@@ -10,6 +11,7 @@ require "ractor-pool"
|
|
|
10
11
|
|
|
11
12
|
require_relative "log"
|
|
12
13
|
require_relative "binder"
|
|
14
|
+
require_relative "control_server"
|
|
13
15
|
require_relative "reactor"
|
|
14
16
|
require_relative "server"
|
|
15
17
|
require_relative "reuseport_bpf"
|
|
@@ -74,6 +76,9 @@ module Raptor
|
|
|
74
76
|
# @rbs @http2_ractor_count: Integer
|
|
75
77
|
# @rbs @thread_count: Integer
|
|
76
78
|
# @rbs @max_thread_count: (Integer | Float)?
|
|
79
|
+
# @rbs @cpu_affinity: bool
|
|
80
|
+
# @rbs @clean_thread_locals: bool
|
|
81
|
+
# @rbs @clean_fiber_locals: bool
|
|
77
82
|
# @rbs @environment: String
|
|
78
83
|
# @rbs @connection_options: Hash[Symbol, untyped]
|
|
79
84
|
# @rbs @http1_options: Hash[Symbol, untyped]
|
|
@@ -87,6 +92,7 @@ module Raptor
|
|
|
87
92
|
# @rbs @before_worker_shutdown: Array[Proc]
|
|
88
93
|
# @rbs @before_refork: Array[Proc]
|
|
89
94
|
# @rbs @stats_file: String?
|
|
95
|
+
# @rbs @control_server: ControlServer?
|
|
90
96
|
# @rbs @pid_file: String?
|
|
91
97
|
# @rbs @stdout_file: String?
|
|
92
98
|
# @rbs @stderr_file: String?
|
|
@@ -102,6 +108,7 @@ module Raptor
|
|
|
102
108
|
# @rbs @workers: Hash[Integer, Integer]
|
|
103
109
|
# @rbs @timed_out: Set[Integer]
|
|
104
110
|
# @rbs @stats: Stats
|
|
111
|
+
# @rbs @started_at: Float
|
|
105
112
|
# @rbs @phase: Integer
|
|
106
113
|
# @rbs @phased_restart_requested: bool
|
|
107
114
|
# @rbs @phased_restarting: bool
|
|
@@ -126,7 +133,10 @@ module Raptor
|
|
|
126
133
|
# @option options [Boolean] :drain_accept_queue whether to drain the kernel accept queue on shutdown
|
|
127
134
|
# @option options [Integer] :workers number of worker processes
|
|
128
135
|
# @option options [Integer] :threads number of threads per worker process
|
|
129
|
-
# @option options [Integer, Float
|
|
136
|
+
# @option options [Integer, Float] :max_threads maximum number of threads per worker process, or `Float::INFINITY` for no limit
|
|
137
|
+
# @option options [Boolean] :cpu_affinity whether to pin each worker to a CPU
|
|
138
|
+
# @option options [Boolean] :clean_thread_locals whether to clear application thread locals after each request
|
|
139
|
+
# @option options [Boolean] :clean_fiber_locals whether to process each request in a fresh Fiber
|
|
130
140
|
# @option options [#call] :app pre-built Rack application
|
|
131
141
|
# @option options [String] :rackup path to Rack configuration file
|
|
132
142
|
# @option options [String, nil] :chdir directory to change to before loading the Rack application, or nil to leave the working directory unchanged
|
|
@@ -140,10 +150,11 @@ module Raptor
|
|
|
140
150
|
# @option options [Integer] :worker_shutdown_timeout seconds to wait for graceful worker exit before force-killing
|
|
141
151
|
# @option options [Integer, Array<Integer>, nil] :refork_after request-count threshold(s) at which a warm worker is promoted to a fork source for phased refork; nil or 0 disables. Requires `PR_SET_CHILD_SUBREAPER` (Linux)
|
|
142
152
|
# @option options [Array<Proc>] :before_fork procs called in the master before every worker fork
|
|
143
|
-
# @option options [Array<Proc>] :before_worker_boot procs called
|
|
144
|
-
# @option options [Array<Proc>] :before_worker_shutdown procs called
|
|
153
|
+
# @option options [Array<Proc>] :before_worker_boot procs called with the worker index before it begins serving
|
|
154
|
+
# @option options [Array<Proc>] :before_worker_shutdown procs called with the worker index before its graceful shutdown
|
|
145
155
|
# @option options [Array<Proc>] :before_refork procs called in a worker before it transitions to a seed
|
|
146
156
|
# @option options [String, nil] :stats_file path to write per-worker stats JSON, or nil to disable
|
|
157
|
+
# @option options [String, nil] :control_url `unix://` URL serving cluster stats, or nil to disable
|
|
147
158
|
# @option options [String, nil] :pid_file path to write the master PID to, or nil to disable
|
|
148
159
|
# @option options [String, nil] :stdout_file path to redirect stdout to, reopened on SIGHUP, or nil to disable
|
|
149
160
|
# @option options [String, nil] :stderr_file path to redirect stderr to, reopened on SIGHUP, or nil to disable
|
|
@@ -161,6 +172,9 @@ module Raptor
|
|
|
161
172
|
@http2_ractor_count = options[:http2][:ractors] || self.class.default_http2_ractor_count(@worker_count)
|
|
162
173
|
@thread_count = options[:threads]
|
|
163
174
|
@max_thread_count = options[:max_threads]
|
|
175
|
+
@cpu_affinity = options[:cpu_affinity]
|
|
176
|
+
@clean_thread_locals = options[:clean_thread_locals]
|
|
177
|
+
@clean_fiber_locals = options[:clean_fiber_locals]
|
|
164
178
|
@environment = options[:environment] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
|
|
165
179
|
@connection_options = options[:connection]
|
|
166
180
|
@http1_options = options[:http1]
|
|
@@ -174,6 +188,7 @@ module Raptor
|
|
|
174
188
|
@before_worker_shutdown = Array(options[:before_worker_shutdown])
|
|
175
189
|
@before_refork = Array(options[:before_refork])
|
|
176
190
|
@stats_file = options[:stats_file]
|
|
191
|
+
@control_server = ControlServer.new(options[:control_url]) { stats_hash } if options[:control_url]
|
|
177
192
|
@pid_file = options[:pid_file]
|
|
178
193
|
@stdout_file = options[:stdout_file]
|
|
179
194
|
@stderr_file = options[:stderr_file]
|
|
@@ -202,6 +217,7 @@ module Raptor
|
|
|
202
217
|
@workers = {}
|
|
203
218
|
@timed_out = Set.new
|
|
204
219
|
@stats = Stats.new(@worker_count)
|
|
220
|
+
@started_at = Process.clock_gettime(Process::CLOCK_REALTIME)
|
|
205
221
|
@phase = 0
|
|
206
222
|
@phased_restart_requested = false
|
|
207
223
|
@phased_restarting = false
|
|
@@ -247,7 +263,9 @@ module Raptor
|
|
|
247
263
|
|
|
248
264
|
@bpf_active = ReuseportBPF.setup(@worker_count)
|
|
249
265
|
|
|
266
|
+
@control_server&.bind
|
|
250
267
|
@worker_count.times { |index| spawn_worker(index) }
|
|
268
|
+
@control_server&.start
|
|
251
269
|
|
|
252
270
|
stats_file_thread = if @stats_file
|
|
253
271
|
Thread.new do
|
|
@@ -274,6 +292,7 @@ module Raptor
|
|
|
274
292
|
Systemd.notify("STOPPING=1")
|
|
275
293
|
stop_workers
|
|
276
294
|
stats_file_thread&.join
|
|
295
|
+
@control_server&.shutdown
|
|
277
296
|
File.delete(@stats_file) rescue nil if @stats_file
|
|
278
297
|
File.delete(@pid_file) rescue nil if @pid_file
|
|
279
298
|
@stats.unmap
|
|
@@ -291,8 +310,57 @@ module Raptor
|
|
|
291
310
|
@stats.all
|
|
292
311
|
end
|
|
293
312
|
|
|
313
|
+
# Returns cluster statistics for the control server.
|
|
314
|
+
# For an adaptive pool, `max_threads` is the number of threads currently
|
|
315
|
+
# running so utilization reflects the capacity available at that moment.
|
|
316
|
+
#
|
|
317
|
+
# @return [Hash] cluster statistics
|
|
318
|
+
#
|
|
319
|
+
# @rbs () -> Hash[Symbol, untyped]
|
|
320
|
+
def stats_hash
|
|
321
|
+
worker_stats = @stats.all
|
|
322
|
+
worker_status = @workers.sort.map do |index, pid|
|
|
323
|
+
stat = worker_stats[index]
|
|
324
|
+
stat = {} unless stat && stat[:pid] == pid
|
|
325
|
+
capacity = stat.fetch(:thread_capacity, 0)
|
|
326
|
+
active = stat.fetch(:busy_threads, 0)
|
|
327
|
+
total_work = stat.fetch(:backlog, 0)
|
|
328
|
+
|
|
329
|
+
{
|
|
330
|
+
started_at: timestamp(stat.fetch(:started_at, 0)),
|
|
331
|
+
pid: pid,
|
|
332
|
+
index: index,
|
|
333
|
+
phase: stat.fetch(:phase, @phase),
|
|
334
|
+
booted: stat.fetch(:booted, false),
|
|
335
|
+
last_checkin: timestamp(stat.fetch(:last_checkin, 0)),
|
|
336
|
+
last_status: {
|
|
337
|
+
backlog: [total_work - active, 0].max,
|
|
338
|
+
running: capacity,
|
|
339
|
+
pool_capacity: [capacity - total_work, 0].max,
|
|
340
|
+
busy_threads: active,
|
|
341
|
+
max_threads: capacity,
|
|
342
|
+
requests_count: stat.fetch(:requests, 0),
|
|
343
|
+
},
|
|
344
|
+
}
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
{
|
|
348
|
+
started_at: timestamp(@started_at),
|
|
349
|
+
workers: worker_status.length,
|
|
350
|
+
phase: @phase,
|
|
351
|
+
booted_workers: worker_status.count { |worker| worker[:booted] },
|
|
352
|
+
old_workers: worker_status.count { |worker| worker[:phase] != @phase },
|
|
353
|
+
worker_status: worker_status,
|
|
354
|
+
}
|
|
355
|
+
end
|
|
356
|
+
|
|
294
357
|
private
|
|
295
358
|
|
|
359
|
+
# @rbs (Float timestamp) -> String
|
|
360
|
+
def timestamp(timestamp)
|
|
361
|
+
Time.at(timestamp).utc.iso8601(6)
|
|
362
|
+
end
|
|
363
|
+
|
|
296
364
|
# Returns the inherited-FDs hash for a systemd socket-activation handoff,
|
|
297
365
|
# pairing each bind URI with the FD systemd passed at the same index.
|
|
298
366
|
# Returns an empty hash when the FD count doesn't match the bind count.
|
|
@@ -700,6 +768,7 @@ module Raptor
|
|
|
700
768
|
stop_workers
|
|
701
769
|
@binder.clear_close_on_exec
|
|
702
770
|
ENV[INHERITED_FDS_ENV] = JSON.generate(@binder.inheritable_fds)
|
|
771
|
+
@control_server&.shutdown
|
|
703
772
|
File.delete(@stats_file) rescue nil if @stats_file
|
|
704
773
|
File.delete(@pid_file) rescue nil if @pid_file
|
|
705
774
|
@stats.unmap
|
|
@@ -731,7 +800,7 @@ module Raptor
|
|
|
731
800
|
trap("USR2", "IGNORE")
|
|
732
801
|
trap("URG") { promote_to_seed = true } if @fork_r
|
|
733
802
|
|
|
734
|
-
Raptor::CPU.pin(index) if Raptor::CPU.count >= @worker_count
|
|
803
|
+
Raptor::CPU.pin(index) if @cpu_affinity && Raptor::CPU.count >= @worker_count
|
|
735
804
|
|
|
736
805
|
started_at = Process.clock_gettime(Process::CLOCK_REALTIME)
|
|
737
806
|
request_count = 0
|
|
@@ -762,6 +831,8 @@ module Raptor
|
|
|
762
831
|
connection_options: @connection_options,
|
|
763
832
|
http1_options: @http1_options,
|
|
764
833
|
access_log_io: @access_log_io,
|
|
834
|
+
clean_thread_locals: @clean_thread_locals,
|
|
835
|
+
clean_fiber_locals: @clean_fiber_locals,
|
|
765
836
|
on_error: @on_error
|
|
766
837
|
)
|
|
767
838
|
http2 = Http2.new(
|
|
@@ -770,6 +841,8 @@ module Raptor
|
|
|
770
841
|
connection_options: @connection_options,
|
|
771
842
|
http2_options: @http2_options,
|
|
772
843
|
access_log_io: @access_log_io,
|
|
844
|
+
clean_thread_locals: @clean_thread_locals,
|
|
845
|
+
clean_fiber_locals: @clean_fiber_locals,
|
|
773
846
|
on_error: @on_error
|
|
774
847
|
)
|
|
775
848
|
http1_ractor_pool = RactorPool.new(
|
|
@@ -784,15 +857,17 @@ module Raptor
|
|
|
784
857
|
end
|
|
785
858
|
end
|
|
786
859
|
|
|
787
|
-
http2_ractor_pool =
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
860
|
+
http2_ractor_pool = if @binder.http2?
|
|
861
|
+
RactorPool.new(
|
|
862
|
+
size: @http2_ractor_count,
|
|
863
|
+
worker: http2.parser_worker,
|
|
864
|
+
name: "HTTP/2"
|
|
865
|
+
) do |parsed_result|
|
|
866
|
+
begin
|
|
867
|
+
http2.handle_parsed_request(parsed_result, reactor, thread_pool)
|
|
868
|
+
rescue => error
|
|
869
|
+
Log.rescued_error(error)
|
|
870
|
+
end
|
|
796
871
|
end
|
|
797
872
|
end
|
|
798
873
|
|
|
@@ -827,7 +902,7 @@ module Raptor
|
|
|
827
902
|
)
|
|
828
903
|
server_thread = server.run
|
|
829
904
|
|
|
830
|
-
@before_worker_boot.each(
|
|
905
|
+
@before_worker_boot.each { |hook| hook.parameters.empty? ? hook.call : hook.call(index) }
|
|
831
906
|
|
|
832
907
|
Log.info "Worker #{index} booted"
|
|
833
908
|
|
|
@@ -863,7 +938,7 @@ module Raptor
|
|
|
863
938
|
@before_refork.each(&:call)
|
|
864
939
|
server.stop_accepting
|
|
865
940
|
else
|
|
866
|
-
@before_worker_shutdown.each(
|
|
941
|
+
@before_worker_shutdown.each { |hook| hook.parameters.empty? ? hook.call : hook.call(index) }
|
|
867
942
|
server.shutdown
|
|
868
943
|
end
|
|
869
944
|
|
|
@@ -871,7 +946,7 @@ module Raptor
|
|
|
871
946
|
reactor.shutdown
|
|
872
947
|
reactor_thread.join
|
|
873
948
|
http1_ractor_pool.shutdown
|
|
874
|
-
http2_ractor_pool
|
|
949
|
+
http2_ractor_pool&.shutdown
|
|
875
950
|
http1.shutdown
|
|
876
951
|
drain_thread_pool(thread_pool)
|
|
877
952
|
stats_thread.join
|
|
@@ -979,8 +1054,10 @@ module Raptor
|
|
|
979
1054
|
Log.info "│ ├─ 1 server thread"
|
|
980
1055
|
Log.info "│ ├─ 1 reactor thread"
|
|
981
1056
|
Log.info "│ ├─ #{@http1_ractor_count} HTTP/1.1 pipeline ractor#{"s" if @http1_ractor_count > 1}"
|
|
982
|
-
|
|
983
|
-
Log.info "│ ├─ 2 pipeline
|
|
1057
|
+
http2_enabled = @binder.http2?
|
|
1058
|
+
Log.info "│ ├─ #{@http2_ractor_count} HTTP/2 pipeline ractor#{"s" if @http2_ractor_count > 1}" if http2_enabled
|
|
1059
|
+
collector_count = http2_enabled ? 2 : 1
|
|
1060
|
+
Log.info "│ ├─ #{collector_count} pipeline collector thread#{"s" if collector_count > 1}"
|
|
984
1061
|
thread_limit = if @max_thread_count == Float::INFINITY
|
|
985
1062
|
" (scaling, no limit)"
|
|
986
1063
|
elsif @max_thread_count
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "socket"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
require "atomic-ruby/atom"
|
|
9
|
+
require "atomic-ruby/atomic_boolean"
|
|
10
|
+
|
|
11
|
+
module Raptor
|
|
12
|
+
# Serves cluster statistics over a Unix socket.
|
|
13
|
+
#
|
|
14
|
+
class ControlServer
|
|
15
|
+
MAX_REQUEST_SIZE = 16 * 1024
|
|
16
|
+
REQUEST_TIMEOUT = 1
|
|
17
|
+
|
|
18
|
+
# @rbs @path: String
|
|
19
|
+
# @rbs @stats: ^() -> Hash[Symbol, untyped]
|
|
20
|
+
# @rbs @server: UNIXServer?
|
|
21
|
+
# @rbs @client: Atom
|
|
22
|
+
# @rbs @thread: Thread?
|
|
23
|
+
# @rbs @running: AtomicBoolean
|
|
24
|
+
|
|
25
|
+
# Creates a control server for `url` without binding it.
|
|
26
|
+
#
|
|
27
|
+
# @param url [String] `unix://` URL to listen on
|
|
28
|
+
# @yieldreturn [Hash] cluster statistics
|
|
29
|
+
# @return [void]
|
|
30
|
+
# @raise [ArgumentError] if the URL is not a Unix socket
|
|
31
|
+
#
|
|
32
|
+
# @rbs (String url) { () -> Hash[Symbol, untyped] } -> void
|
|
33
|
+
def initialize(url, &stats)
|
|
34
|
+
uri = URI(url)
|
|
35
|
+
raise ArgumentError, "control_url must use unix://" unless uri.scheme == "unix" && !uri.path.empty?
|
|
36
|
+
|
|
37
|
+
@path = uri.path
|
|
38
|
+
@stats = stats
|
|
39
|
+
@server = nil
|
|
40
|
+
@client = Atom.new(nil)
|
|
41
|
+
@thread = nil
|
|
42
|
+
@running = AtomicBoolean.new(false)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Binds the Unix socket.
|
|
46
|
+
#
|
|
47
|
+
# @return [void]
|
|
48
|
+
#
|
|
49
|
+
# @rbs () -> void
|
|
50
|
+
def bind
|
|
51
|
+
remove_stale_socket
|
|
52
|
+
@server = UNIXServer.new(@path)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Starts serving requests in a background thread.
|
|
56
|
+
#
|
|
57
|
+
# @return [void]
|
|
58
|
+
#
|
|
59
|
+
# @rbs () -> void
|
|
60
|
+
def start
|
|
61
|
+
@running.make_true
|
|
62
|
+
owner_pid = Process.pid
|
|
63
|
+
at_exit { File.delete(@path) rescue nil if Process.pid == owner_pid }
|
|
64
|
+
|
|
65
|
+
@thread = Thread.new do
|
|
66
|
+
Thread.current.name = "Control Server"
|
|
67
|
+
|
|
68
|
+
serve
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Stops serving and removes the socket.
|
|
73
|
+
#
|
|
74
|
+
# @return [void]
|
|
75
|
+
#
|
|
76
|
+
# @rbs () -> void
|
|
77
|
+
def shutdown
|
|
78
|
+
@running.make_false
|
|
79
|
+
@server&.close
|
|
80
|
+
close_client
|
|
81
|
+
@thread&.join
|
|
82
|
+
File.delete(@path) rescue nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
# @rbs () -> void
|
|
88
|
+
def remove_stale_socket
|
|
89
|
+
return unless File.exist?(@path)
|
|
90
|
+
|
|
91
|
+
begin
|
|
92
|
+
UNIXSocket.new(@path).close
|
|
93
|
+
raise "Socket #{@path.inspect} is already in use"
|
|
94
|
+
rescue Errno::ECONNREFUSED
|
|
95
|
+
File.delete(@path)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @rbs () -> void
|
|
100
|
+
def serve
|
|
101
|
+
while @running.true?
|
|
102
|
+
readable, = IO.select([@server], nil, nil, 1)
|
|
103
|
+
next unless readable
|
|
104
|
+
|
|
105
|
+
client = @server.accept_nonblock(exception: false)
|
|
106
|
+
next unless client.is_a?(UNIXSocket)
|
|
107
|
+
|
|
108
|
+
@client.swap { client }
|
|
109
|
+
unless @running.true?
|
|
110
|
+
close_client
|
|
111
|
+
break
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
handle(client)
|
|
115
|
+
end
|
|
116
|
+
rescue IOError, Errno::EBADF
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @rbs (UNIXSocket client) -> void
|
|
120
|
+
def handle(client)
|
|
121
|
+
request = read_request(client)
|
|
122
|
+
return unless request
|
|
123
|
+
|
|
124
|
+
if request.start_with?("GET /stats ")
|
|
125
|
+
body = JSON.generate(@stats.call)
|
|
126
|
+
client.write("HTTP/1.0 200 OK\r\nContent-Type: application/json\r\nContent-Length: #{body.bytesize}\r\n\r\n#{body}")
|
|
127
|
+
else
|
|
128
|
+
client.write("HTTP/1.0 404 Not Found\r\nContent-Length: 0\r\n\r\n")
|
|
129
|
+
end
|
|
130
|
+
rescue IOError, SystemCallError
|
|
131
|
+
ensure
|
|
132
|
+
close_client
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @rbs (UNIXSocket client) -> String?
|
|
136
|
+
def read_request(client)
|
|
137
|
+
request = String.new
|
|
138
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + REQUEST_TIMEOUT
|
|
139
|
+
|
|
140
|
+
loop do
|
|
141
|
+
return request if request.include?("\r\n\r\n")
|
|
142
|
+
return if request.bytesize >= MAX_REQUEST_SIZE
|
|
143
|
+
|
|
144
|
+
timeout = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
145
|
+
return if timeout <= 0 || !client.wait_readable(timeout)
|
|
146
|
+
|
|
147
|
+
chunk = client.read_nonblock(MAX_REQUEST_SIZE - request.bytesize, exception: false)
|
|
148
|
+
return unless chunk.is_a?(String)
|
|
149
|
+
|
|
150
|
+
request << chunk
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# @rbs () -> void
|
|
155
|
+
def close_client
|
|
156
|
+
client = nil
|
|
157
|
+
@client.swap do |current|
|
|
158
|
+
client = current
|
|
159
|
+
nil
|
|
160
|
+
end
|
|
161
|
+
client&.close
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|