raptor 0.25.0 → 0.26.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/cli.rb CHANGED
@@ -175,11 +175,38 @@ module Raptor
175
175
  #
176
176
  # @rbs () -> void
177
177
  def run
178
- @command == :stats ? run_stats : Cluster.run(@options)
178
+ if @command == :stats
179
+ run_stats
180
+ elsif @options[:workers].zero?
181
+ run_embedded
182
+ else
183
+ Cluster.run(@options)
184
+ end
179
185
  end
180
186
 
181
187
  private
182
188
 
189
+ # Serves in the CLI process until interrupted or terminated.
190
+ #
191
+ # @rbs () -> void
192
+ def run_embedded
193
+ require_relative "embedded"
194
+
195
+ $stdout.sync = true
196
+ $stderr.sync = true
197
+
198
+ Dir.chdir(@options[:chdir]) if @options[:chdir]
199
+ app = @options[:app] || Rack::Builder.parse_file(@options[:rackup])
200
+ server = Embedded.new(app, **@options, log: true).start
201
+ handlers = ["INT", "TERM"].to_h do |signal|
202
+ [signal, trap(signal) { Thread.new { server.shutdown } }]
203
+ end
204
+ server.join
205
+ ensure
206
+ server&.shutdown
207
+ handlers&.each { |signal, handler| trap(signal, handler) }
208
+ end
209
+
183
210
  # Reads and prints the stats file.
184
211
  #
185
212
  # @return [void]
@@ -282,7 +309,7 @@ module Raptor
282
309
  @options[:drain_accept_queue] = bool
283
310
  end
284
311
 
285
- opts.on("-w", "--workers NUM", Integer, "Number of worker processes (default: #{DEFAULT_WORKER_COUNT})") do |num|
312
+ opts.on("-w", "--workers NUM", Integer, "Number of worker processes, or 0 for in-process serving (default: #{DEFAULT_WORKER_COUNT})") do |num|
286
313
  @options[:workers] = num
287
314
  end
288
315
 
@@ -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,19 +68,13 @@ 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
@@ -103,7 +92,6 @@ module Raptor
103
92
  # @rbs @access_log_io: IO?
104
93
  # @rbs @launch_command: String?
105
94
  # @rbs @launch_argv: Array[String]?
106
- # @rbs @on_error: (^(Hash[String, untyped]?, Exception) -> void)?
107
95
  # @rbs @binder: Binder
108
96
  # @rbs @server_port: Integer
109
97
  # @rbs @app: untyped
@@ -178,19 +166,13 @@ module Raptor
178
166
  def initialize(options)
179
167
  @options = options = options.dup
180
168
  @plugins = []
181
- @drain_accept_queue = options[:drain_accept_queue]
182
169
  @worker_count = options[:workers]
183
170
  @http1_ractor_count = options[:http1][:ractors] || self.class.default_http1_ractor_count(@worker_count)
184
171
  @http2_ractor_count = options[:http2][:ractors] || self.class.default_http2_ractor_count(@worker_count)
185
172
  @thread_count = options[:threads]
186
173
  @max_thread_count = options[:max_threads]
187
174
  @cpu_affinity = options[:cpu_affinity]
188
- @clean_thread_locals = options[:clean_thread_locals]
189
- @clean_fiber_locals = options[:clean_fiber_locals]
190
175
  @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
176
  @worker_boot_timeout = options[:worker_boot_timeout]
195
177
  @worker_timeout = options[:worker_timeout]
196
178
  @worker_drain_timeout = options[:worker_drain_timeout]
@@ -208,7 +190,6 @@ module Raptor
208
190
  @access_log_io = nil
209
191
  @launch_command = options[:launch_command]
210
192
  @launch_argv = options[:launch_argv]
211
- @on_error = options[:on_error]
212
193
 
213
194
  Dir.chdir(options[:chdir]) if options[:chdir]
214
195
 
@@ -850,7 +831,6 @@ module Raptor
850
831
  CPU.pin(index) if @cpu_affinity && CPU.count >= @worker_count
851
832
 
852
833
  started_at = Process.clock_gettime(Process::CLOCK_REALTIME)
853
- request_count = 0
854
834
 
855
835
  @stats.write(
856
836
  index,
@@ -867,70 +847,6 @@ module Raptor
867
847
 
868
848
  @before_worker_boot.each { |hook| hook.parameters.empty? ? hook.call : hook.call(index) }
869
849
 
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
850
  bpf_listeners = if @bpf_active
935
851
  ReuseportBPF.enable_load_reporting(index)
936
852
  ReuseportBPF.create_worker_listeners(index, @binder.socket_backlog)
@@ -938,17 +854,17 @@ module Raptor
938
854
  []
939
855
  end
940
856
 
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,
857
+ worker = Worker.new(
858
+ @app,
948
859
  listeners: bpf_listeners + @binder.listeners,
860
+ port: @server_port,
861
+ options: @options,
862
+ http1_ractors: @http1_ractor_count,
863
+ http2_ractors: @http2_ractor_count,
864
+ access_log_io: @access_log_io,
949
865
  worker_index: (index if @bpf_active)
950
866
  )
951
- server_thread = server.run
867
+ worker.start
952
868
 
953
869
  Log.info "Worker #{index} booted"
954
870
 
@@ -960,10 +876,10 @@ module Raptor
960
876
  index,
961
877
  pid: Process.pid,
962
878
  phase: phase,
963
- requests: request_count,
964
- backlog: reactor.backlog,
965
- busy_threads: thread_pool.active_count,
966
- thread_capacity: thread_pool.size,
879
+ requests: worker.requests,
880
+ backlog: worker.reactor.backlog,
881
+ busy_threads: worker.thread_pool.active_count,
882
+ thread_capacity: worker.thread_pool.size,
967
883
  started_at: started_at,
968
884
  last_checkin: Process.clock_gettime(Process::CLOCK_REALTIME),
969
885
  booted: true
@@ -975,30 +891,20 @@ module Raptor
975
891
  end
976
892
 
977
893
  until shutdown_requested || promote_to_seed
978
- break unless server_thread.alive? && reactor_thread.alive?
894
+ break unless worker.running?
979
895
 
980
896
  sleep 0.5
981
897
  end
982
898
 
983
899
  if promote_to_seed
984
900
  @before_refork.each(&:call)
985
- server.stop_accepting
986
901
  else
987
902
  @before_worker_shutdown.each { |hook| hook.parameters.empty? ? hook.call : hook.call(index) }
988
- server.shutdown
989
903
  end
990
904
 
991
- server_thread.join
905
+ worker.shutdown(close_listeners: !promote_to_seed)
992
906
  # A seed never accepts again, and the workers it forks bind their own.
993
907
  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
908
  stats_thread.join
1003
909
 
1004
910
  run_seed_loop(index) if promote_to_seed
@@ -1041,27 +947,6 @@ module Raptor
1041
947
  rescue Errno::EPIPE, IOError
1042
948
  end
1043
949
 
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
950
  # Returns a human-readable description of how a process exited.
1066
951
  #
1067
952
  # @param status [Process::Status] the exit status of the process
@@ -0,0 +1,159 @@
1
+ # rbs_inline: enabled
2
+ # frozen_string_literal: true
3
+
4
+ require "atomic-ruby/atom"
5
+ require "atomic-ruby/atomic_condition_variable"
6
+
7
+ require_relative "cli"
8
+ require_relative "worker"
9
+
10
+ module Raptor
11
+ # Serves an existing Rack application in the caller's process without
12
+ # installing signal handlers or changing process-wide configuration.
13
+ #
14
+ class Embedded
15
+ # @rbs @app: untyped
16
+ # @rbs @options: Hash[Symbol, untyped]
17
+ # @rbs @binds: Array[String]
18
+ # @rbs @log: bool
19
+ # @rbs @lifecycle_owner: Atom
20
+ # @rbs @lifecycle_available: AtomicConditionVariable
21
+ # @rbs @started: bool
22
+ # @rbs @stopped: bool
23
+ # @rbs @binder: Binder?
24
+ # @rbs @worker: Worker?
25
+
26
+ attr_reader :port #: Integer
27
+ attr_reader :addresses #: Array[String]
28
+
29
+ # Creates an in-process server. A port of zero selects an available port.
30
+ # Thread and protocol options follow the regular server configuration.
31
+ # Set log to report startup information.
32
+ #
33
+ # @rbs (untyped app, ?host: String, ?port: Integer, ?log: bool, **untyped options) -> void
34
+ def initialize(app, host: "127.0.0.1", port: 0, log: false, **options)
35
+ @app = app
36
+ @port = port
37
+ @log = log
38
+ @addresses = []
39
+ @options = CLI::DEFAULT_OPTIONS.merge(options)
40
+ CLI::NESTED_OPTION_KEYS.each do |key|
41
+ @options[key] = CLI::DEFAULT_OPTIONS[key].merge(options[key] || {})
42
+ end
43
+ @options[:max_threads] = CLI.parse_max_threads(@options[:max_threads], threads: @options[:threads])
44
+ @binds = options[:binds] || ["tcp://#{host}:#{port}"]
45
+ @lifecycle_owner = Atom.new(nil)
46
+ @lifecycle_available = AtomicConditionVariable.new
47
+ @started = false
48
+ @stopped = false
49
+ end
50
+
51
+ # Starts serving and returns once the request-handling stack is ready.
52
+ # Startup errors are raised in the calling thread.
53
+ #
54
+ # @rbs () -> Embedded
55
+ def start
56
+ with_lifecycle do
57
+ raise ThreadError, "server has already been started or stopped" if @started || @stopped
58
+
59
+ @started = true
60
+ begin
61
+ @binder = Binder.new(@binds, socket_backlog: @options[:socket_backlog])
62
+ @port = @binder.server_port
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
73
+ rescue
74
+ @stopped = true
75
+ @binder&.close
76
+ raise
77
+ end
78
+ log_initialization if @log
79
+ end
80
+
81
+ self
82
+ end
83
+
84
+ # Starts serving and waits until shutdown.
85
+ #
86
+ # @rbs () -> void
87
+ def run
88
+ start
89
+ join
90
+ end
91
+
92
+ # Drains requests and closes the server's sockets, threads, and Ractors.
93
+ # Calling shutdown more than once has no effect.
94
+ #
95
+ # @rbs () -> void
96
+ def shutdown
97
+ with_lifecycle do
98
+ return if @stopped
99
+
100
+ @stopped = true
101
+ begin
102
+ @worker&.shutdown
103
+ ensure
104
+ @binder&.close
105
+ end
106
+ end
107
+ end
108
+
109
+ # Waits for termination and propagates serving thread failures.
110
+ #
111
+ # @rbs () -> void
112
+ def join
113
+ worker = with_lifecycle { @worker }
114
+ raise ThreadError, "server has not been started" unless worker
115
+
116
+ worker.join
117
+ ensure
118
+ shutdown if worker
119
+ end
120
+
121
+ private
122
+
123
+ # @rbs () -> void
124
+ def log_initialization
125
+ environment = @options[:environment] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
126
+ thread_count = @options[:threads]
127
+ max_threads = @options[:max_threads]
128
+ thread_limit = if max_threads == Float::INFINITY
129
+ " (scaling, no limit)"
130
+ elsif max_threads > thread_count
131
+ " (scaling up to #{max_threads})"
132
+ else
133
+ ""
134
+ end
135
+
136
+ Log.info "Server initializing:"
137
+ Log.info "├─ Version: #{VERSION}"
138
+ Log.info "├─ Ruby Version: #{RUBY_DESCRIPTION}"
139
+ Log.info "├─ Environment: #{environment}"
140
+ Log.info "├─ PID: #{Process.pid}"
141
+ Log.info "├─ #{thread_count} worker thread#{"s" if thread_count > 1}#{thread_limit}"
142
+ Log.info "└─ Listening on #{@addresses.join(", ")}"
143
+ end
144
+
145
+ # @rbs () { () -> untyped } -> untyped
146
+ def with_lifecycle
147
+ thread = Thread.current
148
+ @lifecycle_available.wait do
149
+ @lifecycle_owner.swap { |owner| owner || thread }.equal?(thread)
150
+ end
151
+ yield
152
+ ensure
153
+ if @lifecycle_owner.value.equal?(thread)
154
+ @lifecycle_owner.swap { nil }
155
+ @lifecycle_available.broadcast
156
+ end
157
+ end
158
+ end
159
+ end
@@ -263,12 +263,21 @@ module Raptor
263
263
  #
264
264
  # @rbs () -> Thread
265
265
  def run
266
- Thread.new do
266
+ ready = AtomicConditionVariable.new
267
+ startup_error = nil
268
+ thread = Thread.new do
267
269
  Thread.current.name = "Reactor"
268
270
  @thread = Thread.current
269
- selector = IO::Event::Selector.new(Fiber.current)
270
- @wakeup = Fiber.new { loop { selector.transfer } }
271
- @selector = selector
271
+ begin
272
+ selector = IO::Event::Selector.new(Fiber.current)
273
+ @wakeup = Fiber.new { loop { selector.transfer } }
274
+ @selector = selector
275
+ rescue => error
276
+ startup_error = error
277
+ ready.signal
278
+ next
279
+ end
280
+ ready.signal
272
281
 
273
282
  until stopped?
274
283
  begin
@@ -310,6 +319,14 @@ module Raptor
310
319
  @thread = nil
311
320
  @selector.close
312
321
  end
322
+
323
+ ready.wait { @selector || startup_error }
324
+ if startup_error
325
+ thread.join
326
+ raise startup_error
327
+ end
328
+
329
+ thread
313
330
  end
314
331
 
315
332
  # Adds a new client connection to the reactor.
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Raptor
5
- VERSION = "0.25.0"
5
+ VERSION = "0.26.0"
6
6
  end