raptor 0.4.0 → 0.5.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/.mise.toml +2 -0
- data/Brewfile +2 -0
- data/CHANGELOG.md +8 -0
- data/README.md +27 -24
- data/lib/rackup/handler/raptor.rb +18 -20
- data/lib/raptor/cli.rb +26 -10
- data/lib/raptor/cluster.rb +128 -56
- data/lib/raptor/http2.rb +4 -3
- data/lib/raptor/log.rb +55 -0
- data/lib/raptor/reactor.rb +25 -29
- data/lib/raptor/request.rb +10 -16
- data/lib/raptor/server.rb +17 -19
- data/lib/raptor/stats.rb +30 -26
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/cli.rbs +1 -1
- data/sig/generated/raptor/cluster.rbs +69 -37
- data/sig/generated/raptor/http2.rbs +2 -1
- data/sig/generated/raptor/log.rbs +41 -0
- data/sig/generated/raptor/reactor.rbs +23 -26
- data/sig/generated/raptor/request.rbs +6 -13
- data/sig/generated/raptor/server.rbs +14 -16
- data/sig/generated/raptor/stats.rbs +24 -20
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c20a98b3c581180e2b9ea2e9cdd23223f4f43438b94852b0b19b8e1b659c751d
|
|
4
|
+
data.tar.gz: bc98ee07befe42ad9c2e30b4645aeac4a1fb369cc598e360a336b591a56d74b1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b065827d528422e5476827c2c01b792cd9b4a83ba38484283c33f05459a744a28a7ce88bfbe396c80fe07209893662350f205dd823a469503be208428f0cc784
|
|
7
|
+
data.tar.gz: '02583bea8f88760256318b3714b9f95cb7936b12ec0d9bb54a120108a734f965a9b85d4e556e6b8e73d7c731a43be6ddf96bd4871228f35d7bf2903df255cc84'
|
data/.mise.toml
ADDED
data/Brewfile
ADDED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.5.0] - 2026-05-31
|
|
4
|
+
|
|
5
|
+
- Apply the default `stats_file` in the Rack handler
|
|
6
|
+
- Add `phase` to per-worker stats
|
|
7
|
+
- Force-kill workers that fail to exit within `--worker-shutdown-timeout` after shutdown is signalled
|
|
8
|
+
- Kill workers that fail to check in within `--worker-timeout` or `--worker-boot-timeout`
|
|
9
|
+
- Add `index`, `busy_threads`, and `thread_capacity` to per-worker stats
|
|
10
|
+
|
|
3
11
|
## [0.4.0] - 2026-05-29
|
|
4
12
|
|
|
5
13
|
- Load `raptor.rb` or `config/raptor.rb` by default when no config path is supplied
|
data/README.md
CHANGED
|
@@ -29,23 +29,23 @@ run proc { |_env| [200, { "content-type" => "text/plain" }, ["Hello, World!"]] }
|
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
```
|
|
32
|
-
> bundle exec raptor -
|
|
33
|
-
Raptor Cluster initializing:
|
|
34
|
-
├─ Version: 0.
|
|
35
|
-
├─ Ruby Version: ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +YJIT +PRISM [arm64-darwin23]
|
|
36
|
-
├─ Master PID:
|
|
37
|
-
│ └─ 4 worker processes
|
|
38
|
-
│ ├─ 1 server thread
|
|
39
|
-
│ ├─ 1 reactor thread
|
|
40
|
-
│ ├─ 1 pipeline ractor
|
|
41
|
-
│ ├─ 1 pipeline collector thread
|
|
42
|
-
│ ├─ 3 worker threads
|
|
43
|
-
│ └─ 1 stats thread
|
|
44
|
-
└─ Listening on 0.0.0.0:9292
|
|
45
|
-
[
|
|
46
|
-
[
|
|
47
|
-
[
|
|
48
|
-
[
|
|
32
|
+
> bundle exec raptor -w 4 -t 3 hello_world.ru
|
|
33
|
+
[Raptor 91348|main|main] Cluster initializing:
|
|
34
|
+
[Raptor 91348|main|main] ├─ Version: 0.5.0
|
|
35
|
+
[Raptor 91348|main|main] ├─ Ruby Version: ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +YJIT +PRISM [arm64-darwin23]
|
|
36
|
+
[Raptor 91348|main|main] ├─ Master PID: 91348
|
|
37
|
+
[Raptor 91348|main|main] │ └─ 4 worker processes
|
|
38
|
+
[Raptor 91348|main|main] │ ├─ 1 server thread
|
|
39
|
+
[Raptor 91348|main|main] │ ├─ 1 reactor thread
|
|
40
|
+
[Raptor 91348|main|main] │ ├─ 1 pipeline ractor
|
|
41
|
+
[Raptor 91348|main|main] │ ├─ 1 pipeline collector thread
|
|
42
|
+
[Raptor 91348|main|main] │ ├─ 3 worker threads
|
|
43
|
+
[Raptor 91348|main|main] │ └─ 1 stats thread
|
|
44
|
+
[Raptor 91348|main|main] └─ Listening on 0.0.0.0:9292
|
|
45
|
+
[Raptor 91350|main|main] Worker 0 booted
|
|
46
|
+
[Raptor 91351|main|main] Worker 1 booted
|
|
47
|
+
[Raptor 91352|main|main] Worker 2 booted
|
|
48
|
+
[Raptor 91353|main|main] Worker 3 booted
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
```
|
|
@@ -62,15 +62,18 @@ Also works with `rackup` and `rails server`:
|
|
|
62
62
|
|
|
63
63
|
## (Micro) Benchmarks
|
|
64
64
|
|
|
65
|
-
Raptor 0.
|
|
65
|
+
Raptor 0.5.0 vs Puma 8.0.2:
|
|
66
66
|
|
|
67
|
-
| Protocol | Raptor
|
|
68
|
-
| --------------------- |
|
|
69
|
-
| HTTP/1.1 |
|
|
70
|
-
| HTTP/1.1 (keep-alive) |
|
|
71
|
-
| HTTP/2 |
|
|
67
|
+
| Protocol | Raptor | Puma |
|
|
68
|
+
| --------------------- | ----------- | ----------- |
|
|
69
|
+
| HTTP/1.1 | 20.1k req/s | 20k req/s |
|
|
70
|
+
| HTTP/1.1 (keep-alive) | 61.4k req/s | 39.1k req/s |
|
|
71
|
+
| HTTP/2 | 22.8k req/s | N/A |
|
|
72
72
|
|
|
73
|
-
>
|
|
73
|
+
> ruby 4.0.5 (2026-05-20 revision 64336ffd0e) +YJIT +PRISM [arm64-darwin23]
|
|
74
|
+
> 4 workers, 3 threads, 12 concurrent connections
|
|
75
|
+
|
|
76
|
+
See [bin/benchmark](bin/benchmark) for more details.
|
|
74
77
|
|
|
75
78
|
## Development
|
|
76
79
|
|
|
@@ -42,9 +42,9 @@ module Rackup
|
|
|
42
42
|
{
|
|
43
43
|
"Host=HOST" => "Hostname to listen on (default: #{DEFAULT_OPTIONS[:Host]})",
|
|
44
44
|
"Port=PORT" => "Port to listen on (default: #{DEFAULT_OPTIONS[:Port]})",
|
|
45
|
-
"Threads=NUM" => "Number of threads per worker (default: 3)",
|
|
46
|
-
"Ractors=NUM" => "Number of pipeline ractors per worker (default: 1)",
|
|
47
45
|
"Workers=NUM" => "Number of worker processes (default: nprocessors)",
|
|
46
|
+
"Ractors=NUM" => "Number of pipeline ractors per worker (default: 1)",
|
|
47
|
+
"Threads=NUM" => "Number of threads per worker (default: 3)",
|
|
48
48
|
"Config=PATH" => "Load additional configuration from PATH"
|
|
49
49
|
}
|
|
50
50
|
end
|
|
@@ -72,27 +72,25 @@ module Rackup
|
|
|
72
72
|
config_path = options[:Config] || ::Raptor::CLI.default_config_path
|
|
73
73
|
config = config_path ? ::Raptor::CLI.load_config_file(config_path) : {}
|
|
74
74
|
|
|
75
|
-
binds = if options[:Host] || options[:Port]
|
|
76
|
-
host = options[:Host] || defaults[:Host]
|
|
77
|
-
port = options[:Port] || defaults[:Port]
|
|
78
|
-
["tcp://#{host}:#{port}"]
|
|
79
|
-
else
|
|
80
|
-
config[:binds] || ["tcp://#{defaults[:Host]}:#{defaults[:Port]}"]
|
|
81
|
-
end
|
|
82
|
-
|
|
83
75
|
result = {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
76
|
+
binds: if options[:Host] || options[:Port]
|
|
77
|
+
["tcp://#{options[:Host] || defaults[:Host]}:#{options[:Port] || defaults[:Port]}"]
|
|
78
|
+
else
|
|
79
|
+
config[:binds] || ["tcp://#{defaults[:Host]}:#{defaults[:Port]}"]
|
|
80
|
+
end,
|
|
88
81
|
workers: (options[:Workers] || config[:workers] || Etc.nprocessors).to_i,
|
|
89
|
-
|
|
82
|
+
ractors: (options[:Ractors] || config[:ractors] || cli_defaults[:ractors]).to_i,
|
|
83
|
+
threads: (options[:Threads] || config[:threads] || cli_defaults[:threads]).to_i,
|
|
84
|
+
app: app
|
|
90
85
|
}
|
|
91
|
-
|
|
92
|
-
[:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
86
|
+
result[:rackup] = config[:rackup] if config.key?(:rackup)
|
|
87
|
+
result[:client] = cli_defaults[:client].merge(config[:client] || {})
|
|
88
|
+
result[:worker_timeout] = (config[:worker_timeout] || cli_defaults[:worker_timeout]).to_i
|
|
89
|
+
result[:worker_boot_timeout] = (config[:worker_boot_timeout] || cli_defaults[:worker_boot_timeout]).to_i
|
|
90
|
+
result[:worker_shutdown_timeout] = (config[:worker_shutdown_timeout] || cli_defaults[:worker_shutdown_timeout]).to_i
|
|
91
|
+
result[:stats_file] = config.key?(:stats_file) ? config[:stats_file] : cli_defaults[:stats_file]
|
|
92
|
+
result[:pid_file] = config[:pid_file] if config.key?(:pid_file)
|
|
93
|
+
result[:on_error] = config[:on_error] if config.key?(:on_error)
|
|
96
94
|
result
|
|
97
95
|
end
|
|
98
96
|
private_class_method :build_cluster_options
|
data/lib/raptor/cli.rb
CHANGED
|
@@ -12,7 +12,7 @@ module Raptor
|
|
|
12
12
|
#
|
|
13
13
|
# CLI parses command-line arguments and starts the server cluster with the
|
|
14
14
|
# specified configuration options. It supports configuring the number of
|
|
15
|
-
# workers,
|
|
15
|
+
# workers, ractors, threads, bind addresses, and various client timeout
|
|
16
16
|
# settings.
|
|
17
17
|
#
|
|
18
18
|
# @example Basic usage
|
|
@@ -28,9 +28,9 @@ module Raptor
|
|
|
28
28
|
|
|
29
29
|
DEFAULT_OPTIONS = {
|
|
30
30
|
binds: ["tcp://0.0.0.0:9292"].freeze,
|
|
31
|
-
threads: 3,
|
|
32
|
-
ractors: 1,
|
|
33
31
|
workers: DEFAULT_WORKER_COUNT,
|
|
32
|
+
ractors: 1,
|
|
33
|
+
threads: 3,
|
|
34
34
|
rackup: "config.ru",
|
|
35
35
|
client: {
|
|
36
36
|
first_data_timeout: 30,
|
|
@@ -39,6 +39,9 @@ module Raptor
|
|
|
39
39
|
max_body_size: nil,
|
|
40
40
|
body_spool_threshold: 1024 * 1024,
|
|
41
41
|
},
|
|
42
|
+
worker_timeout: 60,
|
|
43
|
+
worker_boot_timeout: 60,
|
|
44
|
+
worker_shutdown_timeout: 30,
|
|
42
45
|
stats_file: "tmp/raptor.json",
|
|
43
46
|
pid_file: nil,
|
|
44
47
|
}.freeze
|
|
@@ -143,11 +146,12 @@ module Raptor
|
|
|
143
146
|
data = JSON.parse(File.read(stats_file), symbolize_names: true)
|
|
144
147
|
|
|
145
148
|
puts "Master PID: #{data[:master_pid]}"
|
|
146
|
-
data[:workers].
|
|
149
|
+
data[:workers].each do |worker|
|
|
147
150
|
status = worker[:booted] ? "booted" : "starting"
|
|
148
151
|
last_checkin = Time.at(worker[:last_checkin]).strftime("%H:%M:%S")
|
|
149
|
-
puts "Worker #{index}: pid=#{worker[:pid]}, requests=#{worker[:requests]}, " \
|
|
150
|
-
"
|
|
152
|
+
puts "Worker #{worker[:index]} (phase #{worker[:phase]}): pid=#{worker[:pid]}, requests=#{worker[:requests]}, " \
|
|
153
|
+
"busy=#{worker[:busy_threads]}/#{worker[:thread_capacity]}, backlog=#{worker[:backlog]}, " \
|
|
154
|
+
"#{status}, last_checkin=#{last_checkin}"
|
|
151
155
|
end
|
|
152
156
|
end
|
|
153
157
|
|
|
@@ -215,16 +219,16 @@ module Raptor
|
|
|
215
219
|
end
|
|
216
220
|
end
|
|
217
221
|
|
|
218
|
-
opts.on("-
|
|
219
|
-
@options[:
|
|
222
|
+
opts.on("-w", "--workers NUM", Integer, "Number of worker processes (default: #{DEFAULT_WORKER_COUNT})") do |num|
|
|
223
|
+
@options[:workers] = num
|
|
220
224
|
end
|
|
221
225
|
|
|
222
226
|
opts.on("-r", "--ractors NUM", Integer, "Number of ractors (default: 1)") do |num|
|
|
223
227
|
@options[:ractors] = num
|
|
224
228
|
end
|
|
225
229
|
|
|
226
|
-
opts.on("-
|
|
227
|
-
@options[:
|
|
230
|
+
opts.on("-t", "--threads NUM", Integer, "Number of threads (default: 3)") do |num|
|
|
231
|
+
@options[:threads] = num
|
|
228
232
|
end
|
|
229
233
|
|
|
230
234
|
opts.on("--first-data-timeout SECONDS", Integer, "First data timeout in seconds (default: 30)") do |timeout|
|
|
@@ -247,6 +251,18 @@ module Raptor
|
|
|
247
251
|
@options[:client][:body_spool_threshold] = bytes
|
|
248
252
|
end
|
|
249
253
|
|
|
254
|
+
opts.on("--worker-timeout SECONDS", Integer, "Worker check-in timeout in seconds (default: 60)") do |timeout|
|
|
255
|
+
@options[:worker_timeout] = timeout
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
opts.on("--worker-boot-timeout SECONDS", Integer, "Worker boot timeout in seconds (default: 60)") do |timeout|
|
|
259
|
+
@options[:worker_boot_timeout] = timeout
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
opts.on("--worker-shutdown-timeout SECONDS", Integer, "Worker shutdown timeout in seconds (default: 30)") do |timeout|
|
|
263
|
+
@options[:worker_shutdown_timeout] = timeout
|
|
264
|
+
end
|
|
265
|
+
|
|
250
266
|
opts.on("--stats-file PATH", String, "Stats file path (default: tmp/raptor.json)") do |path|
|
|
251
267
|
@options[:stats_file] = path
|
|
252
268
|
end
|
data/lib/raptor/cluster.rb
CHANGED
|
@@ -7,6 +7,7 @@ require "atomic-ruby/atomic_thread_pool"
|
|
|
7
7
|
require "rack/builder"
|
|
8
8
|
require "ractor-pool"
|
|
9
9
|
|
|
10
|
+
require_relative "log"
|
|
10
11
|
require_relative "binder"
|
|
11
12
|
require_relative "server"
|
|
12
13
|
require_relative "reactor"
|
|
@@ -18,14 +19,15 @@ module Raptor
|
|
|
18
19
|
# Multi-process web server cluster with advanced concurrency architecture.
|
|
19
20
|
#
|
|
20
21
|
# Cluster manages multiple worker processes, each running a complete server
|
|
21
|
-
# stack including a
|
|
22
|
-
#
|
|
23
|
-
# forking, signal management, graceful shutdown, and
|
|
24
|
-
# restart when a worker process unexpectedly exits.
|
|
22
|
+
# stack including a ractor pool for HTTP parsing, a thread pool for
|
|
23
|
+
# application processing, plus dedicated reactor and server threads. It
|
|
24
|
+
# handles process forking, signal management, graceful shutdown, and
|
|
25
|
+
# automatic worker restart when a worker process unexpectedly exits.
|
|
25
26
|
#
|
|
26
27
|
# The architecture provides horizontal scaling through processes while
|
|
27
|
-
# maintaining efficient I/O and CPU utilization within each process
|
|
28
|
-
# the combination of
|
|
28
|
+
# maintaining efficient I/O and CPU utilization within each process
|
|
29
|
+
# through the combination of ractor-based parsing and thread pools on
|
|
30
|
+
# top of NIO reactors.
|
|
29
31
|
#
|
|
30
32
|
# Flow per worker process:
|
|
31
33
|
# 1. Server continuously accepts connections but skips acceptance when backlog is high
|
|
@@ -36,7 +38,7 @@ module Raptor
|
|
|
36
38
|
#
|
|
37
39
|
# @example Basic usage
|
|
38
40
|
# options = {
|
|
39
|
-
#
|
|
41
|
+
# workers: 4, ractors: 2, threads: 8,
|
|
40
42
|
# binds: ["tcp://0.0.0.0:3000"],
|
|
41
43
|
# rackup: "config.ru",
|
|
42
44
|
# client: { first_data_timeout: 30, chunk_data_timeout: 10 }
|
|
@@ -54,50 +56,61 @@ module Raptor
|
|
|
54
56
|
new(options).run
|
|
55
57
|
end
|
|
56
58
|
|
|
57
|
-
# @rbs @thread_count: Integer
|
|
58
|
-
# @rbs @ractor_count: Integer
|
|
59
59
|
# @rbs @worker_count: Integer
|
|
60
|
+
# @rbs @ractor_count: Integer
|
|
61
|
+
# @rbs @thread_count: Integer
|
|
60
62
|
# @rbs @client_options: Hash[Symbol, Integer]
|
|
61
|
-
# @rbs @
|
|
63
|
+
# @rbs @worker_timeout: Integer
|
|
64
|
+
# @rbs @worker_boot_timeout: Integer
|
|
65
|
+
# @rbs @worker_shutdown_timeout: Integer
|
|
62
66
|
# @rbs @stats_file: String?
|
|
63
67
|
# @rbs @pid_file: String?
|
|
68
|
+
# @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
64
69
|
# @rbs @binder: Binder
|
|
65
70
|
# @rbs @server_port: Integer
|
|
66
71
|
# @rbs @app: untyped
|
|
67
72
|
# @rbs @shutdown: bool
|
|
68
73
|
# @rbs @workers: Hash[Integer, Integer]
|
|
74
|
+
# @rbs @timed_out: Set[Integer]
|
|
69
75
|
# @rbs @stats: Stats
|
|
76
|
+
# @rbs @phase: Integer
|
|
70
77
|
# @rbs @phased_restart_requested: bool
|
|
71
78
|
# @rbs @phased_restarting: bool
|
|
72
79
|
|
|
73
80
|
# Creates a new Cluster with the specified configuration.
|
|
74
81
|
#
|
|
75
|
-
# Initializes the cluster with
|
|
82
|
+
# Initializes the cluster with worker, ractor, and thread counts,
|
|
76
83
|
# sets up network binding, loads the Rack application, and prepares
|
|
77
84
|
# for multi-process operation.
|
|
78
85
|
#
|
|
79
86
|
# @param options [Hash] cluster configuration options
|
|
80
|
-
# @option options [Integer] :threads number of threads per worker process
|
|
81
|
-
# @option options [Integer] :ractors number of ractors per worker process
|
|
82
|
-
# @option options [Integer] :workers number of worker processes
|
|
83
87
|
# @option options [Array<String>] :binds array of bind URIs
|
|
88
|
+
# @option options [Integer] :workers number of worker processes
|
|
89
|
+
# @option options [Integer] :ractors number of ractors per worker process
|
|
90
|
+
# @option options [Integer] :threads number of threads per worker process
|
|
84
91
|
# @option options [#call] :app pre-built Rack application
|
|
85
92
|
# @option options [String] :rackup path to Rack configuration file
|
|
86
93
|
# @option options [Hash] :client client configuration
|
|
87
|
-
# @option options [
|
|
94
|
+
# @option options [Integer] :worker_timeout seconds to wait for a booted worker to check in before killing it
|
|
95
|
+
# @option options [Integer] :worker_boot_timeout seconds to wait for a worker to finish booting before killing it
|
|
96
|
+
# @option options [Integer] :worker_shutdown_timeout seconds to wait for graceful worker exit before force-killing
|
|
88
97
|
# @option options [String, nil] :stats_file path to write per-worker stats JSON, or nil to disable
|
|
89
98
|
# @option options [String, nil] :pid_file path to write the master PID to, or nil to disable
|
|
99
|
+
# @option options [#call] :on_error callback invoked with (env, exception) when the Rack app raises
|
|
90
100
|
# @return [void]
|
|
91
101
|
#
|
|
92
102
|
# @rbs (Hash[Symbol, untyped] options) -> void
|
|
93
103
|
def initialize(options)
|
|
94
|
-
@thread_count = options[:threads]
|
|
95
|
-
@ractor_count = options[:ractors]
|
|
96
104
|
@worker_count = options[:workers]
|
|
105
|
+
@ractor_count = options[:ractors]
|
|
106
|
+
@thread_count = options[:threads]
|
|
97
107
|
@client_options = options[:client]
|
|
98
|
-
@
|
|
108
|
+
@worker_timeout = options[:worker_timeout]
|
|
109
|
+
@worker_boot_timeout = options[:worker_boot_timeout]
|
|
110
|
+
@worker_shutdown_timeout = options[:worker_shutdown_timeout]
|
|
99
111
|
@stats_file = options[:stats_file]
|
|
100
112
|
@pid_file = options[:pid_file]
|
|
113
|
+
@on_error = options[:on_error]
|
|
101
114
|
|
|
102
115
|
@binder = Binder.new(options[:binds])
|
|
103
116
|
@server_port = @binder.server_port
|
|
@@ -106,7 +119,9 @@ module Raptor
|
|
|
106
119
|
|
|
107
120
|
@shutdown = false
|
|
108
121
|
@workers = {}
|
|
122
|
+
@timed_out = Set.new
|
|
109
123
|
@stats = Stats.new(@worker_count)
|
|
124
|
+
@phase = 0
|
|
110
125
|
@phased_restart_requested = false
|
|
111
126
|
@phased_restarting = false
|
|
112
127
|
end
|
|
@@ -114,15 +129,15 @@ module Raptor
|
|
|
114
129
|
# Starts the multi-process cluster and manages worker processes.
|
|
115
130
|
#
|
|
116
131
|
# Forks the configured number of worker processes and monitors them,
|
|
117
|
-
#
|
|
118
|
-
# shutdown via INT or TERM signals, stats logging via USR1,
|
|
119
|
-
# restart via USR2.
|
|
132
|
+
# restarting any that exit unexpectedly or stop checking in. Handles
|
|
133
|
+
# graceful shutdown via INT or TERM signals, stats logging via USR1,
|
|
134
|
+
# and phased restart via USR2.
|
|
120
135
|
#
|
|
121
136
|
# Each worker process includes:
|
|
122
137
|
# - 1 server thread (continuously accepts connections with backpressure control)
|
|
123
138
|
# - 1 reactor thread (I/O multiplexing, timeout handling, backlog monitoring)
|
|
124
|
-
# - N
|
|
125
|
-
# - 1
|
|
139
|
+
# - N pipeline ractors (parallel HTTP parsing)
|
|
140
|
+
# - 1 pipeline collector thread (coordinates parsing results)
|
|
126
141
|
# - M worker threads (Rack application processing and response writing)
|
|
127
142
|
# - 1 stats thread (writes per-worker metrics to shared memory every second)
|
|
128
143
|
#
|
|
@@ -151,12 +166,12 @@ module Raptor
|
|
|
151
166
|
break if reap_workers == :no_children
|
|
152
167
|
|
|
153
168
|
perform_phased_restart if @phased_restart_requested && !@phased_restarting
|
|
169
|
+
timeout_hung_workers
|
|
154
170
|
|
|
155
171
|
sleep 0.1
|
|
156
172
|
end
|
|
157
173
|
|
|
158
|
-
|
|
159
|
-
@workers.values.each { |pid| Process.wait(pid) rescue nil }
|
|
174
|
+
stop_workers
|
|
160
175
|
stats_file_thread&.join
|
|
161
176
|
File.delete(@stats_file) rescue nil if @stats_file
|
|
162
177
|
File.delete(@pid_file) rescue nil if @pid_file
|
|
@@ -176,13 +191,14 @@ module Raptor
|
|
|
176
191
|
private
|
|
177
192
|
|
|
178
193
|
# Forks a new worker process and registers it at the given index.
|
|
194
|
+
# The worker inherits the cluster's current phase.
|
|
179
195
|
#
|
|
180
196
|
# @param index [Integer] slot index for this worker in the stats region
|
|
181
197
|
# @return [void]
|
|
182
198
|
#
|
|
183
199
|
# @rbs (Integer index) -> void
|
|
184
200
|
def spawn_worker(index)
|
|
185
|
-
pid = fork { run_worker(index) }
|
|
201
|
+
pid = fork { run_worker(index, @phase) }
|
|
186
202
|
@workers[index] = pid
|
|
187
203
|
end
|
|
188
204
|
|
|
@@ -199,9 +215,10 @@ module Raptor
|
|
|
199
215
|
|
|
200
216
|
index = @workers.key(pid)
|
|
201
217
|
@workers.delete(index)
|
|
218
|
+
@timed_out.delete(pid)
|
|
202
219
|
|
|
203
220
|
unless @shutdown
|
|
204
|
-
warn "
|
|
221
|
+
Log.warn "Restarting worker #{index} (#{pid}), #{exit_description(status)}"
|
|
205
222
|
spawn_worker(index)
|
|
206
223
|
end
|
|
207
224
|
end
|
|
@@ -209,6 +226,57 @@ module Raptor
|
|
|
209
226
|
:no_children
|
|
210
227
|
end
|
|
211
228
|
|
|
229
|
+
# Stops every worker, escalating from TERM to KILL if any fail to
|
|
230
|
+
# exit within `worker_shutdown_timeout`.
|
|
231
|
+
#
|
|
232
|
+
# @return [void]
|
|
233
|
+
#
|
|
234
|
+
# @rbs () -> void
|
|
235
|
+
def stop_workers
|
|
236
|
+
@workers.values.each { |pid| Process.kill("TERM", pid) rescue nil }
|
|
237
|
+
|
|
238
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @worker_shutdown_timeout
|
|
239
|
+
until @workers.empty? || Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
240
|
+
reap_workers
|
|
241
|
+
sleep 0.05
|
|
242
|
+
end
|
|
243
|
+
return if @workers.empty?
|
|
244
|
+
|
|
245
|
+
Log.warn "Force-killing #{@workers.size} worker(s) after #{@worker_shutdown_timeout}s"
|
|
246
|
+
@workers.values.each { |pid| Process.kill("KILL", pid) rescue nil }
|
|
247
|
+
@workers.values.each { |pid| Process.wait(pid) rescue nil }
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Kills workers that have stopped checking in. A booted worker that
|
|
251
|
+
# fails to update its stats slot within `worker_timeout` seconds is
|
|
252
|
+
# assumed to be hung (deadlocked app, runaway loop, blocked syscall);
|
|
253
|
+
# a worker still in startup is held to `worker_boot_timeout`. Killed
|
|
254
|
+
# workers are then restarted by `reap_workers`.
|
|
255
|
+
#
|
|
256
|
+
# @return [void]
|
|
257
|
+
#
|
|
258
|
+
# @rbs () -> void
|
|
259
|
+
def timeout_hung_workers
|
|
260
|
+
now = Process.clock_gettime(Process::CLOCK_REALTIME)
|
|
261
|
+
stats = @stats.all
|
|
262
|
+
|
|
263
|
+
@workers.each do |index, pid|
|
|
264
|
+
next if @timed_out.include?(pid)
|
|
265
|
+
|
|
266
|
+
stat = stats[index]
|
|
267
|
+
next unless stat[:pid] == pid
|
|
268
|
+
|
|
269
|
+
timeout = stat[:booted] ? @worker_timeout : @worker_boot_timeout
|
|
270
|
+
elapsed = now - stat[:last_checkin]
|
|
271
|
+
next if elapsed <= timeout
|
|
272
|
+
|
|
273
|
+
action = stat[:booted] ? "check in" : "boot"
|
|
274
|
+
Log.warn "Killing worker #{index} (#{pid}), failed to #{action} within #{timeout}s"
|
|
275
|
+
Process.kill("KILL", pid) rescue nil
|
|
276
|
+
@timed_out << pid
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
212
280
|
# Replaces each worker process one at a time, waiting for the new
|
|
213
281
|
# worker to boot before moving on to the next. Triggered by SIGUSR2.
|
|
214
282
|
#
|
|
@@ -218,7 +286,8 @@ module Raptor
|
|
|
218
286
|
def perform_phased_restart
|
|
219
287
|
@phased_restart_requested = false
|
|
220
288
|
@phased_restarting = true
|
|
221
|
-
|
|
289
|
+
@phase += 1
|
|
290
|
+
Log.info "Phased restart starting"
|
|
222
291
|
|
|
223
292
|
begin
|
|
224
293
|
@workers.keys.sort.each do |index|
|
|
@@ -240,7 +309,7 @@ module Raptor
|
|
|
240
309
|
end
|
|
241
310
|
end
|
|
242
311
|
|
|
243
|
-
|
|
312
|
+
Log.info "Phased restart complete"
|
|
244
313
|
ensure
|
|
245
314
|
@phased_restarting = false
|
|
246
315
|
end
|
|
@@ -253,10 +322,11 @@ module Raptor
|
|
|
253
322
|
# critical component fails.
|
|
254
323
|
#
|
|
255
324
|
# @param index [Integer] slot index for this worker in the stats region
|
|
325
|
+
# @param phase [Integer] the cluster phase this worker was forked at
|
|
256
326
|
# @return [void]
|
|
257
327
|
#
|
|
258
|
-
# @rbs (Integer index) -> void
|
|
259
|
-
def run_worker(index)
|
|
328
|
+
# @rbs (Integer index, Integer phase) -> void
|
|
329
|
+
def run_worker(index, phase)
|
|
260
330
|
shutdown_requested = false
|
|
261
331
|
trap("INT") { shutdown_requested = true }
|
|
262
332
|
trap("TERM") { shutdown_requested = true }
|
|
@@ -267,8 +337,11 @@ module Raptor
|
|
|
267
337
|
@stats.write(
|
|
268
338
|
index,
|
|
269
339
|
pid: Process.pid,
|
|
340
|
+
phase: phase,
|
|
270
341
|
requests: 0,
|
|
271
342
|
backlog: 0,
|
|
343
|
+
busy_threads: 0,
|
|
344
|
+
thread_capacity: @thread_count,
|
|
272
345
|
started_at:,
|
|
273
346
|
last_checkin: started_at,
|
|
274
347
|
booted: false
|
|
@@ -295,18 +368,17 @@ module Raptor
|
|
|
295
368
|
request.handle_parsed_request(parsed_result, reactor, thread_pool)
|
|
296
369
|
end
|
|
297
370
|
rescue => error
|
|
298
|
-
|
|
299
|
-
warn error.full_message
|
|
371
|
+
Log.rescued_error(error)
|
|
300
372
|
end
|
|
301
373
|
end
|
|
302
374
|
|
|
303
|
-
reactor = Reactor.new(
|
|
375
|
+
reactor = Reactor.new(ractor_pool, thread_pool, client_options: @client_options)
|
|
304
376
|
reactor_thread = reactor.run
|
|
305
377
|
|
|
306
378
|
server = Server.new(@binder, reactor, thread_pool, request, client_options: @client_options)
|
|
307
379
|
server_thread = server.run
|
|
308
380
|
|
|
309
|
-
|
|
381
|
+
Log.info "Worker #{index} booted"
|
|
310
382
|
|
|
311
383
|
stats_thread = Thread.new do
|
|
312
384
|
Thread.current.name = "Raptor Stats"
|
|
@@ -315,8 +387,11 @@ module Raptor
|
|
|
315
387
|
@stats.write(
|
|
316
388
|
index,
|
|
317
389
|
pid: Process.pid,
|
|
390
|
+
phase: phase,
|
|
318
391
|
requests: request_count,
|
|
319
392
|
backlog: reactor.backlog,
|
|
393
|
+
busy_threads: thread_pool.active_count,
|
|
394
|
+
thread_capacity: @thread_count,
|
|
320
395
|
started_at:,
|
|
321
396
|
last_checkin: Process.clock_gettime(Process::CLOCK_REALTIME),
|
|
322
397
|
booted: true
|
|
@@ -370,28 +445,25 @@ module Raptor
|
|
|
370
445
|
@shutdown = true
|
|
371
446
|
end
|
|
372
447
|
|
|
373
|
-
#
|
|
374
|
-
#
|
|
375
|
-
# Outputs a hierarchical view of the cluster configuration showing
|
|
376
|
-
# the master process, worker processes, and per-process thread/ractor
|
|
377
|
-
# allocation along with listening addresses.
|
|
448
|
+
# Prints the cluster's startup banner showing process structure
|
|
449
|
+
# and bind addresses.
|
|
378
450
|
#
|
|
379
451
|
# @return [void]
|
|
380
452
|
#
|
|
381
453
|
# @rbs () -> void
|
|
382
454
|
def log_initialization
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
455
|
+
Log.info "Cluster initializing:"
|
|
456
|
+
Log.info "├─ Version: #{VERSION}"
|
|
457
|
+
Log.info "├─ Ruby Version: #{RUBY_DESCRIPTION}"
|
|
458
|
+
Log.info "├─ Master PID: #{Process.pid}"
|
|
459
|
+
Log.info "│ └─ #{@worker_count} worker process#{"es" if @worker_count > 1}"
|
|
460
|
+
Log.info "│ ├─ 1 server thread"
|
|
461
|
+
Log.info "│ ├─ 1 reactor thread"
|
|
462
|
+
Log.info "│ ├─ #{@ractor_count} pipeline ractor#{"s" if @ractor_count > 1}"
|
|
463
|
+
Log.info "│ ├─ 1 pipeline collector thread"
|
|
464
|
+
Log.info "│ ├─ #{@thread_count} worker thread#{"s" if @thread_count > 1}"
|
|
465
|
+
Log.info "│ └─ 1 stats thread"
|
|
466
|
+
Log.info "└─ Listening on #{@binder.addresses.join(", ")}"
|
|
395
467
|
end
|
|
396
468
|
|
|
397
469
|
# Logs current stats for all workers to stdout.
|
|
@@ -402,11 +474,11 @@ module Raptor
|
|
|
402
474
|
#
|
|
403
475
|
# @rbs () -> void
|
|
404
476
|
def log_stats
|
|
405
|
-
@stats.all.
|
|
477
|
+
@stats.all.each do |stat|
|
|
406
478
|
status = stat[:booted] ? "booted" : "starting"
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
479
|
+
Log.info "Worker #{stat[:index]} (phase #{stat[:phase]}): pid=#{stat[:pid]}, requests=#{stat[:requests]}, " \
|
|
480
|
+
"busy=#{stat[:busy_threads]}/#{stat[:thread_capacity]}, backlog=#{stat[:backlog]}, " \
|
|
481
|
+
"#{status}, last_checkin=#{Time.at(stat[:last_checkin]).strftime("%H:%M:%S")}"
|
|
410
482
|
end
|
|
411
483
|
end
|
|
412
484
|
|
data/lib/raptor/http2.rb
CHANGED
|
@@ -81,7 +81,8 @@ module Raptor
|
|
|
81
81
|
# outbound DATA frames respect RFC 7540 §5.2. Threads dispatching stream
|
|
82
82
|
# responses call `acquire` to reserve send capacity; threads applying
|
|
83
83
|
# inbound WINDOW_UPDATE or SETTINGS frames call the mutating methods to
|
|
84
|
-
# replenish it.
|
|
84
|
+
# replenish it. The connection window and per-stream windows live in
|
|
85
|
+
# separate `Atom`s so the common fast path skips per-stream tracking.
|
|
85
86
|
#
|
|
86
87
|
class FlowControl
|
|
87
88
|
ACQUIRE_POLL_INTERVAL = 0.001
|
|
@@ -529,7 +530,7 @@ module Raptor
|
|
|
529
530
|
|
|
530
531
|
result[:completed_requests]&.each do |request|
|
|
531
532
|
stream_id = request[:stream_id]
|
|
532
|
-
remote_addr = result[:remote_addr] ||
|
|
533
|
+
remote_addr = result[:remote_addr] || Server::DEFAULT_REMOTE_ADDR
|
|
533
534
|
|
|
534
535
|
thread_pool << proc do
|
|
535
536
|
dispatch_stream_request(
|
|
@@ -749,7 +750,7 @@ module Raptor
|
|
|
749
750
|
env[Rack::SERVER_NAME] ||= host
|
|
750
751
|
env[Rack::SERVER_PORT] ||= port || @server_port.to_s
|
|
751
752
|
else
|
|
752
|
-
env[Rack::SERVER_NAME] ||=
|
|
753
|
+
env[Rack::SERVER_NAME] ||= Server::DEFAULT_SERVER_NAME
|
|
753
754
|
env[Rack::SERVER_PORT] ||= @server_port.to_s
|
|
754
755
|
end
|
|
755
756
|
end
|