raptor 0.19.0 → 0.20.1
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/CHANGELOG.md +16 -0
- data/README.md +65 -39
- data/docs/brisrails-talk.md +10 -7
- data/docs/raptor-vs-puma.md +31 -22
- data/lib/rackup/handler/raptor.rb +12 -3
- data/lib/raptor/cli.rb +35 -2
- data/lib/raptor/cluster.rb +78 -5
- data/lib/raptor/control_server.rb +131 -0
- data/lib/raptor/http1.rb +32 -12
- data/lib/raptor/http2.rb +22 -2
- data/lib/raptor/thread_locals.rb +38 -0
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/cli.rbs +8 -0
- data/sig/generated/raptor/cluster.rbs +42 -16
- data/sig/generated/raptor/control_server.rbs +62 -0
- data/sig/generated/raptor/http1.rbs +16 -6
- data/sig/generated/raptor/http2.rbs +13 -2
- data/sig/generated/raptor/thread_locals.rbs +24 -0
- metadata +5 -1
data/lib/raptor/cluster.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
require "concurrent/utility/processor_counter"
|
|
5
5
|
require "json"
|
|
6
|
+
require "time"
|
|
6
7
|
|
|
7
8
|
require "atomic-ruby/atomic_thread_pool"
|
|
8
9
|
require "rack/builder"
|
|
@@ -10,6 +11,7 @@ require "ractor-pool"
|
|
|
10
11
|
|
|
11
12
|
require_relative "log"
|
|
12
13
|
require_relative "binder"
|
|
14
|
+
require_relative "control_server"
|
|
13
15
|
require_relative "reactor"
|
|
14
16
|
require_relative "server"
|
|
15
17
|
require_relative "reuseport_bpf"
|
|
@@ -74,6 +76,9 @@ module Raptor
|
|
|
74
76
|
# @rbs @http2_ractor_count: Integer
|
|
75
77
|
# @rbs @thread_count: Integer
|
|
76
78
|
# @rbs @max_thread_count: (Integer | Float)?
|
|
79
|
+
# @rbs @cpu_affinity: bool
|
|
80
|
+
# @rbs @clean_thread_locals: bool
|
|
81
|
+
# @rbs @clean_fiber_locals: bool
|
|
77
82
|
# @rbs @environment: String
|
|
78
83
|
# @rbs @connection_options: Hash[Symbol, untyped]
|
|
79
84
|
# @rbs @http1_options: Hash[Symbol, untyped]
|
|
@@ -87,6 +92,7 @@ module Raptor
|
|
|
87
92
|
# @rbs @before_worker_shutdown: Array[Proc]
|
|
88
93
|
# @rbs @before_refork: Array[Proc]
|
|
89
94
|
# @rbs @stats_file: String?
|
|
95
|
+
# @rbs @control_server: ControlServer?
|
|
90
96
|
# @rbs @pid_file: String?
|
|
91
97
|
# @rbs @stdout_file: String?
|
|
92
98
|
# @rbs @stderr_file: String?
|
|
@@ -102,6 +108,7 @@ module Raptor
|
|
|
102
108
|
# @rbs @workers: Hash[Integer, Integer]
|
|
103
109
|
# @rbs @timed_out: Set[Integer]
|
|
104
110
|
# @rbs @stats: Stats
|
|
111
|
+
# @rbs @started_at: Float
|
|
105
112
|
# @rbs @phase: Integer
|
|
106
113
|
# @rbs @phased_restart_requested: bool
|
|
107
114
|
# @rbs @phased_restarting: bool
|
|
@@ -127,6 +134,9 @@ module Raptor
|
|
|
127
134
|
# @option options [Integer] :workers number of worker processes
|
|
128
135
|
# @option options [Integer] :threads number of threads per worker process
|
|
129
136
|
# @option options [Integer, Float] :max_threads maximum number of threads per worker process, or `Float::INFINITY` for no limit
|
|
137
|
+
# @option options [Boolean] :cpu_affinity whether to pin each worker to a CPU
|
|
138
|
+
# @option options [Boolean] :clean_thread_locals whether to clear application thread locals after each request
|
|
139
|
+
# @option options [Boolean] :clean_fiber_locals whether to process each request in a fresh Fiber
|
|
130
140
|
# @option options [#call] :app pre-built Rack application
|
|
131
141
|
# @option options [String] :rackup path to Rack configuration file
|
|
132
142
|
# @option options [String, nil] :chdir directory to change to before loading the Rack application, or nil to leave the working directory unchanged
|
|
@@ -140,10 +150,11 @@ module Raptor
|
|
|
140
150
|
# @option options [Integer] :worker_shutdown_timeout seconds to wait for graceful worker exit before force-killing
|
|
141
151
|
# @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)
|
|
142
152
|
# @option options [Array<Proc>] :before_fork procs called in the master before every worker fork
|
|
143
|
-
# @option options [Array<Proc>] :before_worker_boot procs called
|
|
144
|
-
# @option options [Array<Proc>] :before_worker_shutdown procs called
|
|
153
|
+
# @option options [Array<Proc>] :before_worker_boot procs called with the worker index before it begins serving
|
|
154
|
+
# @option options [Array<Proc>] :before_worker_shutdown procs called with the worker index before its graceful shutdown
|
|
145
155
|
# @option options [Array<Proc>] :before_refork procs called in a worker before it transitions to a seed
|
|
146
156
|
# @option options [String, nil] :stats_file path to write per-worker stats JSON, or nil to disable
|
|
157
|
+
# @option options [String, nil] :control_url `unix://` URL serving cluster stats, or nil to disable
|
|
147
158
|
# @option options [String, nil] :pid_file path to write the master PID to, or nil to disable
|
|
148
159
|
# @option options [String, nil] :stdout_file path to redirect stdout to, reopened on SIGHUP, or nil to disable
|
|
149
160
|
# @option options [String, nil] :stderr_file path to redirect stderr to, reopened on SIGHUP, or nil to disable
|
|
@@ -161,6 +172,9 @@ module Raptor
|
|
|
161
172
|
@http2_ractor_count = options[:http2][:ractors] || self.class.default_http2_ractor_count(@worker_count)
|
|
162
173
|
@thread_count = options[:threads]
|
|
163
174
|
@max_thread_count = options[:max_threads]
|
|
175
|
+
@cpu_affinity = options[:cpu_affinity]
|
|
176
|
+
@clean_thread_locals = options[:clean_thread_locals]
|
|
177
|
+
@clean_fiber_locals = options[:clean_fiber_locals]
|
|
164
178
|
@environment = options[:environment] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
|
|
165
179
|
@connection_options = options[:connection]
|
|
166
180
|
@http1_options = options[:http1]
|
|
@@ -174,6 +188,7 @@ module Raptor
|
|
|
174
188
|
@before_worker_shutdown = Array(options[:before_worker_shutdown])
|
|
175
189
|
@before_refork = Array(options[:before_refork])
|
|
176
190
|
@stats_file = options[:stats_file]
|
|
191
|
+
@control_server = ControlServer.new(options[:control_url]) { stats_hash } if options[:control_url]
|
|
177
192
|
@pid_file = options[:pid_file]
|
|
178
193
|
@stdout_file = options[:stdout_file]
|
|
179
194
|
@stderr_file = options[:stderr_file]
|
|
@@ -202,6 +217,7 @@ module Raptor
|
|
|
202
217
|
@workers = {}
|
|
203
218
|
@timed_out = Set.new
|
|
204
219
|
@stats = Stats.new(@worker_count)
|
|
220
|
+
@started_at = Process.clock_gettime(Process::CLOCK_REALTIME)
|
|
205
221
|
@phase = 0
|
|
206
222
|
@phased_restart_requested = false
|
|
207
223
|
@phased_restarting = false
|
|
@@ -247,7 +263,9 @@ module Raptor
|
|
|
247
263
|
|
|
248
264
|
@bpf_active = ReuseportBPF.setup(@worker_count)
|
|
249
265
|
|
|
266
|
+
@control_server&.bind
|
|
250
267
|
@worker_count.times { |index| spawn_worker(index) }
|
|
268
|
+
@control_server&.start
|
|
251
269
|
|
|
252
270
|
stats_file_thread = if @stats_file
|
|
253
271
|
Thread.new do
|
|
@@ -274,6 +292,7 @@ module Raptor
|
|
|
274
292
|
Systemd.notify("STOPPING=1")
|
|
275
293
|
stop_workers
|
|
276
294
|
stats_file_thread&.join
|
|
295
|
+
@control_server&.shutdown
|
|
277
296
|
File.delete(@stats_file) rescue nil if @stats_file
|
|
278
297
|
File.delete(@pid_file) rescue nil if @pid_file
|
|
279
298
|
@stats.unmap
|
|
@@ -291,8 +310,57 @@ module Raptor
|
|
|
291
310
|
@stats.all
|
|
292
311
|
end
|
|
293
312
|
|
|
313
|
+
# Returns cluster statistics for the control server.
|
|
314
|
+
# For an adaptive pool, `max_threads` is the number of threads currently
|
|
315
|
+
# running so utilization reflects the capacity available at that moment.
|
|
316
|
+
#
|
|
317
|
+
# @return [Hash] cluster statistics
|
|
318
|
+
#
|
|
319
|
+
# @rbs () -> Hash[Symbol, untyped]
|
|
320
|
+
def stats_hash
|
|
321
|
+
worker_stats = @stats.all
|
|
322
|
+
worker_status = @workers.sort.map do |index, pid|
|
|
323
|
+
stat = worker_stats[index]
|
|
324
|
+
stat = {} unless stat && stat[:pid] == pid
|
|
325
|
+
capacity = stat.fetch(:thread_capacity, 0)
|
|
326
|
+
active = stat.fetch(:busy_threads, 0)
|
|
327
|
+
total_work = stat.fetch(:backlog, 0)
|
|
328
|
+
|
|
329
|
+
{
|
|
330
|
+
started_at: timestamp(stat.fetch(:started_at, 0)),
|
|
331
|
+
pid: pid,
|
|
332
|
+
index: index,
|
|
333
|
+
phase: stat.fetch(:phase, @phase),
|
|
334
|
+
booted: stat.fetch(:booted, false),
|
|
335
|
+
last_checkin: timestamp(stat.fetch(:last_checkin, 0)),
|
|
336
|
+
last_status: {
|
|
337
|
+
backlog: [total_work - active, 0].max,
|
|
338
|
+
running: capacity,
|
|
339
|
+
pool_capacity: [capacity - total_work, 0].max,
|
|
340
|
+
busy_threads: active,
|
|
341
|
+
max_threads: capacity,
|
|
342
|
+
requests_count: stat.fetch(:requests, 0),
|
|
343
|
+
},
|
|
344
|
+
}
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
{
|
|
348
|
+
started_at: timestamp(@started_at),
|
|
349
|
+
workers: worker_status.length,
|
|
350
|
+
phase: @phase,
|
|
351
|
+
booted_workers: worker_status.count { |worker| worker[:booted] },
|
|
352
|
+
old_workers: worker_status.count { |worker| worker[:phase] != @phase },
|
|
353
|
+
worker_status: worker_status,
|
|
354
|
+
}
|
|
355
|
+
end
|
|
356
|
+
|
|
294
357
|
private
|
|
295
358
|
|
|
359
|
+
# @rbs (Float timestamp) -> String
|
|
360
|
+
def timestamp(timestamp)
|
|
361
|
+
Time.at(timestamp).utc.iso8601(6)
|
|
362
|
+
end
|
|
363
|
+
|
|
296
364
|
# Returns the inherited-FDs hash for a systemd socket-activation handoff,
|
|
297
365
|
# pairing each bind URI with the FD systemd passed at the same index.
|
|
298
366
|
# Returns an empty hash when the FD count doesn't match the bind count.
|
|
@@ -700,6 +768,7 @@ module Raptor
|
|
|
700
768
|
stop_workers
|
|
701
769
|
@binder.clear_close_on_exec
|
|
702
770
|
ENV[INHERITED_FDS_ENV] = JSON.generate(@binder.inheritable_fds)
|
|
771
|
+
@control_server&.shutdown
|
|
703
772
|
File.delete(@stats_file) rescue nil if @stats_file
|
|
704
773
|
File.delete(@pid_file) rescue nil if @pid_file
|
|
705
774
|
@stats.unmap
|
|
@@ -731,7 +800,7 @@ module Raptor
|
|
|
731
800
|
trap("USR2", "IGNORE")
|
|
732
801
|
trap("URG") { promote_to_seed = true } if @fork_r
|
|
733
802
|
|
|
734
|
-
Raptor::CPU.pin(index) if Raptor::CPU.count >= @worker_count
|
|
803
|
+
Raptor::CPU.pin(index) if @cpu_affinity && Raptor::CPU.count >= @worker_count
|
|
735
804
|
|
|
736
805
|
started_at = Process.clock_gettime(Process::CLOCK_REALTIME)
|
|
737
806
|
request_count = 0
|
|
@@ -762,6 +831,8 @@ module Raptor
|
|
|
762
831
|
connection_options: @connection_options,
|
|
763
832
|
http1_options: @http1_options,
|
|
764
833
|
access_log_io: @access_log_io,
|
|
834
|
+
clean_thread_locals: @clean_thread_locals,
|
|
835
|
+
clean_fiber_locals: @clean_fiber_locals,
|
|
765
836
|
on_error: @on_error
|
|
766
837
|
)
|
|
767
838
|
http2 = Http2.new(
|
|
@@ -770,6 +841,8 @@ module Raptor
|
|
|
770
841
|
connection_options: @connection_options,
|
|
771
842
|
http2_options: @http2_options,
|
|
772
843
|
access_log_io: @access_log_io,
|
|
844
|
+
clean_thread_locals: @clean_thread_locals,
|
|
845
|
+
clean_fiber_locals: @clean_fiber_locals,
|
|
773
846
|
on_error: @on_error
|
|
774
847
|
)
|
|
775
848
|
http1_ractor_pool = RactorPool.new(
|
|
@@ -829,7 +902,7 @@ module Raptor
|
|
|
829
902
|
)
|
|
830
903
|
server_thread = server.run
|
|
831
904
|
|
|
832
|
-
@before_worker_boot.each(
|
|
905
|
+
@before_worker_boot.each { |hook| hook.parameters.empty? ? hook.call : hook.call(index) }
|
|
833
906
|
|
|
834
907
|
Log.info "Worker #{index} booted"
|
|
835
908
|
|
|
@@ -865,7 +938,7 @@ module Raptor
|
|
|
865
938
|
@before_refork.each(&:call)
|
|
866
939
|
server.stop_accepting
|
|
867
940
|
else
|
|
868
|
-
@before_worker_shutdown.each(
|
|
941
|
+
@before_worker_shutdown.each { |hook| hook.parameters.empty? ? hook.call : hook.call(index) }
|
|
869
942
|
server.shutdown
|
|
870
943
|
end
|
|
871
944
|
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "socket"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
module Raptor
|
|
9
|
+
# Serves cluster statistics over a Unix socket.
|
|
10
|
+
#
|
|
11
|
+
class ControlServer
|
|
12
|
+
# @rbs @path: String
|
|
13
|
+
# @rbs @stats: ^() -> Hash[Symbol, untyped]
|
|
14
|
+
# @rbs @server: UNIXServer?
|
|
15
|
+
# @rbs @client: UNIXSocket?
|
|
16
|
+
# @rbs @thread: Thread?
|
|
17
|
+
# @rbs @running: bool
|
|
18
|
+
# @rbs @mutex: Mutex
|
|
19
|
+
|
|
20
|
+
# Creates a control server for `url` without binding it.
|
|
21
|
+
#
|
|
22
|
+
# @param url [String] `unix://` URL to listen on
|
|
23
|
+
# @yieldreturn [Hash] cluster statistics
|
|
24
|
+
# @return [void]
|
|
25
|
+
# @raise [ArgumentError] if the URL is not a Unix socket
|
|
26
|
+
#
|
|
27
|
+
# @rbs (String url) { () -> Hash[Symbol, untyped] } -> void
|
|
28
|
+
def initialize(url, &stats)
|
|
29
|
+
uri = URI(url)
|
|
30
|
+
raise ArgumentError, "control_url must use unix://" unless uri.scheme == "unix" && !uri.path.empty?
|
|
31
|
+
|
|
32
|
+
@path = uri.path
|
|
33
|
+
@stats = stats
|
|
34
|
+
@server = nil
|
|
35
|
+
@client = nil
|
|
36
|
+
@thread = nil
|
|
37
|
+
@running = false
|
|
38
|
+
@mutex = Mutex.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Binds the Unix socket.
|
|
42
|
+
#
|
|
43
|
+
# @return [void]
|
|
44
|
+
#
|
|
45
|
+
# @rbs () -> void
|
|
46
|
+
def bind
|
|
47
|
+
remove_stale_socket
|
|
48
|
+
@server = UNIXServer.new(@path)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Starts serving requests in a background thread.
|
|
52
|
+
#
|
|
53
|
+
# @return [void]
|
|
54
|
+
#
|
|
55
|
+
# @rbs () -> void
|
|
56
|
+
def start
|
|
57
|
+
@running = true
|
|
58
|
+
owner_pid = Process.pid
|
|
59
|
+
at_exit { File.delete(@path) rescue nil if Process.pid == owner_pid }
|
|
60
|
+
|
|
61
|
+
@thread = Thread.new do
|
|
62
|
+
Thread.current.name = "Control Server"
|
|
63
|
+
|
|
64
|
+
serve
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Stops serving and removes the socket.
|
|
69
|
+
#
|
|
70
|
+
# @return [void]
|
|
71
|
+
#
|
|
72
|
+
# @rbs () -> void
|
|
73
|
+
def shutdown
|
|
74
|
+
@running = false
|
|
75
|
+
@mutex.synchronize do
|
|
76
|
+
@server&.close
|
|
77
|
+
@client&.close
|
|
78
|
+
end
|
|
79
|
+
@thread&.join
|
|
80
|
+
File.delete(@path) rescue nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# @rbs () -> void
|
|
86
|
+
def remove_stale_socket
|
|
87
|
+
return unless File.exist?(@path)
|
|
88
|
+
|
|
89
|
+
begin
|
|
90
|
+
UNIXSocket.new(@path).close
|
|
91
|
+
raise "Socket #{@path.inspect} is already in use"
|
|
92
|
+
rescue Errno::ECONNREFUSED
|
|
93
|
+
File.delete(@path)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# @rbs () -> void
|
|
98
|
+
def serve
|
|
99
|
+
while @running
|
|
100
|
+
readable, = IO.select([@server], nil, nil, 1)
|
|
101
|
+
next unless readable
|
|
102
|
+
|
|
103
|
+
@mutex.synchronize do
|
|
104
|
+
client = @server.accept_nonblock(exception: false)
|
|
105
|
+
@client = client if client.is_a?(UNIXSocket)
|
|
106
|
+
end
|
|
107
|
+
handle(@client) if @client
|
|
108
|
+
end
|
|
109
|
+
rescue IOError, Errno::EBADF
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# @rbs (UNIXSocket client) -> void
|
|
113
|
+
def handle(client)
|
|
114
|
+
request_line = client.gets
|
|
115
|
+
while line = client.gets
|
|
116
|
+
break if line == "\r\n"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
if request_line&.start_with?("GET /stats ")
|
|
120
|
+
body = JSON.generate(@stats.call)
|
|
121
|
+
client.write("HTTP/1.0 200 OK\r\nContent-Type: application/json\r\nContent-Length: #{body.bytesize}\r\n\r\n#{body}")
|
|
122
|
+
else
|
|
123
|
+
client.write("HTTP/1.0 404 Not Found\r\nContent-Length: 0\r\n\r\n")
|
|
124
|
+
end
|
|
125
|
+
rescue IOError, SystemCallError
|
|
126
|
+
ensure
|
|
127
|
+
client.close rescue nil
|
|
128
|
+
@mutex.synchronize { @client = nil }
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
data/lib/raptor/http1.rb
CHANGED
|
@@ -10,6 +10,7 @@ require "rack"
|
|
|
10
10
|
|
|
11
11
|
require_relative "http"
|
|
12
12
|
require_relative "raptor_http"
|
|
13
|
+
require_relative "thread_locals"
|
|
13
14
|
|
|
14
15
|
module Raptor
|
|
15
16
|
# Parses HTTP/1.x requests and dispatches them to the Rack
|
|
@@ -23,8 +24,7 @@ module Raptor
|
|
|
23
24
|
FILE_CHUNK_SIZE = 64 * 1024
|
|
24
25
|
READ_BUFFER_SIZE = 64 * 1024
|
|
25
26
|
RESPONSE_BUFFER_CAPACITY = 4 * 1024
|
|
26
|
-
|
|
27
|
-
MAX_KEEPALIVE_REQUESTS = 100
|
|
27
|
+
MAX_KEEPALIVE_REQUESTS = 1000
|
|
28
28
|
|
|
29
29
|
HTTP_10 = "HTTP/1.0"
|
|
30
30
|
HTTP_11 = "HTTP/1.1"
|
|
@@ -168,6 +168,8 @@ module Raptor
|
|
|
168
168
|
# @rbs @body_spool_threshold: Integer?
|
|
169
169
|
# @rbs @max_keepalive_requests: Integer
|
|
170
170
|
# @rbs @access_log_io: IO?
|
|
171
|
+
# @rbs @clean_thread_locals: bool
|
|
172
|
+
# @rbs @clean_fiber_locals: bool
|
|
171
173
|
# @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
172
174
|
# @rbs @running: AtomicBoolean
|
|
173
175
|
# @rbs @env_template: Hash[String, untyped]
|
|
@@ -183,11 +185,13 @@ module Raptor
|
|
|
183
185
|
# @param http1_options [Hash] HTTP/1.1-specific settings
|
|
184
186
|
# @option http1_options [Integer] :max_keepalive_requests maximum requests per HTTP/1.1 keep-alive connection
|
|
185
187
|
# @param access_log_io [IO, nil] IO to write Common Log Format access entries to, or nil to disable
|
|
188
|
+
# @param clean_thread_locals [Boolean] whether to clear application thread locals after each request
|
|
189
|
+
# @param clean_fiber_locals [Boolean] whether to process each request in a fresh Fiber
|
|
186
190
|
# @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
|
|
187
191
|
# @return [void]
|
|
188
192
|
#
|
|
189
|
-
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?connection_options: Hash[Symbol, untyped], ?http1_options: Hash[Symbol, untyped], ?access_log_io: IO?, ?on_error: ^(Hash[String, untyped]?, Exception) -> void | nil) -> void
|
|
190
|
-
def initialize(app, server_port, connection_options: {}, http1_options: {}, access_log_io: nil, on_error: nil)
|
|
193
|
+
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?connection_options: Hash[Symbol, untyped], ?http1_options: Hash[Symbol, untyped], ?access_log_io: IO?, ?clean_thread_locals: bool, ?clean_fiber_locals: bool, ?on_error: ^(Hash[String, untyped]?, Exception) -> void | nil) -> void
|
|
194
|
+
def initialize(app, server_port, connection_options: {}, http1_options: {}, access_log_io: nil, clean_thread_locals: true, clean_fiber_locals: false, on_error: nil)
|
|
191
195
|
@app = app
|
|
192
196
|
@server_port = server_port
|
|
193
197
|
@server_port_string = server_port.to_s.freeze
|
|
@@ -196,6 +200,8 @@ module Raptor
|
|
|
196
200
|
@body_spool_threshold = connection_options[:body_spool_threshold]
|
|
197
201
|
@max_keepalive_requests = http1_options[:max_keepalive_requests] || MAX_KEEPALIVE_REQUESTS
|
|
198
202
|
@access_log_io = access_log_io
|
|
203
|
+
@clean_thread_locals = clean_thread_locals
|
|
204
|
+
@clean_fiber_locals = clean_fiber_locals
|
|
199
205
|
@on_error = on_error
|
|
200
206
|
@running = AtomicBoolean.new(true)
|
|
201
207
|
@env_template = {
|
|
@@ -373,7 +379,7 @@ module Raptor
|
|
|
373
379
|
#
|
|
374
380
|
# @rbs (TCPSocket socket) -> String
|
|
375
381
|
def read_into_thread_buffer(socket)
|
|
376
|
-
buffer = (
|
|
382
|
+
buffer = ThreadLocals.fetch(:raptor_read_buffer) { String.new(capacity: READ_BUFFER_SIZE) }
|
|
377
383
|
socket.read_nonblock(READ_BUFFER_SIZE, buffer)
|
|
378
384
|
|
|
379
385
|
while socket.respond_to?(:pending) && socket.pending.positive?
|
|
@@ -392,7 +398,7 @@ module Raptor
|
|
|
392
398
|
#
|
|
393
399
|
# @rbs (String buffer) -> [Hash[String, untyped], Hash[Symbol, untyped], Integer, HttpParser]
|
|
394
400
|
def parse_next_request(buffer)
|
|
395
|
-
parser = (
|
|
401
|
+
parser = ThreadLocals.fetch(:raptor_http_parser) { HttpParser.new }
|
|
396
402
|
parser.reset
|
|
397
403
|
env = @env_template.dup
|
|
398
404
|
nread = parser.execute(env, buffer, 0)
|
|
@@ -505,6 +511,19 @@ module Raptor
|
|
|
505
511
|
#
|
|
506
512
|
# @rbs (TCPSocket socket, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, String? body, Integer request_count, String remote_addr, String url_scheme) -> bool
|
|
507
513
|
def process_request(socket, env, parse_data, body, request_count, remote_addr, url_scheme)
|
|
514
|
+
if @clean_fiber_locals
|
|
515
|
+
Fiber.new { perform_request(socket, env, parse_data, body, request_count, remote_addr, url_scheme) }.resume
|
|
516
|
+
else
|
|
517
|
+
perform_request(socket, env, parse_data, body, request_count, remote_addr, url_scheme)
|
|
518
|
+
end
|
|
519
|
+
ensure
|
|
520
|
+
ThreadLocals.clear if @clean_thread_locals
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
# Calls the Rack app and writes its response for one request.
|
|
524
|
+
#
|
|
525
|
+
# @rbs (TCPSocket socket, Hash[String, untyped] env, Hash[Symbol, untyped] parse_data, String? body, Integer request_count, String remote_addr, String url_scheme) -> bool
|
|
526
|
+
def perform_request(socket, env, parse_data, body, request_count, remote_addr, url_scheme)
|
|
508
527
|
rack_env = nil
|
|
509
528
|
status = nil
|
|
510
529
|
headers = nil
|
|
@@ -571,8 +590,9 @@ module Raptor
|
|
|
571
590
|
end
|
|
572
591
|
|
|
573
592
|
# Reads and processes subsequent requests inline on a kept-alive
|
|
574
|
-
# connection.
|
|
575
|
-
#
|
|
593
|
+
# connection. Returns the connection to the reactor when no data is ready
|
|
594
|
+
# or a request is incomplete, and queues complete requests behind work
|
|
595
|
+
# that is already waiting.
|
|
576
596
|
#
|
|
577
597
|
# @param socket [TCPSocket] the client socket
|
|
578
598
|
# @param id [Integer] unique client identifier
|
|
@@ -591,7 +611,7 @@ module Raptor
|
|
|
591
611
|
return
|
|
592
612
|
end
|
|
593
613
|
|
|
594
|
-
unless socket.wait_readable(
|
|
614
|
+
unless socket.wait_readable(0)
|
|
595
615
|
reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
|
|
596
616
|
return
|
|
597
617
|
end
|
|
@@ -626,7 +646,7 @@ module Raptor
|
|
|
626
646
|
|
|
627
647
|
request_count += 1
|
|
628
648
|
|
|
629
|
-
if thread_pool.queue_size
|
|
649
|
+
if thread_pool.queue_size.positive?
|
|
630
650
|
thread_pool << proc do
|
|
631
651
|
process_client(
|
|
632
652
|
socket,
|
|
@@ -900,7 +920,7 @@ module Raptor
|
|
|
900
920
|
|
|
901
921
|
response = build_status_line(http_version, status)
|
|
902
922
|
|
|
903
|
-
corked = !keep_alive
|
|
923
|
+
corked = !keep_alive && !body.respond_to?(:to_ary) && !no_body && !response_hijack
|
|
904
924
|
cork_socket(socket) if corked
|
|
905
925
|
|
|
906
926
|
if response_hijack
|
|
@@ -983,7 +1003,7 @@ module Raptor
|
|
|
983
1003
|
# @rbs (String http_version, Integer status) -> String
|
|
984
1004
|
def build_status_line(http_version, status)
|
|
985
1005
|
cache = http_version == HTTP_11 ? STATUS_LINE_CACHE_11 : STATUS_LINE_CACHE_10
|
|
986
|
-
response = (
|
|
1006
|
+
response = ThreadLocals.fetch(:raptor_response_buffer) { String.new(capacity: RESPONSE_BUFFER_CAPACITY) }
|
|
987
1007
|
response.clear
|
|
988
1008
|
response << cache[status]
|
|
989
1009
|
response
|
data/lib/raptor/http2.rb
CHANGED
|
@@ -8,6 +8,7 @@ require "rack"
|
|
|
8
8
|
|
|
9
9
|
require_relative "http"
|
|
10
10
|
require_relative "raptor_http2"
|
|
11
|
+
require_relative "thread_locals"
|
|
11
12
|
|
|
12
13
|
module Raptor
|
|
13
14
|
# Handles HTTP/2 request processing and Rack application integration.
|
|
@@ -267,6 +268,8 @@ module Raptor
|
|
|
267
268
|
# @rbs @server_port: Integer
|
|
268
269
|
# @rbs @write_timeout: Integer
|
|
269
270
|
# @rbs @access_log_io: IO?
|
|
271
|
+
# @rbs @clean_thread_locals: bool
|
|
272
|
+
# @rbs @clean_fiber_locals: bool
|
|
270
273
|
# @rbs @on_error: ^(Hash[String, untyped]?, Exception) -> void | nil
|
|
271
274
|
# @rbs @initial_settings_frame: String
|
|
272
275
|
|
|
@@ -285,15 +288,19 @@ module Raptor
|
|
|
285
288
|
# @param http2_options [Hash] HTTP/2-specific settings
|
|
286
289
|
# @option http2_options [Integer] :max_concurrent_streams maximum HTTP/2 concurrent streams per connection
|
|
287
290
|
# @param access_log_io [IO, nil] IO to write Common Log Format access entries to, or nil to disable
|
|
291
|
+
# @param clean_thread_locals [Boolean] whether to clear application thread locals after each request
|
|
292
|
+
# @param clean_fiber_locals [Boolean] whether to process each request in a fresh Fiber
|
|
288
293
|
# @param on_error [#call, nil] callback invoked with (env, exception) when the Rack app raises
|
|
289
294
|
# @return [void]
|
|
290
295
|
#
|
|
291
|
-
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?connection_options: Hash[Symbol, untyped], ?http2_options: Hash[Symbol, untyped], ?access_log_io: IO?, ?on_error: ^(Hash[String, untyped]?, Exception) -> void | nil) -> void
|
|
292
|
-
def initialize(app, server_port, connection_options: {}, http2_options: {}, access_log_io: nil, on_error: nil)
|
|
296
|
+
# @rbs (^(Hash[String, untyped]) -> [Integer, Hash[String, String | Array[String]], untyped] app, Integer server_port, ?connection_options: Hash[Symbol, untyped], ?http2_options: Hash[Symbol, untyped], ?access_log_io: IO?, ?clean_thread_locals: bool, ?clean_fiber_locals: bool, ?on_error: ^(Hash[String, untyped]?, Exception) -> void | nil) -> void
|
|
297
|
+
def initialize(app, server_port, connection_options: {}, http2_options: {}, access_log_io: nil, clean_thread_locals: true, clean_fiber_locals: false, on_error: nil)
|
|
293
298
|
@app = app
|
|
294
299
|
@server_port = server_port
|
|
295
300
|
@write_timeout = connection_options[:write_timeout] || Http::WRITE_TIMEOUT
|
|
296
301
|
@access_log_io = access_log_io
|
|
302
|
+
@clean_thread_locals = clean_thread_locals
|
|
303
|
+
@clean_fiber_locals = clean_fiber_locals
|
|
297
304
|
@on_error = on_error
|
|
298
305
|
|
|
299
306
|
parser = Http2Parser.new
|
|
@@ -730,6 +737,19 @@ module Raptor
|
|
|
730
737
|
#
|
|
731
738
|
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
|
|
732
739
|
def dispatch_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:)
|
|
740
|
+
if @clean_fiber_locals
|
|
741
|
+
Fiber.new { perform_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:) }.resume
|
|
742
|
+
else
|
|
743
|
+
perform_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:)
|
|
744
|
+
end
|
|
745
|
+
ensure
|
|
746
|
+
ThreadLocals.clear if @clean_thread_locals
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
# Calls the Rack app and writes its response for one stream.
|
|
750
|
+
#
|
|
751
|
+
# @rbs (OpenSSL::SSL::SSLSocket socket, Writer writer, FlowControl flow_control, Integer stream_id, Array[[String, String]] headers, String body, remote_addr: String) -> void
|
|
752
|
+
def perform_stream_request(socket, writer, flow_control, stream_id, headers, body, remote_addr:)
|
|
733
753
|
env = build_rack_env(headers, body, remote_addr: remote_addr)
|
|
734
754
|
status, response_headers, response_body = @app.call(env)
|
|
735
755
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Raptor
|
|
5
|
+
# Owns Raptor's reusable per-thread values and clears application thread
|
|
6
|
+
# locals after each request.
|
|
7
|
+
#
|
|
8
|
+
module ThreadLocals
|
|
9
|
+
PRESERVED_KEYS = [
|
|
10
|
+
:raptor_http_parser,
|
|
11
|
+
:raptor_read_buffer,
|
|
12
|
+
:raptor_response_buffer,
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
# Returns a reusable value stored on the current thread.
|
|
16
|
+
#
|
|
17
|
+
# @return [Object]
|
|
18
|
+
#
|
|
19
|
+
# @rbs (Symbol key) { () -> untyped } -> untyped
|
|
20
|
+
def self.fetch(key)
|
|
21
|
+
thread = Thread.current
|
|
22
|
+
thread.thread_variable_get(key) || thread.thread_variable_set(key, yield)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Clears application-owned true thread locals while retaining Raptor's
|
|
26
|
+
# reusable per-thread values.
|
|
27
|
+
#
|
|
28
|
+
# @return [void]
|
|
29
|
+
#
|
|
30
|
+
# @rbs () -> void
|
|
31
|
+
def self.clear
|
|
32
|
+
thread = Thread.current
|
|
33
|
+
thread.thread_variables.each do |key|
|
|
34
|
+
thread.thread_variable_set(key, nil) unless PRESERVED_KEYS.include?(key)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/raptor/version.rb
CHANGED
|
@@ -106,6 +106,14 @@ module Raptor
|
|
|
106
106
|
# @rbs (String? path) -> void
|
|
107
107
|
def apply_config_file: (String? path) -> void
|
|
108
108
|
|
|
109
|
+
# Applies worker and thread settings from the environment. Explicit CLI
|
|
110
|
+
# options are parsed afterwards and take precedence.
|
|
111
|
+
#
|
|
112
|
+
# @return [void]
|
|
113
|
+
#
|
|
114
|
+
# @rbs () -> void
|
|
115
|
+
def apply_environment: () -> void
|
|
116
|
+
|
|
109
117
|
# Creates the OptionParser instance with all supported command-line options.
|
|
110
118
|
#
|
|
111
119
|
# @return [OptionParser] configured option parser
|