raptor 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c247b4c221d0f19ff1383ca0a4092a54bc12cd2f945655fa24e1ab49745eb70
4
- data.tar.gz: cb932a7a048d87eb0913f8c0dbe52750ea4e525e4c4dc32e5fcc531af0daaf41
3
+ metadata.gz: c20a98b3c581180e2b9ea2e9cdd23223f4f43438b94852b0b19b8e1b659c751d
4
+ data.tar.gz: bc98ee07befe42ad9c2e30b4645aeac4a1fb369cc598e360a336b591a56d74b1
5
5
  SHA512:
6
- metadata.gz: d8e1543054862e8a87c7261884348456da13d4432c3424687b74f506b884283d2773068b6beb10cbe9e209e969b98a918720861b3d0e05bc04d930ba38ec1921
7
- data.tar.gz: f215cc4a92af86542d49f2c4772f0a65af09988779bbb1c44512ab075d4fb5c0758cb51d9765a0ee9d08fe340d31b42cea1610b021c443818fe40fbdcea63b93
6
+ metadata.gz: b065827d528422e5476827c2c01b792cd9b4a83ba38484283c33f05459a744a28a7ce88bfbe396c80fe07209893662350f205dd823a469503be208428f0cc784
7
+ data.tar.gz: '02583bea8f88760256318b3714b9f95cb7936b12ec0d9bb54a120108a734f965a9b85d4e556e6b8e73d7c731a43be6ddf96bd4871228f35d7bf2903df255cc84'
data/.mise.toml ADDED
@@ -0,0 +1,2 @@
1
+ [tools]
2
+ ruby = "latest"
data/Brewfile ADDED
@@ -0,0 +1,2 @@
1
+ brew "hey"
2
+ brew "nghttp2"
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
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
+
11
+ ## [0.4.0] - 2026-05-29
12
+
13
+ - Load `raptor.rb` or `config/raptor.rb` by default when no config path is supplied
14
+ - Honour the peer's HTTP/2 flow-control windows when sending `DATA` frames
15
+ - Assemble HEADERS across `CONTINUATION` frames
16
+ - Validate HTTP/2 stream IDs and emit `GOAWAY` on protocol errors
17
+ - Offload TLS handshakes to the thread pool to keep the server thread responsive
18
+ - Exit eager keep-alive loops on cluster shutdown
19
+ - Apply the write timeout to HTTP/2 frame writes
20
+ - Reject HPACK dynamic table size updates larger than 4096 bytes
21
+ - Reject malformed HTTP/1.1 requests with a 400 response
22
+ - Rescue unexpected errors in the reactor and pipeline collector
23
+
3
24
  ## [0.3.0] - 2026-05-25
4
25
 
5
26
  - Load cluster options from a Ruby config file via `--config`
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Raptor
2
2
 
3
- Raptor is a high-performance, preloading, multi-process, multi-threaded Ruby 4+ web server implementing Rack 3+,
3
+ Raptor is a high-performance, preloading, multi-process, multi-threaded Ruby 4+ web server implementing Rack 3.2+,
4
4
  leveraging Ractors for parallel HTTP/1.1 and HTTP/2 request processing, native C extensions for HTTP parsing and HPACK
5
5
  compression, and NIO for non-blocking I/O.
6
6
 
@@ -29,23 +29,23 @@ run proc { |_env| [200, { "content-type" => "text/plain" }, ["Hello, World!"]] }
29
29
  ```
30
30
 
31
31
  ```
32
- > bundle exec raptor -t 3 -w 4 hello_world.ru
33
- Raptor Cluster initializing:
34
- ├─ Version: 0.3.0
35
- ├─ Ruby Version: ruby 4.0.4 (2026-05-12 revision b89eb1bcbf) +YJIT +PRISM [arm64-darwin23]
36
- ├─ Master PID: 31504
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
- [31506] Worker 0 booted
46
- [31507] Worker 1 booted
47
- [31508] Worker 2 booted
48
- [31509] Worker 3 booted
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.3.0 vs Puma 8.0.1:
65
+ Raptor 0.5.0 vs Puma 8.0.2:
66
66
 
67
- | Protocol | Raptor | Puma |
68
- | --------------------- | ------------ | ------------ |
69
- | HTTP/1.1 | 20.3k req/s | 20.8k req/s |
70
- | HTTP/1.1 (keep-alive) | 60.9k req/s | 45.4k req/s |
71
- | HTTP/2 | 22.9k req/s | N/A |
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
- > Ruby 4.0.4 +YJIT, macOS Apple Silicon. 4 workers, 3 threads, 12 concurrent connections.
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
 
@@ -457,6 +457,7 @@ static int hpack_decode_header_block(const uint8_t *buf, size_t len,
457
457
  /* dynamic table size update (RFC 7541 6.3) */
458
458
  uint64_t new_size;
459
459
  if (hpack_decode_int(buf, len, &pos, 5, &new_size) < 0) return -1;
460
+ if (new_size > HTTP2_DEFAULT_HEADER_TABLE_SIZE) return -1;
460
461
  *max_table_size = (long)new_size;
461
462
  *dyn_table = dynamic_table_evict(*dyn_table, *max_table_size);
462
463
 
@@ -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
@@ -69,29 +69,28 @@ module Rackup
69
69
  end
70
70
 
71
71
  cli_defaults = ::Raptor::CLI::DEFAULT_OPTIONS
72
- config = options[:Config] ? ::Raptor::CLI.load_config_file(options[:Config]) : {}
73
-
74
- binds = if options[:Host] || options[:Port]
75
- host = options[:Host] || defaults[:Host]
76
- port = options[:Port] || defaults[:Port]
77
- ["tcp://#{host}:#{port}"]
78
- else
79
- config[:binds] || ["tcp://#{defaults[:Host]}:#{defaults[:Port]}"]
80
- end
72
+ config_path = options[:Config] || ::Raptor::CLI.default_config_path
73
+ config = config_path ? ::Raptor::CLI.load_config_file(config_path) : {}
81
74
 
82
75
  result = {
83
- app: app,
84
- binds: binds,
85
- threads: (options[:Threads] || config[:threads] || cli_defaults[:threads]).to_i,
86
- ractors: (options[:Ractors] || config[:ractors] || cli_defaults[:ractors]).to_i,
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,
87
81
  workers: (options[:Workers] || config[:workers] || Etc.nprocessors).to_i,
88
- client: cli_defaults[:client].merge(config[:client] || {})
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
89
85
  }
90
-
91
- [:rackup, :on_error, :stats_file, :pidfile].each do |key|
92
- result[key] = config[key] if config.key?(key)
93
- end
94
-
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)
95
94
  result
96
95
  end
97
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, threads, ractors, bind addresses, and various client timeout
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,10 +39,15 @@ 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
- pidfile: nil,
46
+ pid_file: nil,
44
47
  }.freeze
45
48
 
49
+ DEFAULT_CONFIG_PATHS = ["raptor.rb", "config/raptor.rb"].freeze
50
+
46
51
  # Loads a configuration file and returns the hash it evaluates to.
47
52
  #
48
53
  # The file is evaluated at the top level so constants like `Raptor::*` resolve
@@ -61,6 +66,20 @@ module Raptor
61
66
  config
62
67
  end
63
68
 
69
+ # Returns the first existing path in {DEFAULT_CONFIG_PATHS} resolved
70
+ # against `root`, or nil if none exist.
71
+ #
72
+ # Used to pick up a project-local config file when no `-c`/`--config`
73
+ # flag was supplied.
74
+ #
75
+ # @param root [String] directory to resolve the default paths against
76
+ # @return [String, nil] the config path, or nil if no default file exists
77
+ #
78
+ # @rbs (?String root) -> String?
79
+ def self.default_config_path(root = Dir.pwd)
80
+ DEFAULT_CONFIG_PATHS.find { |path| File.exist?(File.join(root, path)) }
81
+ end
82
+
64
83
  # @rbs @command: Symbol
65
84
  # @rbs @options: Hash[Symbol, untyped]
66
85
  # @rbs @parser: OptionParser
@@ -92,7 +111,7 @@ module Raptor
92
111
  @options = DEFAULT_OPTIONS.dup
93
112
  @options[:client] = @options[:client].dup
94
113
 
95
- apply_config_file(extract_config_path(argv))
114
+ apply_config_file(extract_config_path(argv) || self.class.default_config_path)
96
115
 
97
116
  @parser = create_parser
98
117
  @parser.parse!(argv)
@@ -127,11 +146,12 @@ module Raptor
127
146
  data = JSON.parse(File.read(stats_file), symbolize_names: true)
128
147
 
129
148
  puts "Master PID: #{data[:master_pid]}"
130
- data[:workers].each_with_index do |worker, index|
149
+ data[:workers].each do |worker|
131
150
  status = worker[:booted] ? "booted" : "starting"
132
151
  last_checkin = Time.at(worker[:last_checkin]).strftime("%H:%M:%S")
133
- puts "Worker #{index}: pid=#{worker[:pid]}, requests=#{worker[:requests]}, " \
134
- "backlog=#{worker[:backlog]}, #{status}, last_checkin=#{last_checkin}"
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}"
135
155
  end
136
156
  end
137
157
 
@@ -199,16 +219,16 @@ module Raptor
199
219
  end
200
220
  end
201
221
 
202
- opts.on("-t", "--threads NUM", Integer, "Number of threads (default: 3)") do |num|
203
- @options[:threads] = num
222
+ opts.on("-w", "--workers NUM", Integer, "Number of worker processes (default: #{DEFAULT_WORKER_COUNT})") do |num|
223
+ @options[:workers] = num
204
224
  end
205
225
 
206
226
  opts.on("-r", "--ractors NUM", Integer, "Number of ractors (default: 1)") do |num|
207
227
  @options[:ractors] = num
208
228
  end
209
229
 
210
- opts.on("-w", "--workers NUM", Integer, "Number of worker processes (default: #{DEFAULT_WORKER_COUNT})") do |num|
211
- @options[:workers] = num
230
+ opts.on("-t", "--threads NUM", Integer, "Number of threads (default: 3)") do |num|
231
+ @options[:threads] = num
212
232
  end
213
233
 
214
234
  opts.on("--first-data-timeout SECONDS", Integer, "First data timeout in seconds (default: 30)") do |timeout|
@@ -231,12 +251,24 @@ module Raptor
231
251
  @options[:client][:body_spool_threshold] = bytes
232
252
  end
233
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
+
234
266
  opts.on("--stats-file PATH", String, "Stats file path (default: tmp/raptor.json)") do |path|
235
267
  @options[:stats_file] = path
236
268
  end
237
269
 
238
- opts.on("--pidfile PATH", String, "Pidfile path (default: none)") do |path|
239
- @options[:pidfile] = path
270
+ opts.on("--pid-file PATH", String, "PID file path (default: none)") do |path|
271
+ @options[:pid_file] = path
240
272
  end
241
273
 
242
274
  opts.on("--help", "Show this help") do