raptor 0.8.0 → 0.9.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 +90 -32
- data/.dockerignore +5 -0
- data/CHANGELOG.md +11 -0
- data/Dockerfile +28 -0
- data/README.md +16 -9
- data/Rakefile +19 -1
- data/docs/raptor-vs-puma.md +557 -0
- data/ext/raptor_bpf/reuseport_select.bpf.c +63 -0
- data/ext/raptor_http/extconf.rb +1 -1
- data/ext/raptor_native/extconf.rb +7 -0
- data/ext/raptor_native/raptor_native.c +99 -0
- data/lib/raptor/binder.rb +18 -4
- data/lib/raptor/cli.rb +1 -1
- data/lib/raptor/cluster.rb +27 -2
- data/lib/raptor/http.rb +41 -0
- data/lib/raptor/http1.rb +32 -24
- data/lib/raptor/reuseport_bpf.rb +108 -0
- data/lib/raptor/server.rb +40 -11
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/binder.rbs +10 -0
- data/sig/generated/raptor/cluster.rbs +4 -2
- data/sig/generated/raptor/http.rbs +13 -0
- data/sig/generated/raptor/http1.rbs +16 -5
- data/sig/generated/raptor/reuseport_bpf.rbs +56 -0
- data/sig/generated/raptor/server.rbs +20 -6
- metadata +24 -1
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#include <linux/bpf.h>
|
|
2
|
+
#include <bpf/bpf_helpers.h>
|
|
3
|
+
|
|
4
|
+
#define MAX_WORKERS 64
|
|
5
|
+
|
|
6
|
+
// Per-worker listening sockets, keyed by worker index.
|
|
7
|
+
struct {
|
|
8
|
+
__uint(type, BPF_MAP_TYPE_REUSEPORT_SOCKARRAY);
|
|
9
|
+
__type(key, __u32);
|
|
10
|
+
__type(value, __u64);
|
|
11
|
+
__uint(max_entries, MAX_WORKERS);
|
|
12
|
+
} socks SEC(".maps");
|
|
13
|
+
|
|
14
|
+
// Worker count at slot 0, per-worker backlog at slots 1..N.
|
|
15
|
+
struct {
|
|
16
|
+
__uint(type, BPF_MAP_TYPE_ARRAY);
|
|
17
|
+
__type(key, __u32);
|
|
18
|
+
__type(value, __u32);
|
|
19
|
+
__uint(max_entries, MAX_WORKERS + 1);
|
|
20
|
+
} loads SEC(".maps");
|
|
21
|
+
|
|
22
|
+
// Routes each incoming connection to the worker with the lowest backlog,
|
|
23
|
+
// with a random tie-break when loads are equal.
|
|
24
|
+
SEC("sk_reuseport")
|
|
25
|
+
int select_least_loaded(struct sk_reuseport_md *ctx) {
|
|
26
|
+
__u32 count_key = 0;
|
|
27
|
+
__u32 *count_ptr = bpf_map_lookup_elem(&loads, &count_key);
|
|
28
|
+
if (!count_ptr || *count_ptr == 0) {
|
|
29
|
+
return SK_DROP;
|
|
30
|
+
}
|
|
31
|
+
__u32 num_workers = *count_ptr;
|
|
32
|
+
if (num_workers > MAX_WORKERS) {
|
|
33
|
+
num_workers = MAX_WORKERS;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
__u32 min_load = ~0u;
|
|
37
|
+
__u32 max_load = 0;
|
|
38
|
+
__u32 min_idx = 0;
|
|
39
|
+
|
|
40
|
+
for (__u32 worker_idx = 0; worker_idx < MAX_WORKERS; worker_idx++) {
|
|
41
|
+
if (worker_idx >= num_workers) {
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
__u32 worker_key = worker_idx + 1;
|
|
45
|
+
__u32 *load_ptr = bpf_map_lookup_elem(&loads, &worker_key);
|
|
46
|
+
if (!load_ptr) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (*load_ptr < min_load) {
|
|
50
|
+
min_load = *load_ptr;
|
|
51
|
+
min_idx = worker_idx;
|
|
52
|
+
}
|
|
53
|
+
if (*load_ptr > max_load) {
|
|
54
|
+
max_load = *load_ptr;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
__u32 chosen_idx = (min_load == max_load) ? (bpf_get_prandom_u32() % num_workers) : min_idx;
|
|
59
|
+
bpf_sk_select_reuseport(ctx, &socks, &chosen_idx, 0);
|
|
60
|
+
return SK_PASS;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
char _license[] SEC("license") = "GPL";
|
data/ext/raptor_http/extconf.rb
CHANGED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#ifdef __linux__
|
|
2
|
+
#define _GNU_SOURCE 1
|
|
3
|
+
#endif
|
|
4
|
+
|
|
5
|
+
#include "ruby.h"
|
|
6
|
+
#include "ruby/io.h"
|
|
7
|
+
#include <sys/uio.h>
|
|
8
|
+
#include <errno.h>
|
|
9
|
+
#include <limits.h>
|
|
10
|
+
|
|
11
|
+
#ifdef __linux__
|
|
12
|
+
#include <sched.h>
|
|
13
|
+
#include <unistd.h>
|
|
14
|
+
#endif
|
|
15
|
+
|
|
16
|
+
static VALUE eEAGAINWaitWritable;
|
|
17
|
+
|
|
18
|
+
static VALUE raptor_native_writev_nonblock(VALUE self, VALUE io, VALUE strings) {
|
|
19
|
+
(void)self;
|
|
20
|
+
Check_Type(strings, T_ARRAY);
|
|
21
|
+
long len = RARRAY_LEN(strings);
|
|
22
|
+
if (len == 0) return LONG2NUM(0);
|
|
23
|
+
if (len > IOV_MAX) len = IOV_MAX;
|
|
24
|
+
|
|
25
|
+
int fd = rb_io_descriptor(io);
|
|
26
|
+
struct iovec *iov = alloca(len * sizeof(struct iovec));
|
|
27
|
+
|
|
28
|
+
for (long i = 0; i < len; i++) {
|
|
29
|
+
VALUE str = RARRAY_AREF(strings, i);
|
|
30
|
+
Check_Type(str, T_STRING);
|
|
31
|
+
iov[i].iov_base = RSTRING_PTR(str);
|
|
32
|
+
iov[i].iov_len = RSTRING_LEN(str);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
ssize_t written;
|
|
36
|
+
while ((written = writev(fd, iov, (int)len)) < 0) {
|
|
37
|
+
if (errno == EINTR) continue;
|
|
38
|
+
if (errno == EAGAIN || errno == EWOULDBLOCK) rb_raise(eEAGAINWaitWritable, "writev would block");
|
|
39
|
+
rb_sys_fail("writev");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return LONG2NUM((long)written);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static VALUE raptor_native_pin_to_cpu(VALUE self, VALUE cpu) {
|
|
46
|
+
(void)self;
|
|
47
|
+
#ifdef __linux__
|
|
48
|
+
int worker_index = NUM2INT(cpu);
|
|
49
|
+
cpu_set_t current;
|
|
50
|
+
CPU_ZERO(¤t);
|
|
51
|
+
if (sched_getaffinity(0, sizeof(current), ¤t) < 0) rb_sys_fail("sched_getaffinity");
|
|
52
|
+
|
|
53
|
+
int cpu_id = -1;
|
|
54
|
+
int seen = 0;
|
|
55
|
+
for (int candidate = 0; candidate < CPU_SETSIZE; candidate++) {
|
|
56
|
+
if (!CPU_ISSET(candidate, ¤t)) continue;
|
|
57
|
+
if (seen == worker_index) { cpu_id = candidate; break; }
|
|
58
|
+
seen++;
|
|
59
|
+
}
|
|
60
|
+
if (cpu_id < 0) return Qfalse;
|
|
61
|
+
|
|
62
|
+
cpu_set_t set;
|
|
63
|
+
CPU_ZERO(&set);
|
|
64
|
+
CPU_SET(cpu_id, &set);
|
|
65
|
+
if (sched_setaffinity(0, sizeof(set), &set) < 0) rb_sys_fail("sched_setaffinity");
|
|
66
|
+
return Qtrue;
|
|
67
|
+
#else
|
|
68
|
+
(void)cpu;
|
|
69
|
+
return Qfalse;
|
|
70
|
+
#endif
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static VALUE raptor_native_cpu_count(VALUE self) {
|
|
74
|
+
(void)self;
|
|
75
|
+
#ifdef __linux__
|
|
76
|
+
cpu_set_t set;
|
|
77
|
+
CPU_ZERO(&set);
|
|
78
|
+
if (sched_getaffinity(0, sizeof(set), &set) < 0) rb_sys_fail("sched_getaffinity");
|
|
79
|
+
return LONG2NUM((long)CPU_COUNT(&set));
|
|
80
|
+
#else
|
|
81
|
+
return LONG2NUM(0);
|
|
82
|
+
#endif
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
RUBY_FUNC_EXPORTED void Init_raptor_native(void) {
|
|
86
|
+
rb_ext_ractor_safe(true);
|
|
87
|
+
|
|
88
|
+
VALUE mRaptor = rb_define_module("Raptor");
|
|
89
|
+
VALUE mVectorIO = rb_define_module_under(mRaptor, "VectorIO");
|
|
90
|
+
VALUE mCPU = rb_define_module_under(mRaptor, "CPU");
|
|
91
|
+
|
|
92
|
+
rb_define_singleton_method(mVectorIO, "writev_nonblock", raptor_native_writev_nonblock, 2);
|
|
93
|
+
|
|
94
|
+
rb_define_singleton_method(mCPU, "pin", raptor_native_pin_to_cpu, 1);
|
|
95
|
+
rb_define_singleton_method(mCPU, "count", raptor_native_cpu_count, 0);
|
|
96
|
+
|
|
97
|
+
eEAGAINWaitWritable = rb_const_get(rb_cIO, rb_intern("EAGAINWaitWritable"));
|
|
98
|
+
rb_global_variable(&eEAGAINWaitWritable);
|
|
99
|
+
}
|
data/lib/raptor/binder.rb
CHANGED
|
@@ -61,6 +61,16 @@ module Raptor
|
|
|
61
61
|
# @rbs @listeners: Array[TCPServer | UNIXServer | SslListener]
|
|
62
62
|
# @rbs @uri_listeners: Hash[String, Array[TCPServer | UNIXServer | SslListener]]
|
|
63
63
|
|
|
64
|
+
# Array of bind URIs.
|
|
65
|
+
#
|
|
66
|
+
# @return [Array<String>] the bind URIs
|
|
67
|
+
attr_reader :bind_uris
|
|
68
|
+
|
|
69
|
+
# Kernel listen() queue depth for TCP/SSL listeners.
|
|
70
|
+
#
|
|
71
|
+
# @return [Integer] the socket backlog
|
|
72
|
+
attr_reader :socket_backlog
|
|
73
|
+
|
|
64
74
|
# Array of listening sockets.
|
|
65
75
|
#
|
|
66
76
|
# @return [Array<TCPServer, UNIXServer, SslListener>] the server sockets
|
|
@@ -244,11 +254,15 @@ module Raptor
|
|
|
244
254
|
|
|
245
255
|
host = host[1..-2] if host&.start_with?("[")
|
|
246
256
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
257
|
+
addrinfo = Addrinfo.tcp(host, port)
|
|
258
|
+
socket = Socket.new(addrinfo.afamily, Socket::SOCK_STREAM, 0)
|
|
259
|
+
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
|
|
260
|
+
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, true) if Socket.const_defined?(:SO_REUSEPORT)
|
|
261
|
+
socket.bind(addrinfo)
|
|
262
|
+
socket.listen(@socket_backlog)
|
|
251
263
|
|
|
264
|
+
tcp_server = TCPServer.for_fd(socket.fileno)
|
|
265
|
+
socket.autoclose = false
|
|
252
266
|
[tcp_server]
|
|
253
267
|
end
|
|
254
268
|
|
data/lib/raptor/cli.rb
CHANGED
|
@@ -229,7 +229,7 @@ module Raptor
|
|
|
229
229
|
end
|
|
230
230
|
|
|
231
231
|
opts.on("-b", "--bind URI", String, "Bind address (default: tcp://0.0.0.0:9292)") do |bind|
|
|
232
|
-
if @options[:binds]
|
|
232
|
+
if @options[:binds].equal?(DEFAULT_OPTIONS[:binds])
|
|
233
233
|
@options[:binds] = [bind]
|
|
234
234
|
else
|
|
235
235
|
@options[:binds] << bind
|
data/lib/raptor/cluster.rb
CHANGED
|
@@ -9,8 +9,9 @@ require "ractor-pool"
|
|
|
9
9
|
|
|
10
10
|
require_relative "log"
|
|
11
11
|
require_relative "binder"
|
|
12
|
-
require_relative "server"
|
|
13
12
|
require_relative "reactor"
|
|
13
|
+
require_relative "server"
|
|
14
|
+
require_relative "reuseport_bpf"
|
|
14
15
|
require_relative "http1"
|
|
15
16
|
require_relative "http2"
|
|
16
17
|
require_relative "stats"
|
|
@@ -91,6 +92,7 @@ module Raptor
|
|
|
91
92
|
# @rbs @phased_restart_requested: bool
|
|
92
93
|
# @rbs @phased_restarting: bool
|
|
93
94
|
# @rbs @hot_restart_requested: bool
|
|
95
|
+
# @rbs @bpf_active: bool
|
|
94
96
|
|
|
95
97
|
# Creates a new Cluster with the specified configuration.
|
|
96
98
|
#
|
|
@@ -204,6 +206,8 @@ module Raptor
|
|
|
204
206
|
|
|
205
207
|
File.open(@pid_file, File::CREAT | File::EXCL | File::WRONLY) { |file| file.write(Process.pid.to_s) } if @pid_file
|
|
206
208
|
|
|
209
|
+
@bpf_active = ReuseportBPF.setup(@worker_count)
|
|
210
|
+
|
|
207
211
|
@worker_count.times { |index| spawn_worker(index) }
|
|
208
212
|
|
|
209
213
|
stats_file_thread = if @stats_file
|
|
@@ -232,6 +236,7 @@ module Raptor
|
|
|
232
236
|
File.delete(@stats_file) rescue nil if @stats_file
|
|
233
237
|
File.delete(@pid_file) rescue nil if @pid_file
|
|
234
238
|
@stats.unmap
|
|
239
|
+
@binder.close
|
|
235
240
|
end
|
|
236
241
|
|
|
237
242
|
# Returns stats for all worker processes.
|
|
@@ -439,6 +444,8 @@ module Raptor
|
|
|
439
444
|
trap("USR1", "IGNORE")
|
|
440
445
|
trap("USR2", "IGNORE")
|
|
441
446
|
|
|
447
|
+
Raptor::CPU.pin(index) if Raptor::CPU.count >= @worker_count
|
|
448
|
+
|
|
442
449
|
started_at = Process.clock_gettime(Process::CLOCK_REALTIME)
|
|
443
450
|
request_count = 0
|
|
444
451
|
|
|
@@ -501,7 +508,25 @@ module Raptor
|
|
|
501
508
|
)
|
|
502
509
|
reactor_thread = reactor.run
|
|
503
510
|
|
|
504
|
-
|
|
511
|
+
worker_listeners = if @bpf_active
|
|
512
|
+
bpf_listeners = ReuseportBPF.create_worker_listeners(@binder.bind_uris, index, @binder.socket_backlog)
|
|
513
|
+
non_tcp_listeners = @binder.listeners.reject { |listener| listener.is_a?(TCPServer) }
|
|
514
|
+
bpf_listeners + non_tcp_listeners
|
|
515
|
+
else
|
|
516
|
+
@binder.listeners
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
server = Server.new(
|
|
520
|
+
@binder,
|
|
521
|
+
reactor,
|
|
522
|
+
thread_pool,
|
|
523
|
+
http1,
|
|
524
|
+
http2,
|
|
525
|
+
connection_options: @connection_options,
|
|
526
|
+
drain_accept_queue: @drain_accept_queue,
|
|
527
|
+
listeners: worker_listeners,
|
|
528
|
+
worker_index: (index if @bpf_active)
|
|
529
|
+
)
|
|
505
530
|
server_thread = server.run
|
|
506
531
|
|
|
507
532
|
Log.info "Worker #{index} booted"
|
data/lib/raptor/http.rb
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
require "rack"
|
|
5
5
|
|
|
6
6
|
require_relative "version"
|
|
7
|
+
require_relative "raptor_native"
|
|
7
8
|
|
|
8
9
|
module Raptor
|
|
9
10
|
# Shared HTTP utilities used by both the HTTP/1.x and HTTP/2 handlers:
|
|
@@ -51,6 +52,46 @@ module Raptor
|
|
|
51
52
|
end
|
|
52
53
|
end
|
|
53
54
|
|
|
55
|
+
# Writes `strings` in full via a single `writev(2)` syscall when possible,
|
|
56
|
+
# falling back to per-string writes on partial results. Bounded by
|
|
57
|
+
# `timeout` so a slow client can't pin the writing thread.
|
|
58
|
+
#
|
|
59
|
+
# @param socket [TCPSocket] the socket to write to
|
|
60
|
+
# @param strings [Array<String>] the buffers to write in order
|
|
61
|
+
# @param timeout [Integer] seconds to wait for the socket to become writable on each retry
|
|
62
|
+
# @return [void]
|
|
63
|
+
# @raise [WriteError] if the socket is not writable within the timeout or raises IOError
|
|
64
|
+
#
|
|
65
|
+
# @rbs (TCPSocket socket, Array[String] strings, ?timeout: Integer) -> void
|
|
66
|
+
def self.socket_writev(socket, strings, timeout: WRITE_TIMEOUT)
|
|
67
|
+
total = strings.sum(&:bytesize)
|
|
68
|
+
return if total.zero?
|
|
69
|
+
|
|
70
|
+
begin
|
|
71
|
+
written = Raptor::VectorIO.writev_nonblock(socket, strings)
|
|
72
|
+
rescue IO::WaitWritable
|
|
73
|
+
raise WriteError unless socket.wait_writable(timeout)
|
|
74
|
+
retry
|
|
75
|
+
rescue IOError
|
|
76
|
+
raise WriteError
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
return if written == total
|
|
80
|
+
|
|
81
|
+
offset = 0
|
|
82
|
+
strings.each do |string|
|
|
83
|
+
size = string.bytesize
|
|
84
|
+
if written >= offset + size
|
|
85
|
+
offset += size
|
|
86
|
+
else
|
|
87
|
+
start = written - offset
|
|
88
|
+
socket_write(socket, start.zero? ? string : string.byteslice(start..-1), timeout: timeout)
|
|
89
|
+
offset += size
|
|
90
|
+
written = offset
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
54
95
|
# Writes a Common Log Format entry to `io`. Write failures are silently
|
|
55
96
|
# ignored.
|
|
56
97
|
#
|
data/lib/raptor/http1.rb
CHANGED
|
@@ -22,6 +22,7 @@ module Raptor
|
|
|
22
22
|
FILE_CHUNK_SIZE = 64 * 1024
|
|
23
23
|
MAX_CHUNK_OVERHEAD = 16 * 1024
|
|
24
24
|
READ_BUFFER_SIZE = 64 * 1024
|
|
25
|
+
RESPONSE_BUFFER_CAPACITY = 4 * 1024
|
|
25
26
|
KEEPALIVE_READ_TIMEOUT = 0.001
|
|
26
27
|
MAX_KEEPALIVE_REQUESTS = 100
|
|
27
28
|
|
|
@@ -170,6 +171,19 @@ module Raptor
|
|
|
170
171
|
Http.socket_write(socket, string, timeout: @write_timeout)
|
|
171
172
|
end
|
|
172
173
|
|
|
174
|
+
# Instance-level wrapper around {Http.socket_writev} that applies the
|
|
175
|
+
# configured `write_timeout`.
|
|
176
|
+
#
|
|
177
|
+
# @param socket [TCPSocket] the socket to write to
|
|
178
|
+
# @param strings [Array<String>] the buffers to write in order
|
|
179
|
+
# @return [void]
|
|
180
|
+
# @raise [Http::WriteError] if the socket is not writable within the timeout or raises IOError
|
|
181
|
+
#
|
|
182
|
+
# @rbs (TCPSocket socket, Array[String] strings) -> void
|
|
183
|
+
def socket_writev(socket, strings)
|
|
184
|
+
Http.socket_writev(socket, strings, timeout: @write_timeout)
|
|
185
|
+
end
|
|
186
|
+
|
|
173
187
|
# Signals eager keep-alive loops to stop processing further requests on
|
|
174
188
|
# their connections. In-flight requests complete normally.
|
|
175
189
|
#
|
|
@@ -194,8 +208,10 @@ module Raptor
|
|
|
194
208
|
#
|
|
195
209
|
# @rbs (TCPSocket socket, Integer id, Reactor reactor, AtomicThreadPool thread_pool, String remote_addr, String url_scheme) -> void
|
|
196
210
|
def eager_accept(socket, id, reactor, thread_pool, remote_addr, url_scheme)
|
|
197
|
-
|
|
198
|
-
|
|
211
|
+
buffer = (Thread.current[:raptor_read_buffer] ||= String.new(capacity: READ_BUFFER_SIZE))
|
|
212
|
+
|
|
213
|
+
begin
|
|
214
|
+
socket.read_nonblock(READ_BUFFER_SIZE, buffer)
|
|
199
215
|
rescue IO::WaitReadable
|
|
200
216
|
reactor.add(
|
|
201
217
|
id: id,
|
|
@@ -209,9 +225,6 @@ module Raptor
|
|
|
209
225
|
return
|
|
210
226
|
end
|
|
211
227
|
|
|
212
|
-
buffer = String.new
|
|
213
|
-
buffer << data
|
|
214
|
-
|
|
215
228
|
while socket.respond_to?(:pending) && socket.pending > 0
|
|
216
229
|
buffer << socket.read_nonblock(socket.pending)
|
|
217
230
|
end
|
|
@@ -530,8 +543,10 @@ module Raptor
|
|
|
530
543
|
return
|
|
531
544
|
end
|
|
532
545
|
|
|
533
|
-
|
|
534
|
-
|
|
546
|
+
buffer = (Thread.current[:raptor_read_buffer] ||= String.new(capacity: READ_BUFFER_SIZE))
|
|
547
|
+
|
|
548
|
+
begin
|
|
549
|
+
socket.read_nonblock(READ_BUFFER_SIZE, buffer)
|
|
535
550
|
rescue IO::WaitReadable
|
|
536
551
|
reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
|
|
537
552
|
return
|
|
@@ -540,9 +555,6 @@ module Raptor
|
|
|
540
555
|
return
|
|
541
556
|
end
|
|
542
557
|
|
|
543
|
-
buffer = String.new
|
|
544
|
-
buffer << data
|
|
545
|
-
|
|
546
558
|
while socket.respond_to?(:pending) && socket.pending > 0
|
|
547
559
|
buffer << socket.read_nonblock(socket.pending)
|
|
548
560
|
end
|
|
@@ -631,7 +643,7 @@ module Raptor
|
|
|
631
643
|
reactor.persist(socket, id, request_count, remote_addr: remote_addr, url_scheme: url_scheme)
|
|
632
644
|
state = {
|
|
633
645
|
id: id,
|
|
634
|
-
buffer: buffer,
|
|
646
|
+
buffer: buffer.dup,
|
|
635
647
|
env: env,
|
|
636
648
|
request_count: request_count,
|
|
637
649
|
parse_data: parse_data,
|
|
@@ -922,7 +934,8 @@ module Raptor
|
|
|
922
934
|
end
|
|
923
935
|
end
|
|
924
936
|
|
|
925
|
-
#
|
|
937
|
+
# Returns the HTTP status line for `status`. Callers must not retain the
|
|
938
|
+
# returned string across further calls on the same thread.
|
|
926
939
|
#
|
|
927
940
|
# @param http_version [String] "HTTP/1.1" or "HTTP/1.0"
|
|
928
941
|
# @param status [Integer] HTTP status code
|
|
@@ -931,7 +944,10 @@ module Raptor
|
|
|
931
944
|
# @rbs (String http_version, Integer status) -> String
|
|
932
945
|
def build_status_line(http_version, status)
|
|
933
946
|
cache = http_version == HTTP_11 ? STATUS_LINE_CACHE_11 : STATUS_LINE_CACHE_10
|
|
934
|
-
|
|
947
|
+
response = (Thread.current[:raptor_response_buffer] ||= String.new(capacity: RESPONSE_BUFFER_CAPACITY))
|
|
948
|
+
response.clear
|
|
949
|
+
response << cache[status]
|
|
950
|
+
response
|
|
935
951
|
end
|
|
936
952
|
|
|
937
953
|
# Writes response headers and delegates body writing to the hijack callback.
|
|
@@ -1106,10 +1122,7 @@ module Raptor
|
|
|
1106
1122
|
end
|
|
1107
1123
|
end
|
|
1108
1124
|
|
|
1109
|
-
# Writes a single-element array body
|
|
1110
|
-
#
|
|
1111
|
-
# Small bodies are concatenated with the headers into one write to reduce
|
|
1112
|
-
# system call overhead.
|
|
1125
|
+
# Writes a single-element array body to the socket.
|
|
1113
1126
|
#
|
|
1114
1127
|
# @param socket [TCPSocket] the client socket
|
|
1115
1128
|
# @param response [String] headers already serialized, to be written before the body
|
|
@@ -1125,11 +1138,8 @@ module Raptor
|
|
|
1125
1138
|
if use_chunked
|
|
1126
1139
|
response << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n"
|
|
1127
1140
|
socket_write(socket, response)
|
|
1128
|
-
elsif chunk.bytesize < BODY_BUFFER_THRESHOLD
|
|
1129
|
-
socket_write(socket, response << chunk)
|
|
1130
1141
|
else
|
|
1131
|
-
|
|
1132
|
-
socket_write(socket, chunk)
|
|
1142
|
+
socket_writev(socket, [response, chunk])
|
|
1133
1143
|
end
|
|
1134
1144
|
end
|
|
1135
1145
|
|
|
@@ -1156,10 +1166,8 @@ module Raptor
|
|
|
1156
1166
|
else
|
|
1157
1167
|
body_array.each do |chunk|
|
|
1158
1168
|
raise TypeError, "body must yield String values" unless chunk.is_a?(String)
|
|
1159
|
-
|
|
1160
|
-
response << chunk
|
|
1161
1169
|
end
|
|
1162
|
-
|
|
1170
|
+
socket_writev(socket, [response, *body_array])
|
|
1163
1171
|
end
|
|
1164
1172
|
end
|
|
1165
1173
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "socket"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
begin
|
|
8
|
+
require "libbpf-ruby"
|
|
9
|
+
rescue LoadError
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module Raptor
|
|
13
|
+
# Routes incoming connections across worker processes to the worker with
|
|
14
|
+
# the lowest reactor backlog.
|
|
15
|
+
#
|
|
16
|
+
# Auto-enabled on Linux when the `libbpf-ruby` gem is installed and the
|
|
17
|
+
# BPF object file has been compiled. Falls back silently to standard
|
|
18
|
+
# `SO_REUSEPORT` when either is missing. Raises when the kernel refuses
|
|
19
|
+
# the BPF program. Wraps `tcp://` bindings only; non-tcp bindings are
|
|
20
|
+
# left to the binder's shared listeners.
|
|
21
|
+
#
|
|
22
|
+
module ReuseportBPF
|
|
23
|
+
LIBBPF_LOADED = !!defined?(LibBPFRuby)
|
|
24
|
+
BPF_OBJECT_PATH = File.expand_path("../../ext/raptor_bpf/reuseport_select.bpf.o", __dir__)
|
|
25
|
+
|
|
26
|
+
# Whether the preconditions for BPF-directed reuseport are met on this
|
|
27
|
+
# platform. Does not imply that the kernel will accept the program.
|
|
28
|
+
#
|
|
29
|
+
# @return [Boolean]
|
|
30
|
+
#
|
|
31
|
+
# @rbs () -> bool
|
|
32
|
+
def self.supported?
|
|
33
|
+
LIBBPF_LOADED && File.exist?(BPF_OBJECT_PATH)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Prepares BPF-directed routing for `worker_count` workers.
|
|
37
|
+
#
|
|
38
|
+
# Returns true when BPF-directed dispatch is active. Returns false
|
|
39
|
+
# when the platform is unsupported. Raises when the kernel refuses
|
|
40
|
+
# the program.
|
|
41
|
+
#
|
|
42
|
+
# @param worker_count [Integer] number of worker processes that will bind sockets
|
|
43
|
+
# @return [Boolean]
|
|
44
|
+
#
|
|
45
|
+
# @rbs (Integer worker_count) -> bool
|
|
46
|
+
def self.setup(worker_count)
|
|
47
|
+
return false unless supported?
|
|
48
|
+
|
|
49
|
+
@object = LibBPFRuby::Object.new(BPF_OBJECT_PATH)
|
|
50
|
+
@program_fd = @object.program_fd("select_least_loaded")
|
|
51
|
+
@socks_fd = @object.map_fd("socks")
|
|
52
|
+
@loads_fd = @object.map_fd("loads")
|
|
53
|
+
|
|
54
|
+
LibBPFRuby.map_update(@loads_fd, [0].pack("L"), [worker_count].pack("L"))
|
|
55
|
+
true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Returns tcp listening sockets bound and registered for `worker_index`.
|
|
59
|
+
# Skips non-tcp bind URIs.
|
|
60
|
+
#
|
|
61
|
+
# @param bind_uris [Array<String>] the configured bind URIs
|
|
62
|
+
# @param worker_index [Integer] slot index for this worker in the socks map
|
|
63
|
+
# @param socket_backlog [Integer] kernel listen() queue depth
|
|
64
|
+
# @return [Array<TCPServer>] the tcp listening sockets created for this worker
|
|
65
|
+
#
|
|
66
|
+
# @rbs (Array[String] bind_uris, Integer worker_index, Integer socket_backlog) -> Array[TCPServer]
|
|
67
|
+
def self.create_worker_listeners(bind_uris, worker_index, socket_backlog)
|
|
68
|
+
@load_key = [worker_index + 1].pack("L")
|
|
69
|
+
@load_value = String.new("\x00\x00\x00\x00", encoding: Encoding::ASCII_8BIT)
|
|
70
|
+
|
|
71
|
+
bind_uris.filter_map do |bind_uri|
|
|
72
|
+
uri = URI(bind_uri)
|
|
73
|
+
next unless uri.scheme == "tcp"
|
|
74
|
+
|
|
75
|
+
host = uri.host
|
|
76
|
+
host = host[1..-2] if host&.start_with?("[")
|
|
77
|
+
|
|
78
|
+
addrinfo = Addrinfo.tcp(host, uri.port)
|
|
79
|
+
socket = Socket.new(addrinfo.afamily, Socket::SOCK_STREAM, 0)
|
|
80
|
+
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
|
|
81
|
+
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, true)
|
|
82
|
+
socket.bind(addrinfo)
|
|
83
|
+
socket.listen(socket_backlog)
|
|
84
|
+
|
|
85
|
+
LibBPFRuby.sockmap_update(@socks_fd, [worker_index].pack("L"), socket)
|
|
86
|
+
LibBPFRuby.attach_reuseport(socket, @program_fd)
|
|
87
|
+
|
|
88
|
+
tcp_server = TCPServer.for_fd(socket.fileno)
|
|
89
|
+
socket.autoclose = false
|
|
90
|
+
tcp_server
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Publishes this worker's current backlog to the BPF map.
|
|
95
|
+
#
|
|
96
|
+
# @param backlog [Integer] the worker's current reactor backlog
|
|
97
|
+
# @return [void]
|
|
98
|
+
#
|
|
99
|
+
# @rbs (Integer backlog) -> void
|
|
100
|
+
def self.update_load(backlog)
|
|
101
|
+
@load_value.setbyte(0, backlog & 0xff)
|
|
102
|
+
@load_value.setbyte(1, (backlog >> 8) & 0xff)
|
|
103
|
+
@load_value.setbyte(2, (backlog >> 16) & 0xff)
|
|
104
|
+
@load_value.setbyte(3, (backlog >> 24) & 0xff)
|
|
105
|
+
LibBPFRuby.map_update(@loads_fd, @load_key, @load_value)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|