raptor 0.26.0 → 0.27.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: 01e94b50f111d86ede3add1b2ff22fe674c4b2da50d748fc81728ac56458d9c4
4
- data.tar.gz: cff8caa5355ce8f7b4e1d7fb7ec1ae17e8fe5332852b57042831d9a10921276f
3
+ metadata.gz: 6f72ac9941a9da99f3fe49f3464ac925ff6304cf196836502a480220eb99d48f
4
+ data.tar.gz: 7a72e415e3663c54ac3c4f2fcfd13481356b3357693298c082f2b2eda3727182
5
5
  SHA512:
6
- metadata.gz: a9d7fd70b2cea77c2d6b1b232542a9a70d8d4f7a40a88870128af620e5125e24d4578e77885dcbd1df9fc750785547a8a71f87791381358a693a3ee382defdf9
7
- data.tar.gz: ea9c5cf5e367b17e47bdaeb8e927d5c2214a7d739d6a646b3162c808b3bbc3f82c026e3632a8846f4f978069add42f34cc3727415a8b90a9a321eb20dea93147
6
+ metadata.gz: bf330c4d82552ee29b56b4bf0998355d3faad012147a5d34b9ade39d1f333ef4cc46b9d0e81c9b96516462fa920c3efe0c849fb716cdb158261cc3a6aff5014c
7
+ data.tar.gz: 9ff03e07f24109d0c1a0af66c309b6a49fa8f45c6db0aecd0a1c8507082d2e558a67bf72c01bf6c59f83c2aa379a41e8046f2b91f2d17deb8ecd59c5e44b162f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.27.0] - 2026-11-10
4
+
5
+ - Support master-owned Rack listeners across worker forks
6
+ - Add `after_worker_exit` hooks
7
+ - Add `after_worker_drain` hooks
8
+
3
9
  ## [0.26.0] - 2026-11-10
4
10
 
5
11
  - Add in-process Rack serving
data/README.md CHANGED
@@ -37,7 +37,7 @@ run proc { |_env| [200, { "content-type" => "text/plain" }, ["Hello, World!"]] }
37
37
  ```
38
38
  > bundle exec raptor -w 10 -t 3 hello_world.ru
39
39
  [Raptor 39704|Main|Main] Cluster initializing:
40
- [Raptor 39704|Main|Main] ├─ Version: 0.26.0
40
+ [Raptor 39704|Main|Main] ├─ Version: 0.27.0
41
41
  [Raptor 39704|Main|Main] ├─ Ruby Version: ruby 4.0.9 (2026-10-23 revision 3e93b23afc) +YJIT +PRISM [arm64-darwin25]
42
42
  [Raptor 39704|Main|Main] ├─ Environment: development
43
43
  [Raptor 39704|Main|Main] ├─ Master PID: 39704
@@ -148,8 +148,11 @@ working directory. Pass `-c PATH` to point at a specific file.
148
148
  worker_shutdown_timeout: 30,
149
149
  refork_after: 1000, # `nil` on non-Linux
150
150
  before_fork: [],
151
+ after_fork: [],
151
152
  before_worker_boot: [],
152
153
  before_worker_shutdown: [],
154
+ after_worker_drain: [],
155
+ after_worker_exit: [],
153
156
  before_refork: [],
154
157
  plugins: [],
155
158
  stats_file: "tmp/raptor.json",
@@ -191,21 +194,9 @@ Raptor::Plugin.create(:stats_logger) do
191
194
  end
192
195
  ```
193
196
 
194
- For an existing class, inherit from `Raptor::Plugin` and register it with
195
- `Raptor::Plugin.register(:stats_logger, MyStatsLogger)` instead.
196
-
197
- `start(cluster)` runs once before the initial workers fork. `cluster.options` exposes configuration, including
198
- plugin-specific keys. Append callbacks to its `before_fork`, `before_worker_boot`, `before_worker_shutdown`, and
199
- `before_refork` arrays during startup. If your plugin opens a listener, close the worker's inherited copy with
200
- `cluster.options[:before_worker_boot] << proc { @listener.close }` so only the master serves it.
201
-
202
- `cluster.stats` returns per-worker snapshots. `cluster.stats_hash` returns the same aggregate structure as `GET /stats`,
203
- including boot status, busy threads, capacity, backlog, and request counts. Both remain available while workers drain.
204
-
205
- Override `stop` to close listeners and flush exporters. Raptor calls it after workers exit, in reverse plugin order,
206
- then stops and joins threads created with `in_background`. These callbacks also run before hot restart and when startup
207
- fails, so `stop` must handle partially initialized resources. Worker replacement and phased restart keep the same
208
- master plugin instances running.
197
+ `cluster.options` exposes configuration and lifecycle hooks. `cluster.stats` and `cluster.stats_hash` provide worker
198
+ and aggregate statistics. Override `stop` to release plugin resources after workers exit, including during hot restart.
199
+ See the [API documentation](https://joshuay03.github.io/raptor/Raptor/Plugin.html) for lifecycle details.
209
200
 
210
201
  ## Bindings
211
202
 
@@ -274,7 +265,7 @@ Set `control_url` to a Unix socket URL such as `unix:///tmp/raptor-control.sock`
274
265
 
275
266
  ## (Micro) Benchmarks
276
267
 
277
- Raptor 0.26.0 vs Puma 8.0.2 vs Falcon 0.57.0. Each case has 3 fresh-process samples, interleaved in seeded shuffled
268
+ Raptor 0.27.0 vs Puma 8.0.2 vs Falcon 0.57.0. Each case has 3 fresh-process samples, interleaved in seeded shuffled
278
269
  rounds. Each sample warms up for 3s and measures for 15s. Metrics are independent medians. ± is (max − min) / 2 /
279
270
  median, not a confidence interval. Failure counts below each table sum all samples, so a failed run cannot disappear in
280
271
  a median. Throughput counts successful ordinary requests only.
@@ -302,90 +293,90 @@ accounting for shared pages instead of counting them fully in every worker.
302
293
 
303
294
  | Server | Mode | Raptor baseline | req/s | vs Raptor req/s | p95 ms | vs Raptor p95 | CPU ms/req | PSS MiB |
304
295
  | ------ | -------------- | --------------- | ------------ | --------------- | ------ | ------------- | ---------- | ------- |
305
- | Raptor | Fixed (3) | - | 3.39k ±0.6% | - | 46.17 | - | 2.434 | 337.1 |
306
- | Raptor | Scaling (3–16) | - | 8.18k ±1.3% | - | 22.19 | - | 0.467 | 325.6 |
307
- | Puma | Fixed (3) | Fixed (3) | 1.52k ±1.1% | 55.2% lower | 117.49 | 154.5% higher | 0.996 | 272.0 |
308
- | Puma | Scaling (3–16) | Scaling (3–16) | 8.14k ±1.3% | 0.4% lower | 22.37 | 0.8% higher | 0.473 | 318.8 |
309
- | Falcon | Fibers | Fixed (3) | 10.99k ±1.2% | 224.1% higher | 15.60 | 66.2% lower | 0.440 | 447.2 |
310
- | Falcon | Fibers | Scaling (3–16) | 10.99k ±1.2% | 34.3% higher | 15.60 | 29.7% lower | 0.440 | 447.2 |
296
+ | Raptor | Fixed (3) | - | 3.35k ±1.2% | - | 49.00 | - | 2.421 | 337.8 |
297
+ | Raptor | Scaling (3–16) | - | 8.21k ±0.6% | - | 22.09 | - | 0.468 | 323.6 |
298
+ | Puma | Fixed (3) | Fixed (3) | 1.53k ±0.7% | 54.4% lower | 120.94 | 146.8% higher | 0.969 | 272.0 |
299
+ | Puma | Scaling (3–16) | Scaling (3–16) | 8.15k ±0.5% | 0.7% lower | 22.23 | 0.6% higher | 0.467 | 319.5 |
300
+ | Falcon | Fibers | Fixed (3) | 11.07k ±0.5% | 230.3% higher | 15.44 | 68.5% lower | 0.439 | 446.6 |
301
+ | Falcon | Fibers | Scaling (3–16) | 11.07k ±0.5% | 34.8% higher | 15.44 | 30.1% lower | 0.439 | 446.6 |
311
302
 
312
303
  ### IO over HTTP/1.1 keep-alive
313
304
 
314
305
  | Server | Mode | Raptor baseline | req/s | vs Raptor req/s | p95 ms | vs Raptor p95 | CPU ms/req | PSS MiB |
315
306
  | ------ | -------------- | --------------- | ----------- | --------------- | ------ | ------------- | ---------- | ------- |
316
- | Raptor | Fixed (3) | - | 3.37k ±1.8% | - | 37.11 | - | 2.460 | 353.2 |
317
- | Raptor | Scaling (3–16) | - | 7.39k ±1.7% | - | 24.53 | - | 0.470 | 344.9 |
318
- | Raptor | Fixed (16) | - | 7.49k ±1.6% | - | 24.24 | - | 0.455 | 341.6 |
319
- | Puma | Fixed (3) | Fixed (3) | 1.48k ±0.2% | 56.2% lower | 106.41 | 186.8% higher | 0.979 | 282.1 |
320
- | Puma | Scaling (3–16) | Scaling (3–16) | 7.58k ±1.1% | 2.5% higher | 24.00 | 2.2% lower | 0.435 | 310.5 |
321
- | Puma | Fixed (16) | Fixed (16) | 7.59k ±0.9% | 1.4% higher | 23.97 | 1.1% lower | 0.437 | 316.5 |
322
- | Falcon | Fibers | Fixed (3) | 7.31k ±1.5% | 116.9% higher | 24.86 | 33.0% lower | 0.380 | 336.6 |
323
- | Falcon | Fibers | Scaling (3–16) | 7.31k ±1.5% | 1.1% lower | 24.86 | 1.4% higher | 0.380 | 336.6 |
324
- | Falcon | Fibers | Fixed (16) | 7.31k ±1.5% | 2.3% lower | 24.86 | 2.6% higher | 0.380 | 336.6 |
307
+ | Raptor | Fixed (3) | - | 3.09k ±6.5% | - | 51.42 | - | 2.346 | 348.9 |
308
+ | Raptor | Scaling (3–16) | - | 7.39k ±0.6% | - | 24.67 | - | 0.465 | 344.2 |
309
+ | Raptor | Fixed (16) | - | 7.48k ±0.5% | - | 24.30 | - | 0.453 | 344.1 |
310
+ | Puma | Fixed (3) | Fixed (3) | 1.49k ±0.4% | 51.7% lower | 100.59 | 95.6% higher | 1.009 | 282.5 |
311
+ | Puma | Scaling (3–16) | Scaling (3–16) | 7.39k ±0.4% | 0.0% higher | 24.58 | 0.4% lower | 0.434 | 310.0 |
312
+ | Puma | Fixed (16) | Fixed (16) | 7.52k ±0.4% | 0.5% higher | 24.16 | 0.6% lower | 0.428 | 316.3 |
313
+ | Falcon | Fibers | Fixed (3) | 7.34k ±2.1% | 137.4% higher | 24.78 | 51.8% lower | 0.382 | 329.6 |
314
+ | Falcon | Fibers | Scaling (3–16) | 7.34k ±2.1% | 0.7% lower | 24.78 | 0.4% higher | 0.382 | 329.6 |
315
+ | Falcon | Fibers | Fixed (16) | 7.34k ±2.1% | 1.9% lower | 24.78 | 2.0% higher | 0.382 | 329.6 |
325
316
 
326
317
  ### IO over HTTP/2
327
318
 
328
- | Server | Mode | Raptor baseline | req/s | vs Raptor req/s | p95 ms | vs Raptor p95 | CPU ms/req | PSS MiB |
329
- | ------ | -------------- | --------------- | ------------ | --------------- | ------ | ------------- | ---------- | ------- |
330
- | Raptor | Fixed (3) | - | 1.61k ±4.7% | - | 120.85 | - | 0.815 | 399.9 |
331
- | Raptor | Scaling (3–16) | - | 6.80k ±22.8% | - | 40.32 | - | 0.580 | 912.4 |
332
- | Falcon | Fibers | Fixed (3) | 7.12k ±3.5% | 342.8% higher | 26.17 | 78.3% lower | 0.432 | 539.7 |
333
- | Falcon | Fibers | Scaling (3–16) | 7.12k ±3.5% | 4.7% higher | 26.17 | 35.1% lower | 0.432 | 539.7 |
319
+ | Server | Mode | Raptor baseline | req/s | vs Raptor req/s | p95 ms | vs Raptor p95 | CPU ms/req | PSS MiB |
320
+ | ------ | -------------- | --------------- | ----------- | --------------- | ------ | ------------- | ---------- | ------- |
321
+ | Raptor | Fixed (3) | - | 1.61k ±0.3% | - | 131.78 | - | 0.901 | 410.2 |
322
+ | Raptor | Scaling (3–16) | - | 7.24k ±8.6% | - | 30.21 | - | 0.563 | 940.4 |
323
+ | Falcon | Fibers | Fixed (3) | 7.82k ±1.7% | 386.4% higher | 25.75 | 80.5% lower | 0.426 | 526.6 |
324
+ | Falcon | Fibers | Scaling (3–16) | 7.82k ±1.7% | 8.0% higher | 25.75 | 14.7% lower | 0.426 | 526.6 |
334
325
 
335
326
  ### CPU over HTTP/1.1
336
327
 
337
328
  | Server | Mode | Raptor baseline | req/s | vs Raptor req/s | p95 ms | vs Raptor p95 | CPU ms/req | PSS MiB |
338
329
  | ------ | -------------- | --------------- | ----------- | --------------- | ------ | ------------- | ---------- | ------- |
339
- | Raptor | Fixed (3) | - | 3.43k ±1.1% | - | 67.46 | - | 2.507 | 509.4 |
340
- | Raptor | Scaling (3–16) | - | 3.26k ±2.5% | - | 70.51 | - | 2.572 | 499.6 |
341
- | Puma | Fixed (3) | Fixed (3) | 3.21k ±3.5% | 6.3% lower | 56.00 | 17.0% lower | 2.454 | 441.3 |
342
- | Puma | Scaling (3–16) | Scaling (3–16) | 3.15k ±2.3% | 3.4% lower | 49.26 | 30.1% lower | 2.186 | 431.0 |
343
- | Falcon | Fibers | Fixed (3) | 3.36k ±4.2% | 2.1% lower | 48.20 | 28.5% lower | 2.555 | 435.8 |
344
- | Falcon | Fibers | Scaling (3–16) | 3.36k ±4.2% | 3.0% higher | 48.20 | 31.6% lower | 2.555 | 435.8 |
330
+ | Raptor | Fixed (3) | - | 3.40k ±1.5% | - | 66.74 | - | 2.519 | 511.8 |
331
+ | Raptor | Scaling (3–16) | - | 3.45k ±3.9% | - | 62.67 | - | 2.389 | 504.6 |
332
+ | Puma | Fixed (3) | Fixed (3) | 3.24k ±0.9% | 4.7% lower | 57.00 | 14.6% lower | 2.457 | 444.5 |
333
+ | Puma | Scaling (3–16) | Scaling (3–16) | 3.09k ±2.7% | 10.4% lower | 49.54 | 21.0% lower | 2.182 | 437.4 |
334
+ | Falcon | Fibers | Fixed (3) | 3.53k ±0.8% | 3.8% higher | 47.04 | 29.5% lower | 2.430 | 432.3 |
335
+ | Falcon | Fibers | Scaling (3–16) | 3.53k ±0.8% | 2.2% higher | 47.04 | 24.9% lower | 2.430 | 432.3 |
345
336
 
346
337
  ### CPU over HTTP/1.1 keep-alive
347
338
 
348
339
  | Server | Mode | Raptor baseline | req/s | vs Raptor req/s | p95 ms | vs Raptor p95 | CPU ms/req | PSS MiB |
349
340
  | ------ | -------------- | --------------- | ----------- | --------------- | ------ | ------------- | ---------- | ------- |
350
- | Raptor | Fixed (3) | - | 3.10k ±5.2% | - | 54.76 | - | 2.560 | 484.4 |
351
- | Raptor | Scaling (3–16) | - | 2.97k ±1.2% | - | 49.64 | - | 2.553 | 505.3 |
352
- | Puma | Fixed (3) | Fixed (3) | 3.52k ±6.1% | 13.7% higher | 51.14 | 6.6% lower | 2.299 | 384.4 |
353
- | Puma | Scaling (3–16) | Scaling (3–16) | 2.90k ±4.2% | 2.3% lower | 65.73 | 32.4% higher | 2.283 | 472.5 |
354
- | Falcon | Fibers | Fixed (3) | 3.17k ±1.9% | 2.4% higher | 69.58 | 27.1% higher | 2.766 | 536.5 |
355
- | Falcon | Fibers | Scaling (3–16) | 3.17k ±1.9% | 6.7% higher | 69.58 | 40.2% higher | 2.766 | 536.5 |
341
+ | Raptor | Fixed (3) | - | 3.12k ±0.3% | - | 50.35 | - | 2.534 | 483.3 |
342
+ | Raptor | Scaling (3–16) | - | 2.99k ±2.1% | - | 50.79 | - | 2.532 | 502.5 |
343
+ | Puma | Fixed (3) | Fixed (3) | 3.49k ±6.5% | 11.7% higher | 51.75 | 2.8% higher | 2.319 | 383.4 |
344
+ | Puma | Scaling (3–16) | Scaling (3–16) | 3.00k ±3.8% | 0.5% higher | 64.39 | 26.8% higher | 2.224 | 470.8 |
345
+ | Falcon | Fibers | Fixed (3) | 3.17k ±0.2% | 1.7% higher | 67.10 | 33.3% higher | 2.763 | 529.6 |
346
+ | Falcon | Fibers | Scaling (3–16) | 3.17k ±0.2% | 6.1% higher | 67.10 | 32.1% higher | 2.763 | 529.6 |
356
347
 
357
348
  ### CPU over HTTP/2
358
349
 
359
350
  | Server | Mode | Raptor baseline | req/s | vs Raptor req/s | p95 ms | vs Raptor p95 | CPU ms/req | PSS MiB |
360
351
  | ------ | -------------- | --------------- | ------------ | --------------- | ------ | ------------- | ---------- | ------- |
361
- | Raptor | Fixed (3) | - | 3.26k ±2.1% | - | 71.84 | - | 2.329 | 573.4 |
362
- | Raptor | Scaling (3–16) | - | 3.25k ±2.6% | - | 69.37 | - | 2.383 | 590.6 |
363
- | Falcon | Fibers | Fixed (3) | 2.02k ±11.2% | 37.9% lower | 176.57 | 145.8% higher | 2.405 | 559.2 |
364
- | Falcon | Fibers | Scaling (3–16) | 2.02k ±11.2% | 37.9% lower | 176.57 | 154.5% higher | 2.405 | 559.2 |
352
+ | Raptor | Fixed (3) | - | 3.32k ±3.7% | - | 70.25 | - | 2.374 | 583.2 |
353
+ | Raptor | Scaling (3–16) | - | 3.31k ±0.9% | - | 60.05 | - | 2.347 | 562.3 |
354
+ | Falcon | Fibers | Fixed (3) | 2.36k ±18.9% | 29.0% lower | 127.17 | 81.0% higher | 2.399 | 561.8 |
355
+ | Falcon | Fibers | Scaling (3–16) | 2.36k ±18.9% | 28.8% lower | 127.17 | 111.8% higher | 2.399 | 561.8 |
365
356
 
366
357
  ### 64 KiB body over HTTP/1.1 keep-alive
367
358
 
368
359
  | Server | Mode | Raptor baseline | req/s | vs Raptor req/s | p95 ms | vs Raptor p95 | CPU ms/req | PSS MiB |
369
360
  | ------ | -------------- | --------------- | ------------- | --------------- | ------ | ------------- | ---------- | ------- |
370
- | Raptor | Scaling (3–16) | - | 94.58k ±2.0% | - | 3.77 | - | 0.052 | 507.6 |
371
- | Puma | Scaling (3–16) | Scaling (3–16) | 88.40k ±2.1% | 6.5% lower | 3.66 | 2.9% lower | 0.054 | 1237.4 |
372
- | Falcon | Fibers | Scaling (3–16) | 103.33k ±5.0% | 9.2% higher | 3.89 | 3.2% higher | 0.051 | 344.7 |
361
+ | Raptor | Scaling (3–16) | - | 94.57k ±1.5% | - | 3.70 | - | 0.051 | 592.0 |
362
+ | Puma | Scaling (3–16) | Scaling (3–16) | 88.51k ±0.7% | 6.4% lower | 3.67 | 0.6% lower | 0.054 | 1201.7 |
363
+ | Falcon | Fibers | Scaling (3–16) | 103.00k ±5.1% | 8.9% higher | 3.84 | 3.9% higher | 0.051 | 341.1 |
373
364
 
374
365
  ### IO + SSE (native) over HTTP/1.1 keep-alive
375
366
 
376
367
  | Server | Mode | Raptor baseline | req/s | vs Raptor req/s | p95 ms | vs Raptor p95 | CPU ms/req | PSS MiB |
377
368
  | ------------- | -------------- | --------------- | ----------- | --------------- | ------ | ------------- | ---------- | ------- |
378
- | Raptor | Fixed (3) | - | 3.24k ±1.6% | - | 44.69 | - | 2.396 | 355.6 |
379
- | Raptor | Scaling (3–16) | - | 7.39k ±0.8% | - | 24.47 | - | 0.468 | 346.4 |
380
- | Puma | Fixed (3) | Fixed (3) | 0.35k ±4.5% | 89.3% lower | 316.86 | 609.1% higher | 1.764 | 243.6 |
381
- | Puma | Scaling (3–16) | Scaling (3–16) | 7.38k ±4.3% | 0.2% lower | 24.61 | 0.6% higher | 0.440 | 311.3 |
382
- | Falcon | Fibers | Fixed (3) | 7.32k ±1.0% | 125.8% higher | 24.83 | 44.4% lower | 0.383 | 354.4 |
383
- | Falcon | Fibers | Scaling (3–16) | 7.32k ±1.0% | 1.0% lower | 24.83 | 1.5% higher | 0.383 | 354.4 |
384
- | Raptor (enum) | Fixed (3) | - | 0.97k ±4.8% | - | 56.38 | - | 7.624 | 324.4 |
369
+ | Raptor | Fixed (3) | - | 2.74k ±6.2% | - | 59.25 | - | 2.298 | 341.5 |
370
+ | Raptor | Scaling (3–16) | - | 7.41k ±1.2% | - | 24.53 | - | 0.471 | 345.4 |
371
+ | Puma | Fixed (3) | Fixed (3) | 0.34k ±5.4% | 87.5% lower | 361.94 | 510.8% higher | 1.715 | 263.9 |
372
+ | Puma | Scaling (3–16) | Scaling (3–16) | 7.34k ±1.0% | 1.0% lower | 24.70 | 0.7% higher | 0.430 | 310.9 |
373
+ | Falcon | Fibers | Fixed (3) | 7.30k ±1.0% | 165.9% higher | 24.88 | 58.0% lower | 0.379 | 356.6 |
374
+ | Falcon | Fibers | Scaling (3–16) | 7.30k ±1.0% | 1.6% lower | 24.88 | 1.5% higher | 0.379 | 356.6 |
375
+ | Raptor (enum) | Fixed (3) | - | 0.88k ±0.3% | - | 66.74 | - | 8.142 | 382.6 |
385
376
 
386
- Puma, Fixed (3) had 68 timeouts, 1 stalled stream across all samples.
377
+ Puma, Fixed (3) had 37 timeouts, 1 stalled stream across all samples.
387
378
 
388
- Raptor (enum), Fixed (3) had 21 timeouts, 2 stalled streams across all samples.
379
+ Raptor (enum), Fixed (3) had 16 timeouts across all samples.
389
380
 
390
381
  > ruby 4.1.0dev (2026-11-10T00:02:04Z HEAD 7a07e456e4) +YJIT +PRISM [aarch64-linux]
391
382
  > 10 workers, 120 HTTP/1.1 client connections, 40 HTTP/2 connections × 3 streams.
data/lib/raptor/cli.rb CHANGED
@@ -67,8 +67,11 @@ module Raptor
67
67
  worker_shutdown_timeout: 30,
68
68
  refork_after: (RUBY_PLATFORM.include?("linux") ? 1000 : nil),
69
69
  before_fork: [].freeze,
70
+ after_fork: [].freeze,
70
71
  before_worker_boot: [].freeze,
71
72
  before_worker_shutdown: [].freeze,
73
+ after_worker_drain: [].freeze,
74
+ after_worker_exit: [].freeze,
72
75
  before_refork: [].freeze,
73
76
  plugins: [].freeze,
74
77
  stats_file: "tmp/raptor.json",
@@ -80,8 +80,11 @@ module Raptor
80
80
  # @rbs @worker_drain_timeout: Integer
81
81
  # @rbs @worker_shutdown_timeout: Integer
82
82
  # @rbs @before_fork: Array[Proc]
83
+ # @rbs @after_fork: Array[Proc]
83
84
  # @rbs @before_worker_boot: Array[Proc]
84
85
  # @rbs @before_worker_shutdown: Array[Proc]
86
+ # @rbs @after_worker_drain: Array[Proc]
87
+ # @rbs @after_worker_exit: Array[Proc]
85
88
  # @rbs @before_refork: Array[Proc]
86
89
  # @rbs @stats_file: String?
87
90
  # @rbs @control_server: ControlServer?
@@ -97,6 +100,7 @@ module Raptor
97
100
  # @rbs @app: untyped
98
101
  # @rbs @shutdown: bool
99
102
  # @rbs @workers: Hash[Integer, Integer]
103
+ # @rbs @worker_indices: Hash[Integer, Integer]
100
104
  # @rbs @timed_out: Set[Integer]
101
105
  # @rbs @stats: Stats
102
106
  # @rbs @started_at: Float
@@ -146,9 +150,12 @@ module Raptor
146
150
  # @option options [Integer] :worker_drain_timeout seconds a worker waits for in-flight requests during shutdown before force-killing app threads
147
151
  # @option options [Integer] :worker_shutdown_timeout seconds to wait for graceful worker exit before force-killing
148
152
  # @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)
149
- # @option options [Array<Proc>] :before_fork procs called in the master before every worker fork
153
+ # @option options [Array<Proc>] :before_fork procs called in the master before each direct worker fork, not seed forks
154
+ # @option options [Array<Proc>] :after_fork procs called in the master after each direct fork attempt, including failures
150
155
  # @option options [Array<Proc>] :before_worker_boot procs called with the worker index before it begins serving
151
156
  # @option options [Array<Proc>] :before_worker_shutdown procs called with the worker index before its graceful shutdown
157
+ # @option options [Array<Proc>] :after_worker_drain procs called with the worker index after draining, before exit or seed promotion
158
+ # @option options [Array<Proc>] :after_worker_exit procs called in the master after reaping, with the worker index, pid, and Process::Status
152
159
  # @option options [Array<Proc>] :before_refork procs called in a worker before it transitions to a seed
153
160
  # @option options [Array<String, Symbol>] :plugins plugins to load and start in order
154
161
  # @option options [String, nil] :stats_file path to write per-worker stats JSON, or nil to disable
@@ -178,8 +185,11 @@ module Raptor
178
185
  @worker_drain_timeout = options[:worker_drain_timeout]
179
186
  @worker_shutdown_timeout = options[:worker_shutdown_timeout]
180
187
  @before_fork = options[:before_fork] = Array(options[:before_fork]).dup
188
+ @after_fork = options[:after_fork] = Array(options[:after_fork]).dup
181
189
  @before_worker_boot = options[:before_worker_boot] = Array(options[:before_worker_boot]).dup
182
190
  @before_worker_shutdown = options[:before_worker_shutdown] = Array(options[:before_worker_shutdown]).dup
191
+ @after_worker_drain = options[:after_worker_drain] = Array(options[:after_worker_drain]).dup
192
+ @after_worker_exit = options[:after_worker_exit] = Array(options[:after_worker_exit]).dup
183
193
  @before_refork = options[:before_refork] = Array(options[:before_refork]).dup
184
194
  @stats_file = options[:stats_file]
185
195
  @control_server = ControlServer.new(options[:control_url]) { stats_hash } if options[:control_url]
@@ -208,6 +218,7 @@ module Raptor
208
218
 
209
219
  @shutdown = false
210
220
  @workers = {}
221
+ @worker_indices = {}
211
222
  @timed_out = Set.new
212
223
  @stats = Stats.new(@worker_count)
213
224
  @started_at = Process.clock_gettime(Process::CLOCK_REALTIME)
@@ -438,15 +449,34 @@ module Raptor
438
449
  pid = spawn_worker_via_seed(index)
439
450
  if pid
440
451
  @workers[index] = pid
452
+ @worker_indices[pid] = index
441
453
  return
442
454
  end
443
455
  Log.warn "Seed (#{@seed_pid}) failed to fork worker #{index}, falling back to direct fork"
444
456
  @seed_pid = nil
445
457
  end
446
458
 
447
- @before_fork.each(&:call)
448
- pid = fork { run_worker(index, @phase) }
449
- @workers[index] = pid
459
+ master_pid = Process.pid
460
+ begin
461
+ @before_fork.each(&:call)
462
+ pid = fork { run_worker(index, @phase) }
463
+ @workers[index] = pid
464
+ @worker_indices[pid] = index
465
+ ensure
466
+ if Process.pid == master_pid
467
+ error = $!
468
+ @after_fork.each do |hook|
469
+ hook.call
470
+ rescue => hook_error
471
+ if error
472
+ Log.rescued_error(hook_error)
473
+ else
474
+ error = hook_error
475
+ end
476
+ end
477
+ raise error if error
478
+ end
479
+ end
450
480
  end
451
481
 
452
482
  # Asks the seed to fork a new worker at the given index, returning the
@@ -511,19 +541,20 @@ module Raptor
511
541
  #
512
542
  # @rbs (Integer pid, Process::Status status) -> void
513
543
  def reap_pid(pid, status)
544
+ index = @worker_indices.delete(pid)
545
+ return unless index
546
+
547
+ active = @workers[index] == pid
514
548
  if pid == @seed_pid
515
549
  Log.info "Seed (#{pid}) exited, #{exit_description(status)}"
516
550
  @seed_pid = nil
517
- return
518
551
  end
519
552
 
520
- index = @workers.key(pid)
521
- return unless index
522
-
523
- @workers.delete(index)
553
+ @workers.delete(index) if active
524
554
  @timed_out.delete(pid)
555
+ @after_worker_exit.each { |hook| hook.parameters.empty? ? hook.call : hook.call(index, pid, status) }
525
556
 
526
- unless @shutdown
557
+ if active && !@shutdown
527
558
  Log.warn "Restarting worker #{index} (#{pid}), #{exit_description(status)}"
528
559
  spawn_worker(index)
529
560
  end
@@ -655,7 +686,11 @@ module Raptor
655
686
  pids = @workers.values + [@seed_pid].compact
656
687
  Log.warn "Force-killing #{pids.size} process(es) after #{@worker_shutdown_timeout}s"
657
688
  pids.each { |pid| Process.kill("KILL", pid) rescue nil }
658
- pids.each { |pid| Process.wait(pid) rescue nil }
689
+ pids.each do |pid|
690
+ _, status = Process.wait2(pid)
691
+ reap_pid(pid, status)
692
+ rescue Errno::ECHILD
693
+ end
659
694
  end
660
695
 
661
696
  # Kills workers that have stopped checking in. A booted worker that
@@ -903,6 +938,7 @@ module Raptor
903
938
  end
904
939
 
905
940
  worker.shutdown(close_listeners: !promote_to_seed)
941
+ @after_worker_drain.each { |hook| hook.parameters.empty? ? hook.call : hook.call(index) }
906
942
  # A seed never accepts again, and the workers it forks bind their own.
907
943
  bpf_listeners.each(&:close) if promote_to_seed
908
944
  stats_thread.join
@@ -61,15 +61,7 @@ module Raptor
61
61
  @binder = Binder.new(@binds, socket_backlog: @options[:socket_backlog])
62
62
  @port = @binder.server_port
63
63
  @addresses = @binder.addresses
64
- @worker = Worker.new(
65
- @app,
66
- listeners: @binder.listeners,
67
- port: @port,
68
- options: @options,
69
- http1_ractors: @options[:http1][:ractors] || Cluster.default_http1_ractor_count(1),
70
- http2_ractors: @options[:http2][:ractors] || Cluster.default_http2_ractor_count(1)
71
- )
72
- @worker.start
64
+ start_worker
73
65
  rescue
74
66
  @stopped = true
75
67
  @binder&.close
@@ -89,6 +81,57 @@ module Raptor
89
81
  join
90
82
  end
91
83
 
84
+ # Drains serving work while retaining the bound listeners for resume.
85
+ # Connections may queue, but requests are not served while paused.
86
+ # Existing keep-alive connections close. Repeated pauses have no effect.
87
+ #
88
+ # Pause before master forks and resume in the parent. Close the inherited
89
+ # instance before other worker boot hooks, and shut down when the plugin stops.
90
+ #
91
+ # @example From a plugin's start callback
92
+ # @server = Raptor::Embedded.new(app, port: 9293).start
93
+ # cluster.options[:before_fork] << proc { @server.pause }
94
+ # cluster.options[:after_fork] << proc { @server.resume }
95
+ # cluster.options[:before_worker_boot].unshift(proc { @server.shutdown })
96
+ #
97
+ # @rbs () -> Embedded
98
+ def pause
99
+ with_lifecycle do
100
+ raise ThreadError, "server is not running" unless @started && !@stopped
101
+
102
+ begin
103
+ @worker&.shutdown(close_listeners: false)
104
+ @worker = nil
105
+ rescue
106
+ @stopped = true
107
+ @binder&.close
108
+ raise
109
+ end
110
+ end
111
+
112
+ self
113
+ end
114
+
115
+ # Resumes serving on the retained listeners. Has no effect while running.
116
+ # Shutdown remains terminal. Startup failures release the listeners and propagate.
117
+ #
118
+ # @rbs () -> Embedded
119
+ def resume
120
+ with_lifecycle do
121
+ raise ThreadError, "server has not been started or has been stopped" unless @started && !@stopped
122
+
123
+ begin
124
+ start_worker unless @worker
125
+ rescue
126
+ @stopped = true
127
+ @binder&.close
128
+ raise
129
+ end
130
+ end
131
+
132
+ self
133
+ end
134
+
92
135
  # Drains requests and closes the server's sockets, threads, and Ractors.
93
136
  # Calling shutdown more than once has no effect.
94
137
  #
@@ -106,20 +149,43 @@ module Raptor
106
149
  end
107
150
  end
108
151
 
109
- # Waits for termination and propagates serving thread failures.
152
+ # Waits across pauses until termination and propagates serving failures.
110
153
  #
111
154
  # @rbs () -> void
112
155
  def join
113
- worker = with_lifecycle { @worker }
114
- raise ThreadError, "server has not been started" unless worker
115
-
116
- worker.join
156
+ started = with_lifecycle { @started }
157
+ raise ThreadError, "server has not been started" unless started
158
+
159
+ loop do
160
+ worker, stopped = with_lifecycle { [@worker, @stopped] }
161
+ break if stopped
162
+
163
+ if worker
164
+ worker.join
165
+ break if with_lifecycle { @worker.equal?(worker) }
166
+ else
167
+ @lifecycle_available.wait { @worker || @stopped }
168
+ end
169
+ end
117
170
  ensure
118
- shutdown if worker
171
+ shutdown if started
119
172
  end
120
173
 
121
174
  private
122
175
 
176
+ # @rbs () -> void
177
+ def start_worker
178
+ @worker = Worker.new(
179
+ @app,
180
+ listeners: @binder.listeners,
181
+ port: @port,
182
+ options: @options,
183
+ http1_ractors: @options[:http1][:ractors] || Cluster.default_http1_ractor_count(1),
184
+ http2_ractors: @options[:http2][:ractors] || Cluster.default_http2_ractor_count(1)
185
+ )
186
+ @worker.start
187
+ end
188
+
123
189
  # @rbs () -> void
124
190
  def log_initialization
125
191
  environment = @options[:environment] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
data/lib/raptor/plugin.rb CHANGED
@@ -53,6 +53,7 @@ module Raptor
53
53
 
54
54
  # Starts in the master before workers fork. Override to open resources
55
55
  # and append callbacks to the cluster's lifecycle hook arrays.
56
+ # See {Cluster#initialize} for hooks and {Embedded#pause} for fork-safe Rack listeners.
56
57
  #
57
58
  # @rbs (Cluster cluster) -> void
58
59
  def start(cluster)
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Raptor
5
- VERSION = "0.26.0"
5
+ VERSION = "0.27.0"
6
6
  end
@@ -72,20 +72,22 @@ module Raptor
72
72
 
73
73
  @before_refork: Array[Proc]
74
74
 
75
+ @after_worker_exit: Array[Proc]
76
+
77
+ @after_worker_drain: Array[Proc]
78
+
75
79
  @before_worker_shutdown: Array[Proc]
76
80
 
77
81
  @before_worker_boot: Array[Proc]
78
82
 
83
+ @after_fork: Array[Proc]
84
+
79
85
  @before_fork: Array[Proc]
80
86
 
81
87
  @worker_shutdown_timeout: Integer
82
88
 
83
89
  @worker_drain_timeout: Integer
84
90
 
85
- @worker_timeout: Integer
86
-
87
- @worker_boot_timeout: Integer
88
-
89
91
  @bpf_active: bool
90
92
 
91
93
  @resp_w: IO?
@@ -122,10 +124,16 @@ module Raptor
122
124
 
123
125
  @timed_out: Set[Integer]
124
126
 
127
+ @worker_indices: Hash[Integer, Integer]
128
+
125
129
  @workers: Hash[Integer, Integer]
126
130
 
127
131
  @shutdown: bool
128
132
 
133
+ @worker_timeout: Integer
134
+
135
+ @worker_boot_timeout: Integer
136
+
129
137
  @environment: String
130
138
 
131
139
  @cpu_affinity: bool
@@ -169,9 +177,12 @@ module Raptor
169
177
  # @option options [Integer] :worker_drain_timeout seconds a worker waits for in-flight requests during shutdown before force-killing app threads
170
178
  # @option options [Integer] :worker_shutdown_timeout seconds to wait for graceful worker exit before force-killing
171
179
  # @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)
172
- # @option options [Array<Proc>] :before_fork procs called in the master before every worker fork
180
+ # @option options [Array<Proc>] :before_fork procs called in the master before each direct worker fork, not seed forks
181
+ # @option options [Array<Proc>] :after_fork procs called in the master after each direct fork attempt, including failures
173
182
  # @option options [Array<Proc>] :before_worker_boot procs called with the worker index before it begins serving
174
183
  # @option options [Array<Proc>] :before_worker_shutdown procs called with the worker index before its graceful shutdown
184
+ # @option options [Array<Proc>] :after_worker_drain procs called with the worker index after draining, before exit or seed promotion
185
+ # @option options [Array<Proc>] :after_worker_exit procs called in the master after reaping, with the worker index, pid, and Process::Status
175
186
  # @option options [Array<Proc>] :before_refork procs called in a worker before it transitions to a seed
176
187
  # @option options [Array<String, Symbol>] :plugins plugins to load and start in order
177
188
  # @option options [String, nil] :stats_file path to write per-worker stats JSON, or nil to disable
@@ -46,19 +46,44 @@ module Raptor
46
46
  # @rbs () -> void
47
47
  def run: () -> void
48
48
 
49
+ # Drains serving work while retaining the bound listeners for resume.
50
+ # Connections may queue, but requests are not served while paused.
51
+ # Existing keep-alive connections close. Repeated pauses have no effect.
52
+ #
53
+ # Pause before master forks and resume in the parent. Close the inherited
54
+ # instance before other worker boot hooks, and shut down when the plugin stops.
55
+ #
56
+ # @example From a plugin's start callback
57
+ # @server = Raptor::Embedded.new(app, port: 9293).start
58
+ # cluster.options[:before_fork] << proc { @server.pause }
59
+ # cluster.options[:after_fork] << proc { @server.resume }
60
+ # cluster.options[:before_worker_boot].unshift(proc { @server.shutdown })
61
+ #
62
+ # @rbs () -> Embedded
63
+ def pause: () -> Embedded
64
+
65
+ # Resumes serving on the retained listeners. Has no effect while running.
66
+ # Shutdown remains terminal. Startup failures release the listeners and propagate.
67
+ #
68
+ # @rbs () -> Embedded
69
+ def resume: () -> Embedded
70
+
49
71
  # Drains requests and closes the server's sockets, threads, and Ractors.
50
72
  # Calling shutdown more than once has no effect.
51
73
  #
52
74
  # @rbs () -> void
53
75
  def shutdown: () -> void
54
76
 
55
- # Waits for termination and propagates serving thread failures.
77
+ # Waits across pauses until termination and propagates serving failures.
56
78
  #
57
79
  # @rbs () -> void
58
80
  def join: () -> void
59
81
 
60
82
  private
61
83
 
84
+ # @rbs () -> void
85
+ def start_worker: () -> void
86
+
62
87
  # @rbs () -> void
63
88
  def log_initialization: () -> void
64
89
 
@@ -28,6 +28,7 @@ module Raptor
28
28
 
29
29
  # Starts in the master before workers fork. Override to open resources
30
30
  # and append callbacks to the cluster's lifecycle hook arrays.
31
+ # See {Cluster#initialize} for hooks and {Embedded#pause} for fork-safe Rack listeners.
31
32
  #
32
33
  # @rbs (Cluster cluster) -> void
33
34
  def start: (Cluster cluster) -> void
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: raptor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.26.0
4
+ version: 0.27.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Young