raptor 0.11.0 → 0.12.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/.buildkite/pipeline.yml +100 -0
- data/CHANGELOG.md +7 -0
- data/README.md +47 -31
- data/docs/raptor-vs-puma.md +61 -43
- data/ext/raptor_bpf/reuseport_select.bpf.c +3 -2
- data/ext/raptor_native/raptor_native.c +16 -2
- data/lib/raptor/cli.rb +9 -0
- data/lib/raptor/cluster.rb +368 -29
- data/lib/raptor/reuseport_bpf.rb +13 -0
- data/lib/raptor/server.rb +14 -6
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/cluster.rbs +173 -34
- data/sig/generated/raptor/reuseport_bpf.rbs +9 -0
- data/sig/generated/raptor/server.rbs +10 -4
- metadata +1 -1
data/lib/raptor/cluster.rb
CHANGED
|
@@ -72,6 +72,10 @@ module Raptor
|
|
|
72
72
|
# @rbs @worker_timeout: Integer
|
|
73
73
|
# @rbs @worker_drain_timeout: Integer
|
|
74
74
|
# @rbs @worker_shutdown_timeout: Integer
|
|
75
|
+
# @rbs @before_fork: Array[Proc]
|
|
76
|
+
# @rbs @before_worker_boot: Array[Proc]
|
|
77
|
+
# @rbs @before_worker_shutdown: Array[Proc]
|
|
78
|
+
# @rbs @before_refork: Array[Proc]
|
|
75
79
|
# @rbs @stats_file: String?
|
|
76
80
|
# @rbs @pid_file: String?
|
|
77
81
|
# @rbs @stdout_file: String?
|
|
@@ -92,6 +96,16 @@ module Raptor
|
|
|
92
96
|
# @rbs @phased_restart_requested: bool
|
|
93
97
|
# @rbs @phased_restarting: bool
|
|
94
98
|
# @rbs @hot_restart_requested: bool
|
|
99
|
+
# @rbs @refork_thresholds: Array[Integer]
|
|
100
|
+
# @rbs @refork_requested: bool
|
|
101
|
+
# @rbs @refork_threshold_idx: Integer
|
|
102
|
+
# @rbs @seed_pid: Integer?
|
|
103
|
+
# @rbs @seed_ready: bool
|
|
104
|
+
# @rbs @seed_vacated_index: Integer?
|
|
105
|
+
# @rbs @fork_r: IO?
|
|
106
|
+
# @rbs @fork_w: IO?
|
|
107
|
+
# @rbs @resp_r: IO?
|
|
108
|
+
# @rbs @resp_w: IO?
|
|
95
109
|
# @rbs @bpf_active: bool
|
|
96
110
|
|
|
97
111
|
# Creates a new Cluster with the specified configuration.
|
|
@@ -118,6 +132,11 @@ module Raptor
|
|
|
118
132
|
# @option options [Integer] :worker_timeout seconds to wait for a booted worker to check in before killing it
|
|
119
133
|
# @option options [Integer] :worker_drain_timeout seconds a worker waits for in-flight requests during shutdown before force-killing app threads
|
|
120
134
|
# @option options [Integer] :worker_shutdown_timeout seconds to wait for graceful worker exit before force-killing
|
|
135
|
+
# @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)
|
|
136
|
+
# @option options [Array<Proc>] :before_fork procs called in the master before every worker fork
|
|
137
|
+
# @option options [Array<Proc>] :before_worker_boot procs called in each worker before it begins serving
|
|
138
|
+
# @option options [Array<Proc>] :before_worker_shutdown procs called in each worker before its graceful shutdown
|
|
139
|
+
# @option options [Array<Proc>] :before_refork procs called in a worker before it transitions to a seed
|
|
121
140
|
# @option options [String, nil] :stats_file path to write per-worker stats JSON, or nil to disable
|
|
122
141
|
# @option options [String, nil] :pid_file path to write the master PID to, or nil to disable
|
|
123
142
|
# @option options [String, nil] :stdout_file path to redirect stdout to, reopened on SIGHUP, or nil to disable
|
|
@@ -142,6 +161,10 @@ module Raptor
|
|
|
142
161
|
@worker_timeout = options[:worker_timeout]
|
|
143
162
|
@worker_drain_timeout = options[:worker_drain_timeout]
|
|
144
163
|
@worker_shutdown_timeout = options[:worker_shutdown_timeout]
|
|
164
|
+
@before_fork = Array(options[:before_fork])
|
|
165
|
+
@before_worker_boot = Array(options[:before_worker_boot])
|
|
166
|
+
@before_worker_shutdown = Array(options[:before_worker_shutdown])
|
|
167
|
+
@before_refork = Array(options[:before_refork])
|
|
145
168
|
@stats_file = options[:stats_file]
|
|
146
169
|
@pid_file = options[:pid_file]
|
|
147
170
|
@stdout_file = options[:stdout_file]
|
|
@@ -175,6 +198,13 @@ module Raptor
|
|
|
175
198
|
@phased_restart_requested = false
|
|
176
199
|
@phased_restarting = false
|
|
177
200
|
@hot_restart_requested = false
|
|
201
|
+
|
|
202
|
+
@refork_thresholds = normalize_refork_thresholds(options[:refork_after])
|
|
203
|
+
@refork_requested = false
|
|
204
|
+
@refork_threshold_idx = 0
|
|
205
|
+
@seed_pid = nil
|
|
206
|
+
@seed_ready = false
|
|
207
|
+
@seed_vacated_index = nil
|
|
178
208
|
end
|
|
179
209
|
|
|
180
210
|
# Starts the multi-process cluster and manages worker processes.
|
|
@@ -196,6 +226,9 @@ module Raptor
|
|
|
196
226
|
#
|
|
197
227
|
# @rbs () -> void
|
|
198
228
|
def run
|
|
229
|
+
$stdout.sync = true
|
|
230
|
+
$stderr.sync = true
|
|
231
|
+
|
|
199
232
|
reopen_logs
|
|
200
233
|
|
|
201
234
|
trap("INT") { shutdown }
|
|
@@ -204,6 +237,17 @@ module Raptor
|
|
|
204
237
|
trap("USR1") { @phased_restart_requested = true }
|
|
205
238
|
trap("USR2") { @hot_restart_requested = true }
|
|
206
239
|
|
|
240
|
+
if @refork_thresholds.any?
|
|
241
|
+
if Subreaper.enable
|
|
242
|
+
@fork_r, @fork_w = IO.pipe
|
|
243
|
+
@resp_r, @resp_w = IO.pipe
|
|
244
|
+
trap("URG") { @refork_requested = true }
|
|
245
|
+
else
|
|
246
|
+
Log.warn "Ignoring refork_after: PR_SET_CHILD_SUBREAPER not supported on this platform"
|
|
247
|
+
@refork_thresholds = [].freeze
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
207
251
|
File.open(@pid_file, File::CREAT | File::EXCL | File::WRONLY) { |file| file.write(Process.pid.to_s) } if @pid_file
|
|
208
252
|
|
|
209
253
|
@bpf_active = ReuseportBPF.setup(@worker_count)
|
|
@@ -224,7 +268,9 @@ module Raptor
|
|
|
224
268
|
break if reap_workers == :no_children
|
|
225
269
|
|
|
226
270
|
perform_hot_restart if @hot_restart_requested
|
|
271
|
+
poll_seed_ready if @seed_pid && !@seed_ready
|
|
227
272
|
perform_phased_restart if @phased_restart_requested && !@phased_restarting
|
|
273
|
+
check_refork_trigger if @refork_thresholds.any? && !@phased_restarting
|
|
228
274
|
timeout_hung_workers
|
|
229
275
|
|
|
230
276
|
sleep 0.1
|
|
@@ -270,61 +316,256 @@ module Raptor
|
|
|
270
316
|
bind_uris.zip(filenos).to_h { |bind_uri, fileno| [bind_uri, [fileno]] }
|
|
271
317
|
end
|
|
272
318
|
|
|
273
|
-
#
|
|
274
|
-
#
|
|
319
|
+
# Normalises the `refork_after` option into a sorted array of positive
|
|
320
|
+
# thresholds. Accepts nil, 0, an Integer, or an Array<Integer>; anything
|
|
321
|
+
# else falls back to an empty array (feature disabled).
|
|
322
|
+
#
|
|
323
|
+
# @param value [Integer, Array<Integer>, nil] the raw option value
|
|
324
|
+
# @return [Array<Integer>]
|
|
325
|
+
#
|
|
326
|
+
# @rbs (untyped value) -> Array[Integer]
|
|
327
|
+
def normalize_refork_thresholds(value)
|
|
328
|
+
case value
|
|
329
|
+
when Integer
|
|
330
|
+
value.positive? ? [value].freeze : [].freeze
|
|
331
|
+
when Array
|
|
332
|
+
value.select { |threshold| threshold.is_a?(Integer) && threshold.positive? }.sort.freeze
|
|
333
|
+
else
|
|
334
|
+
[].freeze
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Forks a new worker process and registers it at the given index,
|
|
339
|
+
# forking from the seed when one is active and off the master otherwise.
|
|
275
340
|
#
|
|
276
341
|
# @param index [Integer] slot index for this worker in the stats region
|
|
277
342
|
# @return [void]
|
|
278
343
|
#
|
|
279
344
|
# @rbs (Integer index) -> void
|
|
280
345
|
def spawn_worker(index)
|
|
346
|
+
if @seed_pid && pid_alive?(@seed_pid)
|
|
347
|
+
pid = spawn_worker_via_seed(index)
|
|
348
|
+
if pid
|
|
349
|
+
@workers[index] = pid
|
|
350
|
+
return
|
|
351
|
+
end
|
|
352
|
+
Log.warn "Seed (#{@seed_pid}) failed to fork worker #{index}, falling back to direct fork"
|
|
353
|
+
@seed_pid = nil
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
@before_fork.each(&:call)
|
|
281
357
|
pid = fork { run_worker(index, @phase) }
|
|
282
358
|
@workers[index] = pid
|
|
283
359
|
end
|
|
284
360
|
|
|
361
|
+
# Asks the seed to fork a new worker at the given index, returning the
|
|
362
|
+
# child pid, or nil when the seed doesn't respond in time.
|
|
363
|
+
#
|
|
364
|
+
# @param index [Integer] slot index for the new worker
|
|
365
|
+
# @return [Integer, nil] the forked worker's pid, or nil on failure
|
|
366
|
+
#
|
|
367
|
+
# @rbs (Integer index) -> Integer?
|
|
368
|
+
def spawn_worker_via_seed(index)
|
|
369
|
+
@fork_w.write([index, @phase].pack("LL"))
|
|
370
|
+
return nil unless @resp_r.wait_readable(5)
|
|
371
|
+
|
|
372
|
+
bytes = @resp_r.read_nonblock(4, exception: false)
|
|
373
|
+
return nil unless bytes.is_a?(String) && bytes.bytesize == 4
|
|
374
|
+
|
|
375
|
+
bytes.unpack1("L")
|
|
376
|
+
rescue Errno::EPIPE, IOError
|
|
377
|
+
nil
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Checks whether a process with the given pid is currently alive.
|
|
381
|
+
#
|
|
382
|
+
# @param pid [Integer] the pid to probe
|
|
383
|
+
# @return [Boolean] true if the process exists
|
|
384
|
+
#
|
|
385
|
+
# @rbs (Integer pid) -> bool
|
|
386
|
+
def pid_alive?(pid)
|
|
387
|
+
Process.kill(0, pid)
|
|
388
|
+
true
|
|
389
|
+
rescue Errno::ESRCH, Errno::ECHILD
|
|
390
|
+
false
|
|
391
|
+
rescue Errno::EPERM
|
|
392
|
+
true
|
|
393
|
+
end
|
|
394
|
+
|
|
285
395
|
# Reaps any worker processes that have exited, respawning each one
|
|
286
396
|
# unless the cluster is shutting down.
|
|
287
397
|
#
|
|
288
|
-
# @return [Symbol] :no_children when
|
|
398
|
+
# @return [Symbol] :no_children when nothing is left to supervise, otherwise :reaped
|
|
289
399
|
#
|
|
290
400
|
# @rbs () -> Symbol
|
|
291
401
|
def reap_workers
|
|
292
402
|
loop do
|
|
293
403
|
pid, status = Process.wait2(-1, Process::WNOHANG)
|
|
294
|
-
|
|
404
|
+
break unless pid
|
|
295
405
|
|
|
296
|
-
|
|
297
|
-
@workers.delete(index)
|
|
298
|
-
@timed_out.delete(pid)
|
|
299
|
-
|
|
300
|
-
unless @shutdown
|
|
301
|
-
Log.warn "Restarting worker #{index} (#{pid}), #{exit_description(status)}"
|
|
302
|
-
spawn_worker(index)
|
|
303
|
-
end
|
|
406
|
+
reap_pid(pid, status)
|
|
304
407
|
end
|
|
408
|
+
|
|
409
|
+
@workers.empty? && !@seed_pid ? :no_children : :reaped
|
|
305
410
|
rescue Errno::ECHILD
|
|
306
|
-
:no_children
|
|
411
|
+
@workers.empty? && !@seed_pid ? :no_children : :reaped
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# Records a reaped pid, respawning its worker slot unless the cluster
|
|
415
|
+
# is shutting down. Clears the seed reference when the seed exits.
|
|
416
|
+
#
|
|
417
|
+
# @param pid [Integer] the reaped pid
|
|
418
|
+
# @param status [Process::Status] the exit status
|
|
419
|
+
# @return [void]
|
|
420
|
+
#
|
|
421
|
+
# @rbs (Integer pid, Process::Status status) -> void
|
|
422
|
+
def reap_pid(pid, status)
|
|
423
|
+
if pid == @seed_pid
|
|
424
|
+
Log.info "Seed (#{pid}) exited, #{exit_description(status)}"
|
|
425
|
+
@seed_pid = nil
|
|
426
|
+
return
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
index = @workers.key(pid)
|
|
430
|
+
return unless index
|
|
431
|
+
|
|
432
|
+
@workers.delete(index)
|
|
433
|
+
@timed_out.delete(pid)
|
|
434
|
+
|
|
435
|
+
unless @shutdown
|
|
436
|
+
Log.warn "Restarting worker #{index} (#{pid}), #{exit_description(status)}"
|
|
437
|
+
spawn_worker(index)
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# Promotes the most-experienced worker to a seed and starts a phased
|
|
442
|
+
# refork when the next `refork_after` threshold is crossed or a manual
|
|
443
|
+
# refork was requested via `SIGURG`.
|
|
444
|
+
#
|
|
445
|
+
# @return [void]
|
|
446
|
+
#
|
|
447
|
+
# @rbs () -> void
|
|
448
|
+
def check_refork_trigger
|
|
449
|
+
candidate = pick_refork_candidate
|
|
450
|
+
return unless candidate
|
|
451
|
+
|
|
452
|
+
candidate_index, candidate_requests = candidate
|
|
453
|
+
threshold = @refork_thresholds[@refork_threshold_idx]
|
|
454
|
+
|
|
455
|
+
if @refork_requested
|
|
456
|
+
@refork_requested = false
|
|
457
|
+
elsif !threshold || candidate_requests < threshold
|
|
458
|
+
return
|
|
459
|
+
else
|
|
460
|
+
@refork_threshold_idx += 1
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
promote_worker_to_seed(candidate_index)
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# Picks the most-experienced booted worker in the current phase,
|
|
467
|
+
# returning its slot index and its request count. Returns nil when
|
|
468
|
+
# no worker qualifies.
|
|
469
|
+
#
|
|
470
|
+
# @return [Array<Integer>, nil]
|
|
471
|
+
#
|
|
472
|
+
# @rbs () -> Array[Integer]?
|
|
473
|
+
def pick_refork_candidate
|
|
474
|
+
best_index = nil
|
|
475
|
+
best_requests = -1
|
|
476
|
+
@stats.all.each_with_index do |stat, index|
|
|
477
|
+
next unless @workers[index] == stat[:pid]
|
|
478
|
+
next unless stat[:booted]
|
|
479
|
+
next unless stat[:phase] == @phase
|
|
480
|
+
next unless stat[:requests] > best_requests
|
|
481
|
+
|
|
482
|
+
best_index = index
|
|
483
|
+
best_requests = stat[:requests]
|
|
484
|
+
end
|
|
485
|
+
[best_index, best_requests] if best_index
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# Retires the current seed and promotes the given worker into its place,
|
|
489
|
+
# queueing a phased refork for the remaining workers.
|
|
490
|
+
#
|
|
491
|
+
# @param index [Integer] slot index of the worker to promote
|
|
492
|
+
# @return [void]
|
|
493
|
+
#
|
|
494
|
+
# @rbs (Integer index) -> void
|
|
495
|
+
def promote_worker_to_seed(index)
|
|
496
|
+
pid = @workers[index]
|
|
497
|
+
return unless pid
|
|
498
|
+
|
|
499
|
+
retire_current_seed
|
|
500
|
+
Log.info "Promoting worker #{index} to seed for phased refork"
|
|
501
|
+
ReuseportBPF.mark_unavailable(index) if @bpf_active
|
|
502
|
+
Process.kill("URG", pid) rescue nil
|
|
503
|
+
@workers.delete(index)
|
|
504
|
+
@seed_pid = pid
|
|
505
|
+
@seed_ready = false
|
|
506
|
+
@seed_vacated_index = index
|
|
507
|
+
@phased_restart_requested = true
|
|
307
508
|
end
|
|
308
509
|
|
|
309
|
-
#
|
|
310
|
-
# exit
|
|
510
|
+
# Terminates the currently-active seed process, if any, and waits for
|
|
511
|
+
# it to exit. Its seed-forked workers stay attached to the master and
|
|
512
|
+
# keep serving.
|
|
513
|
+
#
|
|
514
|
+
# @return [void]
|
|
515
|
+
#
|
|
516
|
+
# @rbs () -> void
|
|
517
|
+
def retire_current_seed
|
|
518
|
+
return unless @seed_pid && pid_alive?(@seed_pid)
|
|
519
|
+
|
|
520
|
+
Log.info "Retiring seed (#{@seed_pid})"
|
|
521
|
+
Process.kill("TERM", @seed_pid) rescue nil
|
|
522
|
+
|
|
523
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @worker_shutdown_timeout
|
|
524
|
+
while @seed_pid && pid_alive?(@seed_pid)
|
|
525
|
+
reap_workers
|
|
526
|
+
break if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
527
|
+
|
|
528
|
+
sleep 0.05
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# Records the seed's readiness when its ready marker has arrived.
|
|
533
|
+
# Non-blocking.
|
|
534
|
+
#
|
|
535
|
+
# @return [void]
|
|
536
|
+
#
|
|
537
|
+
# @rbs () -> void
|
|
538
|
+
def poll_seed_ready
|
|
539
|
+
return unless @resp_r && @seed_pid && !@seed_ready
|
|
540
|
+
return unless @resp_r.wait_readable(0)
|
|
541
|
+
|
|
542
|
+
bytes = @resp_r.read_nonblock(4, exception: false)
|
|
543
|
+
return unless bytes.is_a?(String) && bytes.bytesize == 4
|
|
544
|
+
|
|
545
|
+
@seed_ready = true if bytes.unpack1("L") == 0
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# Stops every worker (and the seed if one is active), escalating
|
|
549
|
+
# from TERM to KILL if any fail to exit within `worker_shutdown_timeout`.
|
|
311
550
|
#
|
|
312
551
|
# @return [void]
|
|
313
552
|
#
|
|
314
553
|
# @rbs () -> void
|
|
315
554
|
def stop_workers
|
|
316
555
|
@workers.values.each { |pid| Process.kill("TERM", pid) rescue nil }
|
|
556
|
+
Process.kill("TERM", @seed_pid) rescue nil if @seed_pid
|
|
317
557
|
|
|
318
558
|
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @worker_shutdown_timeout
|
|
319
|
-
until @workers.empty? || Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
559
|
+
until (@workers.empty? && !@seed_pid) || Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
320
560
|
reap_workers
|
|
321
561
|
sleep 0.05
|
|
322
562
|
end
|
|
323
|
-
return if @workers.empty?
|
|
563
|
+
return if @workers.empty? && !@seed_pid
|
|
324
564
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
565
|
+
pids = @workers.values + [@seed_pid].compact
|
|
566
|
+
Log.warn "Force-killing #{pids.size} process(es) after #{@worker_shutdown_timeout}s"
|
|
567
|
+
pids.each { |pid| Process.kill("KILL", pid) rescue nil }
|
|
568
|
+
pids.each { |pid| Process.wait(pid) rescue nil }
|
|
328
569
|
end
|
|
329
570
|
|
|
330
571
|
# Kills workers that have stopped checking in. A booted worker that
|
|
@@ -370,8 +611,12 @@ module Raptor
|
|
|
370
611
|
Log.info "Phased restart starting"
|
|
371
612
|
|
|
372
613
|
begin
|
|
614
|
+
wait_for_seed_ready
|
|
615
|
+
filled_index = fill_vacated_seed_slot
|
|
616
|
+
|
|
373
617
|
@workers.keys.sort.each do |index|
|
|
374
618
|
return if @shutdown
|
|
619
|
+
next if index == filled_index
|
|
375
620
|
|
|
376
621
|
target_pid = @workers[index]
|
|
377
622
|
next unless target_pid
|
|
@@ -382,7 +627,8 @@ module Raptor
|
|
|
382
627
|
until @shutdown
|
|
383
628
|
reap_workers
|
|
384
629
|
current = @workers[index]
|
|
385
|
-
|
|
630
|
+
stat = @stats.all[index]
|
|
631
|
+
break if current && current != target_pid && stat[:pid] == current && stat[:booted]
|
|
386
632
|
break if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
387
633
|
|
|
388
634
|
sleep 0.1
|
|
@@ -395,6 +641,48 @@ module Raptor
|
|
|
395
641
|
end
|
|
396
642
|
end
|
|
397
643
|
|
|
644
|
+
# Blocks until the freshly-promoted seed has signalled readiness,
|
|
645
|
+
# or times out and clears the seed reference. No-op when no seed is
|
|
646
|
+
# being promoted.
|
|
647
|
+
#
|
|
648
|
+
# @return [void]
|
|
649
|
+
#
|
|
650
|
+
# @rbs () -> void
|
|
651
|
+
def wait_for_seed_ready
|
|
652
|
+
return unless @seed_pid && !@seed_ready
|
|
653
|
+
|
|
654
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @worker_drain_timeout + 5
|
|
655
|
+
until @seed_ready || @shutdown
|
|
656
|
+
break unless pid_alive?(@seed_pid)
|
|
657
|
+
break if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline
|
|
658
|
+
|
|
659
|
+
poll_seed_ready
|
|
660
|
+
sleep 0.1
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
return if @seed_ready
|
|
664
|
+
|
|
665
|
+
Log.warn "Seed (#{@seed_pid}) didn't signal ready in time, falling back to direct forks"
|
|
666
|
+
@seed_pid = nil
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
# Spawns a replacement worker for the slot the seed vacated when
|
|
670
|
+
# it was promoted, returning the slot index it filled.
|
|
671
|
+
#
|
|
672
|
+
# @return [Integer, nil] the filled slot index, or nil if none was vacated
|
|
673
|
+
#
|
|
674
|
+
# @rbs () -> Integer?
|
|
675
|
+
def fill_vacated_seed_slot
|
|
676
|
+
index = @seed_vacated_index
|
|
677
|
+
return unless index
|
|
678
|
+
|
|
679
|
+
@seed_vacated_index = nil
|
|
680
|
+
return if @shutdown
|
|
681
|
+
|
|
682
|
+
spawn_worker(index)
|
|
683
|
+
index
|
|
684
|
+
end
|
|
685
|
+
|
|
398
686
|
# Re-execs the master process with a fresh boot of the same Raptor
|
|
399
687
|
# invocation, handing the new master its listening sockets so accepted
|
|
400
688
|
# connections continue to be served across the swap.
|
|
@@ -425,11 +713,10 @@ module Raptor
|
|
|
425
713
|
exec(@launch_command, *@launch_argv)
|
|
426
714
|
end
|
|
427
715
|
|
|
428
|
-
# Runs
|
|
429
|
-
#
|
|
430
|
-
#
|
|
431
|
-
#
|
|
432
|
-
# critical component fails.
|
|
716
|
+
# Runs a worker process's full server stack until a shutdown signal is
|
|
717
|
+
# received or a critical component fails. On `SIGURG` the worker drains
|
|
718
|
+
# and transitions into a seed loop that forks replacement workers on
|
|
719
|
+
# master's request.
|
|
433
720
|
#
|
|
434
721
|
# @param index [Integer] slot index for this worker in the stats region
|
|
435
722
|
# @param phase [Integer] the cluster phase this worker was forked at
|
|
@@ -437,12 +724,17 @@ module Raptor
|
|
|
437
724
|
#
|
|
438
725
|
# @rbs (Integer index, Integer phase) -> void
|
|
439
726
|
def run_worker(index, phase)
|
|
727
|
+
@fork_w.close if @fork_w && !@fork_w.closed?
|
|
728
|
+
@resp_r.close if @resp_r && !@resp_r.closed?
|
|
729
|
+
|
|
440
730
|
shutdown_requested = false
|
|
731
|
+
promote_to_seed = false
|
|
441
732
|
trap("INT") { shutdown_requested = true }
|
|
442
733
|
trap("TERM") { shutdown_requested = true }
|
|
443
734
|
trap("HUP") { reopen_logs }
|
|
444
735
|
trap("USR1", "IGNORE")
|
|
445
736
|
trap("USR2", "IGNORE")
|
|
737
|
+
trap("URG") { promote_to_seed = true } if @fork_r
|
|
446
738
|
|
|
447
739
|
Raptor::CPU.pin(index) if Raptor::CPU.count >= @worker_count
|
|
448
740
|
|
|
@@ -529,6 +821,8 @@ module Raptor
|
|
|
529
821
|
)
|
|
530
822
|
server_thread = server.run
|
|
531
823
|
|
|
824
|
+
@before_worker_boot.each(&:call)
|
|
825
|
+
|
|
532
826
|
Log.info "Worker #{index} booted"
|
|
533
827
|
|
|
534
828
|
stats_thread = Thread.new do
|
|
@@ -547,19 +841,26 @@ module Raptor
|
|
|
547
841
|
last_checkin: Process.clock_gettime(Process::CLOCK_REALTIME),
|
|
548
842
|
booted: true
|
|
549
843
|
)
|
|
550
|
-
break if shutdown_requested
|
|
844
|
+
break if shutdown_requested || promote_to_seed
|
|
551
845
|
|
|
552
846
|
sleep 1
|
|
553
847
|
end
|
|
554
848
|
end
|
|
555
849
|
|
|
556
|
-
until shutdown_requested
|
|
850
|
+
until shutdown_requested || promote_to_seed
|
|
557
851
|
break unless server_thread.alive? && reactor_thread.alive?
|
|
558
852
|
|
|
559
853
|
sleep 0.5
|
|
560
854
|
end
|
|
561
855
|
|
|
562
|
-
|
|
856
|
+
if promote_to_seed
|
|
857
|
+
@before_refork.each(&:call)
|
|
858
|
+
server.stop_accepting
|
|
859
|
+
else
|
|
860
|
+
@before_worker_shutdown.each(&:call)
|
|
861
|
+
server.shutdown
|
|
862
|
+
end
|
|
863
|
+
|
|
563
864
|
server_thread.join
|
|
564
865
|
reactor.shutdown
|
|
565
866
|
reactor_thread.join
|
|
@@ -567,6 +868,44 @@ module Raptor
|
|
|
567
868
|
http1.shutdown
|
|
568
869
|
drain_thread_pool(thread_pool)
|
|
569
870
|
stats_thread.join
|
|
871
|
+
|
|
872
|
+
run_seed_loop(index) if promote_to_seed
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
# Runs the seed's fork loop, forking a replacement worker for each
|
|
876
|
+
# slot index the master asks for.
|
|
877
|
+
#
|
|
878
|
+
# @param index [Integer] the seed's original slot index
|
|
879
|
+
# @return [void]
|
|
880
|
+
#
|
|
881
|
+
# @rbs (Integer index) -> void
|
|
882
|
+
def run_seed_loop(index)
|
|
883
|
+
Log.info "Worker #{index} promoted to seed"
|
|
884
|
+
|
|
885
|
+
seed_shutdown = false
|
|
886
|
+
trap("INT") { seed_shutdown = true }
|
|
887
|
+
trap("TERM") { seed_shutdown = true }
|
|
888
|
+
trap("URG", "IGNORE")
|
|
889
|
+
|
|
890
|
+
child_pids = []
|
|
891
|
+
trap("CHLD") do
|
|
892
|
+
child_pids.reject! { Process.wait(_1, Process::WNOHANG) rescue true }
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
@resp_w.write([0].pack("L"))
|
|
896
|
+
|
|
897
|
+
until seed_shutdown
|
|
898
|
+
next unless @fork_r.wait_readable(1)
|
|
899
|
+
|
|
900
|
+
bytes = @fork_r.read_nonblock(8, exception: false)
|
|
901
|
+
break unless bytes.is_a?(String) && bytes.bytesize == 8
|
|
902
|
+
|
|
903
|
+
slot_index, child_phase = bytes.unpack("LL")
|
|
904
|
+
pid = fork { run_worker(slot_index, child_phase) }
|
|
905
|
+
child_pids << pid
|
|
906
|
+
@resp_w.write([pid].pack("L")) rescue nil
|
|
907
|
+
end
|
|
908
|
+
rescue Errno::EPIPE, IOError
|
|
570
909
|
end
|
|
571
910
|
|
|
572
911
|
# Shuts down the worker's application thread pool, force-killing the
|
data/lib/raptor/reuseport_bpf.rb
CHANGED
|
@@ -105,5 +105,18 @@ module Raptor
|
|
|
105
105
|
@load_value.setbyte(3, (backlog >> 24) & 0xff)
|
|
106
106
|
LibBPFRuby.map_update(@loads_fd, @load_key, @load_value)
|
|
107
107
|
end
|
|
108
|
+
|
|
109
|
+
# Marks a worker's slot as unavailable by parking its load at the
|
|
110
|
+
# sentinel maximum so the BPF program's least-loaded pick skips it.
|
|
111
|
+
#
|
|
112
|
+
# @param worker_index [Integer] slot index for the worker to mark
|
|
113
|
+
# @return [void]
|
|
114
|
+
#
|
|
115
|
+
# @rbs (Integer worker_index) -> void
|
|
116
|
+
def self.mark_unavailable(worker_index)
|
|
117
|
+
return unless @loads_fd
|
|
118
|
+
|
|
119
|
+
LibBPFRuby.map_update(@loads_fd, [worker_index + 1].pack("L"), [0xffffffff].pack("L"))
|
|
120
|
+
end
|
|
108
121
|
end
|
|
109
122
|
end
|
data/lib/raptor/server.rb
CHANGED
|
@@ -111,18 +111,26 @@ module Raptor
|
|
|
111
111
|
end
|
|
112
112
|
end
|
|
113
113
|
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
# `drain_accept_queue` is enabled, dispatches every connection already in
|
|
118
|
-
# the kernel accept queue before closing the listeners.
|
|
114
|
+
# Stops the accept loop and, when `drain_accept_queue` is enabled,
|
|
115
|
+
# dispatches every connection already in the kernel accept queue. Leaves
|
|
116
|
+
# the listening sockets open so they can be handed to replacement workers.
|
|
119
117
|
#
|
|
120
118
|
# @return [void]
|
|
121
119
|
#
|
|
122
120
|
# @rbs () -> void
|
|
123
|
-
def
|
|
121
|
+
def stop_accepting
|
|
124
122
|
@running.make_false
|
|
125
123
|
drain_accept_queue if @drain_accept_queue
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Gracefully shuts down the server, stopping the accept loop and closing
|
|
127
|
+
# all listening sockets.
|
|
128
|
+
#
|
|
129
|
+
# @return [void]
|
|
130
|
+
#
|
|
131
|
+
# @rbs () -> void
|
|
132
|
+
def shutdown
|
|
133
|
+
stop_accepting
|
|
126
134
|
@listeners.each(&:close)
|
|
127
135
|
end
|
|
128
136
|
|
data/lib/raptor/version.rb
CHANGED