raptor 0.25.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.
data/lib/raptor/binder.rb CHANGED
@@ -176,12 +176,18 @@ module Raptor
176
176
  #
177
177
  # @rbs () -> void
178
178
  def parse
179
- @uri_listeners = @bind_uris.to_h do |bind_uri|
179
+ @listeners = []
180
+ @uri_listeners = {}
181
+ @bind_uris.each do |bind_uri|
180
182
  uri = URI.parse(bind_uri)
181
183
  filenos = @inherited_fds[bind_uri]
182
- [bind_uri, filenos ? restore_listeners(uri, filenos) : create_listeners(uri)]
184
+ listeners = filenos ? restore_listeners(uri, filenos) : create_listeners(uri)
185
+ @uri_listeners[bind_uri] = listeners
186
+ @listeners.concat(listeners)
183
187
  end
184
- @listeners = @uri_listeners.values.flatten
188
+ rescue
189
+ close
190
+ raise
185
191
  end
186
192
 
187
193
  # Creates fresh listeners for the given URI.
@@ -241,7 +247,9 @@ module Raptor
241
247
  # @rbs (String? host, Integer? port) -> Array[TCPServer]
242
248
  def create_tcp_listeners(host, port)
243
249
  if host == "localhost"
244
- return loopback_addresses.flat_map { |address| create_tcp_listeners(address, port) }
250
+ listeners = []
251
+ loopback_addresses.each { |address| listeners.concat(create_tcp_listeners(address, port)) }
252
+ return listeners
245
253
  end
246
254
 
247
255
  host = host[1..-2] if host&.start_with?("[")
@@ -258,6 +266,10 @@ module Raptor
258
266
  tcp_server = TCPServer.for_fd(socket.fileno)
259
267
  socket.autoclose = false
260
268
  [tcp_server]
269
+ rescue
270
+ listeners&.each(&:close)
271
+ socket&.close
272
+ raise
261
273
  end
262
274
 
263
275
  # Creates a Unix domain server socket at the given path. Removes stale
@@ -306,8 +318,8 @@ module Raptor
306
318
  #
307
319
  # @rbs (String? host, Integer? port, Hash[String, String] ssl_params) -> Array[SslListener]
308
320
  def create_ssl_listeners(host, port, ssl_params)
309
- tcp_servers = create_tcp_listeners(host, port)
310
321
  ssl_context = build_ssl_context(ssl_params)
322
+ tcp_servers = create_tcp_listeners(host, port)
311
323
  tcp_servers.map { |tcp_server| SslListener.new(tcp_server: tcp_server, ssl_context: ssl_context) }
312
324
  end
313
325
 
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",
@@ -175,11 +178,38 @@ module Raptor
175
178
  #
176
179
  # @rbs () -> void
177
180
  def run
178
- @command == :stats ? run_stats : Cluster.run(@options)
181
+ if @command == :stats
182
+ run_stats
183
+ elsif @options[:workers].zero?
184
+ run_embedded
185
+ else
186
+ Cluster.run(@options)
187
+ end
179
188
  end
180
189
 
181
190
  private
182
191
 
192
+ # Serves in the CLI process until interrupted or terminated.
193
+ #
194
+ # @rbs () -> void
195
+ def run_embedded
196
+ require_relative "embedded"
197
+
198
+ $stdout.sync = true
199
+ $stderr.sync = true
200
+
201
+ Dir.chdir(@options[:chdir]) if @options[:chdir]
202
+ app = @options[:app] || Rack::Builder.parse_file(@options[:rackup])
203
+ server = Embedded.new(app, **@options, log: true).start
204
+ handlers = ["INT", "TERM"].to_h do |signal|
205
+ [signal, trap(signal) { Thread.new { server.shutdown } }]
206
+ end
207
+ server.join
208
+ ensure
209
+ server&.shutdown
210
+ handlers&.each { |signal, handler| trap(signal, handler) }
211
+ end
212
+
183
213
  # Reads and prints the stats file.
184
214
  #
185
215
  # @return [void]
@@ -282,7 +312,7 @@ module Raptor
282
312
  @options[:drain_accept_queue] = bool
283
313
  end
284
314
 
285
- opts.on("-w", "--workers NUM", Integer, "Number of worker processes (default: #{DEFAULT_WORKER_COUNT})") do |num|
315
+ opts.on("-w", "--workers NUM", Integer, "Number of worker processes, or 0 for in-process serving (default: #{DEFAULT_WORKER_COUNT})") do |num|
286
316
  @options[:workers] = num
287
317
  end
288
318
 
@@ -4,24 +4,19 @@
4
4
  require "json"
5
5
  require "time"
6
6
 
7
- require "atomic-ruby/atomic_thread_pool"
8
7
  require "concurrent/utility/processor_counter"
9
8
  require "rack/builder"
10
- require "ractor-pool"
11
9
 
12
10
  require_relative "binder"
13
11
  require_relative "control_server"
14
- require_relative "http1"
15
- require_relative "http2"
16
12
  require_relative "log"
17
13
  require_relative "plugin"
18
14
  require_relative "raptor_native"
19
- require_relative "reactor"
20
15
  require_relative "reuseport_bpf"
21
- require_relative "server"
22
16
  require_relative "stats"
23
17
  require_relative "systemd"
24
18
  require_relative "version"
19
+ require_relative "worker"
25
20
 
26
21
  module Raptor
27
22
  # Forks and supervises worker processes. Handles graceful shutdown on
@@ -73,26 +68,23 @@ module Raptor
73
68
  (cores.to_f / worker_count).round.clamp(1, HTTP2_RACTOR_COUNT_CAP)
74
69
  end
75
70
 
76
- # @rbs @drain_accept_queue: bool
77
71
  # @rbs @worker_count: Integer
78
72
  # @rbs @http1_ractor_count: Integer
79
73
  # @rbs @http2_ractor_count: Integer
80
74
  # @rbs @thread_count: Integer
81
75
  # @rbs @max_thread_count: (Integer | Float)?
82
76
  # @rbs @cpu_affinity: bool
83
- # @rbs @clean_thread_locals: bool
84
- # @rbs @clean_fiber_locals: bool
85
77
  # @rbs @environment: String
86
- # @rbs @connection_options: Hash[Symbol, untyped]
87
- # @rbs @http1_options: Hash[Symbol, untyped]
88
- # @rbs @http2_options: Hash[Symbol, untyped]
89
78
  # @rbs @worker_boot_timeout: Integer
90
79
  # @rbs @worker_timeout: Integer
91
80
  # @rbs @worker_drain_timeout: Integer
92
81
  # @rbs @worker_shutdown_timeout: Integer
93
82
  # @rbs @before_fork: Array[Proc]
83
+ # @rbs @after_fork: Array[Proc]
94
84
  # @rbs @before_worker_boot: Array[Proc]
95
85
  # @rbs @before_worker_shutdown: Array[Proc]
86
+ # @rbs @after_worker_drain: Array[Proc]
87
+ # @rbs @after_worker_exit: Array[Proc]
96
88
  # @rbs @before_refork: Array[Proc]
97
89
  # @rbs @stats_file: String?
98
90
  # @rbs @control_server: ControlServer?
@@ -103,12 +95,12 @@ module Raptor
103
95
  # @rbs @access_log_io: IO?
104
96
  # @rbs @launch_command: String?
105
97
  # @rbs @launch_argv: Array[String]?
106
- # @rbs @on_error: (^(Hash[String, untyped]?, Exception) -> void)?
107
98
  # @rbs @binder: Binder
108
99
  # @rbs @server_port: Integer
109
100
  # @rbs @app: untyped
110
101
  # @rbs @shutdown: bool
111
102
  # @rbs @workers: Hash[Integer, Integer]
103
+ # @rbs @worker_indices: Hash[Integer, Integer]
112
104
  # @rbs @timed_out: Set[Integer]
113
105
  # @rbs @stats: Stats
114
106
  # @rbs @started_at: Float
@@ -158,9 +150,12 @@ module Raptor
158
150
  # @option options [Integer] :worker_drain_timeout seconds a worker waits for in-flight requests during shutdown before force-killing app threads
159
151
  # @option options [Integer] :worker_shutdown_timeout seconds to wait for graceful worker exit before force-killing
160
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)
161
- # @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
162
155
  # @option options [Array<Proc>] :before_worker_boot procs called with the worker index before it begins serving
163
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
164
159
  # @option options [Array<Proc>] :before_refork procs called in a worker before it transitions to a seed
165
160
  # @option options [Array<String, Symbol>] :plugins plugins to load and start in order
166
161
  # @option options [String, nil] :stats_file path to write per-worker stats JSON, or nil to disable
@@ -178,26 +173,23 @@ module Raptor
178
173
  def initialize(options)
179
174
  @options = options = options.dup
180
175
  @plugins = []
181
- @drain_accept_queue = options[:drain_accept_queue]
182
176
  @worker_count = options[:workers]
183
177
  @http1_ractor_count = options[:http1][:ractors] || self.class.default_http1_ractor_count(@worker_count)
184
178
  @http2_ractor_count = options[:http2][:ractors] || self.class.default_http2_ractor_count(@worker_count)
185
179
  @thread_count = options[:threads]
186
180
  @max_thread_count = options[:max_threads]
187
181
  @cpu_affinity = options[:cpu_affinity]
188
- @clean_thread_locals = options[:clean_thread_locals]
189
- @clean_fiber_locals = options[:clean_fiber_locals]
190
182
  @environment = options[:environment] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
191
- @connection_options = options[:connection]
192
- @http1_options = options[:http1]
193
- @http2_options = options[:http2]
194
183
  @worker_boot_timeout = options[:worker_boot_timeout]
195
184
  @worker_timeout = options[:worker_timeout]
196
185
  @worker_drain_timeout = options[:worker_drain_timeout]
197
186
  @worker_shutdown_timeout = options[:worker_shutdown_timeout]
198
187
  @before_fork = options[:before_fork] = Array(options[:before_fork]).dup
188
+ @after_fork = options[:after_fork] = Array(options[:after_fork]).dup
199
189
  @before_worker_boot = options[:before_worker_boot] = Array(options[:before_worker_boot]).dup
200
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
201
193
  @before_refork = options[:before_refork] = Array(options[:before_refork]).dup
202
194
  @stats_file = options[:stats_file]
203
195
  @control_server = ControlServer.new(options[:control_url]) { stats_hash } if options[:control_url]
@@ -208,7 +200,6 @@ module Raptor
208
200
  @access_log_io = nil
209
201
  @launch_command = options[:launch_command]
210
202
  @launch_argv = options[:launch_argv]
211
- @on_error = options[:on_error]
212
203
 
213
204
  Dir.chdir(options[:chdir]) if options[:chdir]
214
205
 
@@ -227,6 +218,7 @@ module Raptor
227
218
 
228
219
  @shutdown = false
229
220
  @workers = {}
221
+ @worker_indices = {}
230
222
  @timed_out = Set.new
231
223
  @stats = Stats.new(@worker_count)
232
224
  @started_at = Process.clock_gettime(Process::CLOCK_REALTIME)
@@ -457,15 +449,34 @@ module Raptor
457
449
  pid = spawn_worker_via_seed(index)
458
450
  if pid
459
451
  @workers[index] = pid
452
+ @worker_indices[pid] = index
460
453
  return
461
454
  end
462
455
  Log.warn "Seed (#{@seed_pid}) failed to fork worker #{index}, falling back to direct fork"
463
456
  @seed_pid = nil
464
457
  end
465
458
 
466
- @before_fork.each(&:call)
467
- pid = fork { run_worker(index, @phase) }
468
- @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
469
480
  end
470
481
 
471
482
  # Asks the seed to fork a new worker at the given index, returning the
@@ -530,19 +541,20 @@ module Raptor
530
541
  #
531
542
  # @rbs (Integer pid, Process::Status status) -> void
532
543
  def reap_pid(pid, status)
544
+ index = @worker_indices.delete(pid)
545
+ return unless index
546
+
547
+ active = @workers[index] == pid
533
548
  if pid == @seed_pid
534
549
  Log.info "Seed (#{pid}) exited, #{exit_description(status)}"
535
550
  @seed_pid = nil
536
- return
537
551
  end
538
552
 
539
- index = @workers.key(pid)
540
- return unless index
541
-
542
- @workers.delete(index)
553
+ @workers.delete(index) if active
543
554
  @timed_out.delete(pid)
555
+ @after_worker_exit.each { |hook| hook.parameters.empty? ? hook.call : hook.call(index, pid, status) }
544
556
 
545
- unless @shutdown
557
+ if active && !@shutdown
546
558
  Log.warn "Restarting worker #{index} (#{pid}), #{exit_description(status)}"
547
559
  spawn_worker(index)
548
560
  end
@@ -674,7 +686,11 @@ module Raptor
674
686
  pids = @workers.values + [@seed_pid].compact
675
687
  Log.warn "Force-killing #{pids.size} process(es) after #{@worker_shutdown_timeout}s"
676
688
  pids.each { |pid| Process.kill("KILL", pid) rescue nil }
677
- 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
678
694
  end
679
695
 
680
696
  # Kills workers that have stopped checking in. A booted worker that
@@ -850,7 +866,6 @@ module Raptor
850
866
  CPU.pin(index) if @cpu_affinity && CPU.count >= @worker_count
851
867
 
852
868
  started_at = Process.clock_gettime(Process::CLOCK_REALTIME)
853
- request_count = 0
854
869
 
855
870
  @stats.write(
856
871
  index,
@@ -867,70 +882,6 @@ module Raptor
867
882
 
868
883
  @before_worker_boot.each { |hook| hook.parameters.empty? ? hook.call : hook.call(index) }
869
884
 
870
- reactor = nil
871
-
872
- counting_app = ->(env) {
873
- request_count += 1
874
- @app.call(env)
875
- }
876
- thread_pool = AtomicThreadPool.new(size: @thread_count, max_size: @max_thread_count)
877
- http1 = Http1.new(
878
- counting_app,
879
- @server_port,
880
- connection_options: @connection_options,
881
- http1_options: @http1_options,
882
- access_log_io: @access_log_io,
883
- clean_thread_locals: @clean_thread_locals,
884
- clean_fiber_locals: @clean_fiber_locals,
885
- on_error: @on_error
886
- )
887
- http2 = Http2.new(
888
- counting_app,
889
- @server_port,
890
- connection_options: @connection_options,
891
- http2_options: @http2_options,
892
- access_log_io: @access_log_io,
893
- clean_thread_locals: @clean_thread_locals,
894
- clean_fiber_locals: @clean_fiber_locals,
895
- on_error: @on_error
896
- )
897
- http1_ractor_pool = RactorPool.new(
898
- size: @http1_ractor_count,
899
- worker: http1.parser_worker,
900
- name: "HTTP/1.1"
901
- ) do |parsed_result|
902
- begin
903
- http1.handle_parsed_request(parsed_result, reactor, thread_pool)
904
- rescue => error
905
- Log.rescued_error(error)
906
- end
907
- end
908
-
909
- http2_ractor_pool = if @binder.http2?
910
- RactorPool.new(
911
- size: @http2_ractor_count,
912
- worker: http2.parser_worker,
913
- name: "HTTP/2"
914
- ) do |parsed_result|
915
- begin
916
- http2.handle_parsed_request(parsed_result, reactor, thread_pool)
917
- rescue => error
918
- Log.rescued_error(error)
919
- end
920
- end
921
- end
922
-
923
- reactor = Reactor.new(
924
- http1,
925
- http1_ractor_pool,
926
- http2_ractor_pool,
927
- thread_pool,
928
- connection_options: @connection_options,
929
- http1_options: @http1_options,
930
- http2_options: @http2_options
931
- )
932
- reactor_thread = reactor.run
933
-
934
885
  bpf_listeners = if @bpf_active
935
886
  ReuseportBPF.enable_load_reporting(index)
936
887
  ReuseportBPF.create_worker_listeners(index, @binder.socket_backlog)
@@ -938,17 +889,17 @@ module Raptor
938
889
  []
939
890
  end
940
891
 
941
- server = Server.new(
942
- reactor,
943
- thread_pool,
944
- http1,
945
- http2,
946
- connection_options: @connection_options,
947
- drain_accept_queue: @drain_accept_queue,
892
+ worker = Worker.new(
893
+ @app,
948
894
  listeners: bpf_listeners + @binder.listeners,
895
+ port: @server_port,
896
+ options: @options,
897
+ http1_ractors: @http1_ractor_count,
898
+ http2_ractors: @http2_ractor_count,
899
+ access_log_io: @access_log_io,
949
900
  worker_index: (index if @bpf_active)
950
901
  )
951
- server_thread = server.run
902
+ worker.start
952
903
 
953
904
  Log.info "Worker #{index} booted"
954
905
 
@@ -960,10 +911,10 @@ module Raptor
960
911
  index,
961
912
  pid: Process.pid,
962
913
  phase: phase,
963
- requests: request_count,
964
- backlog: reactor.backlog,
965
- busy_threads: thread_pool.active_count,
966
- thread_capacity: thread_pool.size,
914
+ requests: worker.requests,
915
+ backlog: worker.reactor.backlog,
916
+ busy_threads: worker.thread_pool.active_count,
917
+ thread_capacity: worker.thread_pool.size,
967
918
  started_at: started_at,
968
919
  last_checkin: Process.clock_gettime(Process::CLOCK_REALTIME),
969
920
  booted: true
@@ -975,30 +926,21 @@ module Raptor
975
926
  end
976
927
 
977
928
  until shutdown_requested || promote_to_seed
978
- break unless server_thread.alive? && reactor_thread.alive?
929
+ break unless worker.running?
979
930
 
980
931
  sleep 0.5
981
932
  end
982
933
 
983
934
  if promote_to_seed
984
935
  @before_refork.each(&:call)
985
- server.stop_accepting
986
936
  else
987
937
  @before_worker_shutdown.each { |hook| hook.parameters.empty? ? hook.call : hook.call(index) }
988
- server.shutdown
989
938
  end
990
939
 
991
- server_thread.join
940
+ worker.shutdown(close_listeners: !promote_to_seed)
941
+ @after_worker_drain.each { |hook| hook.parameters.empty? ? hook.call : hook.call(index) }
992
942
  # A seed never accepts again, and the workers it forks bind their own.
993
943
  bpf_listeners.each(&:close) if promote_to_seed
994
- http1.shutdown
995
- http2.shutdown(reactor)
996
- reactor.drain_detached_bodies(@worker_drain_timeout)
997
- drain_thread_pool(thread_pool)
998
- reactor.shutdown
999
- reactor_thread.join
1000
- http1_ractor_pool.shutdown
1001
- http2_ractor_pool&.shutdown
1002
944
  stats_thread.join
1003
945
 
1004
946
  run_seed_loop(index) if promote_to_seed
@@ -1041,27 +983,6 @@ module Raptor
1041
983
  rescue Errno::EPIPE, IOError
1042
984
  end
1043
985
 
1044
- # Shuts down the worker's application thread pool, force-killing the
1045
- # underlying threads if in-flight requests have not finished within
1046
- # `worker_drain_timeout` seconds.
1047
- #
1048
- # @param thread_pool [AtomicThreadPool] the worker's thread pool
1049
- # @return [void]
1050
- #
1051
- # @rbs (AtomicThreadPool thread_pool) -> void
1052
- def drain_thread_pool(thread_pool)
1053
- drain_thread = Thread.new do
1054
- Thread.current.name = "Thread Pool Drain"
1055
-
1056
- thread_pool.shutdown
1057
- end
1058
- return if drain_thread.join(@worker_drain_timeout)
1059
-
1060
- Log.warn "Force-killing in-flight app threads after #{@worker_drain_timeout}s drain timeout"
1061
- thread_pool.instance_variable_get(:@threads).each(&:kill)
1062
- drain_thread.join
1063
- end
1064
-
1065
986
  # Returns a human-readable description of how a process exited.
1066
987
  #
1067
988
  # @param status [Process::Status] the exit status of the process