raptor 0.7.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 +30 -0
- data/Dockerfile +28 -0
- data/README.md +159 -27
- 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/rackup/handler/raptor.rb +12 -2
- data/lib/raptor/binder.rb +139 -31
- data/lib/raptor/cli.rb +83 -22
- data/lib/raptor/cluster.rb +214 -33
- data/lib/raptor/http.rb +116 -0
- data/lib/raptor/{request.rb → http1.rb} +228 -99
- data/lib/raptor/http2.rb +85 -40
- data/lib/raptor/reactor.rb +22 -15
- data/lib/raptor/reuseport_bpf.rb +108 -0
- data/lib/raptor/server.rb +92 -43
- data/lib/raptor/stats.rb +1 -1
- data/lib/raptor/systemd.rb +69 -0
- data/lib/raptor/version.rb +1 -1
- data/sig/generated/raptor/binder.rbs +82 -5
- data/sig/generated/raptor/cli.rbs +2 -3
- data/sig/generated/raptor/cluster.rbs +91 -13
- data/sig/generated/raptor/http.rbs +65 -0
- data/sig/generated/raptor/{request.rbs → http1.rbs} +122 -35
- data/sig/generated/raptor/http2.rbs +46 -13
- data/sig/generated/raptor/reactor.rbs +18 -11
- data/sig/generated/raptor/reuseport_bpf.rbs +56 -0
- data/sig/generated/raptor/server.rbs +49 -21
- data/sig/generated/raptor/systemd.rbs +42 -0
- metadata +30 -3
|
@@ -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
|
+
}
|
|
@@ -78,18 +78,28 @@ module Rackup
|
|
|
78
78
|
else
|
|
79
79
|
config[:binds] || ["tcp://#{defaults[:Host]}:#{defaults[:Port]}"]
|
|
80
80
|
end,
|
|
81
|
+
socket_backlog: (config[:socket_backlog] || cli_defaults[:socket_backlog]).to_i,
|
|
82
|
+
drain_accept_queue: config.key?(:drain_accept_queue) ? config[:drain_accept_queue] : cli_defaults[:drain_accept_queue],
|
|
81
83
|
workers: (options[:Workers] || config[:workers] || Etc.nprocessors).to_i,
|
|
82
84
|
ractors: (options[:Ractors] || config[:ractors] || cli_defaults[:ractors]).to_i,
|
|
83
85
|
threads: (options[:Threads] || config[:threads] || cli_defaults[:threads]).to_i,
|
|
84
86
|
app: app
|
|
85
87
|
}
|
|
86
88
|
result[:rackup] = config[:rackup] if config.key?(:rackup)
|
|
87
|
-
result[:
|
|
88
|
-
result[:
|
|
89
|
+
result[:chdir] = config[:chdir] if config.key?(:chdir)
|
|
90
|
+
result[:environment] = config[:environment] if config.key?(:environment)
|
|
91
|
+
::Raptor::CLI::NESTED_OPTION_KEYS.each do |key|
|
|
92
|
+
result[key] = cli_defaults[key].merge(config[key] || {})
|
|
93
|
+
end
|
|
89
94
|
result[:worker_boot_timeout] = (config[:worker_boot_timeout] || cli_defaults[:worker_boot_timeout]).to_i
|
|
95
|
+
result[:worker_timeout] = (config[:worker_timeout] || cli_defaults[:worker_timeout]).to_i
|
|
96
|
+
result[:worker_drain_timeout] = (config[:worker_drain_timeout] || cli_defaults[:worker_drain_timeout]).to_i
|
|
90
97
|
result[:worker_shutdown_timeout] = (config[:worker_shutdown_timeout] || cli_defaults[:worker_shutdown_timeout]).to_i
|
|
91
98
|
result[:stats_file] = config.key?(:stats_file) ? config[:stats_file] : cli_defaults[:stats_file]
|
|
92
99
|
result[:pid_file] = config[:pid_file] if config.key?(:pid_file)
|
|
100
|
+
result[:stdout_file] = config[:stdout_file] if config.key?(:stdout_file)
|
|
101
|
+
result[:stderr_file] = config[:stderr_file] if config.key?(:stderr_file)
|
|
102
|
+
result[:access_log_file] = config[:access_log_file] if config.key?(:access_log_file)
|
|
93
103
|
result[:on_error] = config[:on_error] if config.key?(:on_error)
|
|
94
104
|
result
|
|
95
105
|
end
|
data/lib/raptor/binder.rb
CHANGED
|
@@ -56,7 +56,20 @@ module Raptor
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
# @rbs @bind_uris: Array[String]
|
|
59
|
+
# @rbs @socket_backlog: Integer
|
|
60
|
+
# @rbs @inherited_fds: Hash[String, Array[Integer]]
|
|
59
61
|
# @rbs @listeners: Array[TCPServer | UNIXServer | SslListener]
|
|
62
|
+
# @rbs @uri_listeners: Hash[String, Array[TCPServer | UNIXServer | SslListener]]
|
|
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
|
|
60
73
|
|
|
61
74
|
# Array of listening sockets.
|
|
62
75
|
#
|
|
@@ -67,19 +80,26 @@ module Raptor
|
|
|
67
80
|
#
|
|
68
81
|
# Parses the provided bind URIs and creates listening sockets for each one.
|
|
69
82
|
# Supports tcp://, unix://, and ssl:// schemes. Localhost is expanded to
|
|
70
|
-
# all available loopback addresses (both IPv4 and IPv6).
|
|
83
|
+
# all available loopback addresses (both IPv4 and IPv6). When `inherited_fds`
|
|
84
|
+
# supplies file descriptors for a URI, the listener is reconstructed from
|
|
85
|
+
# those FDs instead of binding fresh.
|
|
71
86
|
#
|
|
72
87
|
# @param bind_uris [Array<String>] array of URI strings to bind to
|
|
88
|
+
# @param socket_backlog [Integer] kernel listen() queue depth for TCP/SSL listeners
|
|
89
|
+
# @param inherited_fds [Hash{String => Array<Integer>}] inherited listener FDs keyed by bind URI
|
|
73
90
|
# @return [void]
|
|
74
91
|
# @raise [UnknownBindSchemeError] if a URI has an unsupported scheme
|
|
75
92
|
#
|
|
76
93
|
# @example
|
|
77
94
|
# binder = Binder.new(["tcp://0.0.0.0:3000", "unix:///tmp/raptor.sock"])
|
|
78
95
|
#
|
|
79
|
-
# @rbs (Array[String] bind_uris) -> void
|
|
80
|
-
def initialize(bind_uris)
|
|
96
|
+
# @rbs (Array[String] bind_uris, ?socket_backlog: Integer, ?inherited_fds: Hash[String, Array[Integer]]) -> void
|
|
97
|
+
def initialize(bind_uris, socket_backlog: SOCKET_BACKLOG, inherited_fds: {})
|
|
81
98
|
@bind_uris = bind_uris
|
|
99
|
+
@socket_backlog = socket_backlog
|
|
100
|
+
@inherited_fds = inherited_fds
|
|
82
101
|
@listeners = nil
|
|
102
|
+
@uri_listeners = nil
|
|
83
103
|
parse
|
|
84
104
|
end
|
|
85
105
|
|
|
@@ -133,28 +153,91 @@ module Raptor
|
|
|
133
153
|
@listeners.each(&:close)
|
|
134
154
|
end
|
|
135
155
|
|
|
156
|
+
# Returns the file descriptors of every listener, grouped by the bind URI
|
|
157
|
+
# they were created from. The result is the payload to hand to a successor
|
|
158
|
+
# process via the `inherited_fds:` constructor argument.
|
|
159
|
+
#
|
|
160
|
+
# @return [Hash{String => Array<Integer>}]
|
|
161
|
+
#
|
|
162
|
+
# @rbs () -> Hash[String, Array[Integer]]
|
|
163
|
+
def inheritable_fds
|
|
164
|
+
@uri_listeners.transform_values { |listeners| listeners.map { |listener| listener.to_io.fileno } }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Clears the close-on-exec flag on every listener so the file descriptors
|
|
168
|
+
# survive `Kernel.exec`.
|
|
169
|
+
#
|
|
170
|
+
# @return [void]
|
|
171
|
+
#
|
|
172
|
+
# @rbs () -> void
|
|
173
|
+
def clear_close_on_exec
|
|
174
|
+
@listeners.each { |listener| listener.to_io.close_on_exec = false }
|
|
175
|
+
end
|
|
176
|
+
|
|
136
177
|
private
|
|
137
178
|
|
|
138
|
-
# Parses bind URIs and creates listening sockets
|
|
179
|
+
# Parses bind URIs and creates listening sockets, reusing inherited file
|
|
180
|
+
# descriptors for URIs supplied in `@inherited_fds`.
|
|
139
181
|
#
|
|
140
182
|
# @return [void]
|
|
141
183
|
# @raise [UnknownBindSchemeError] if a URI scheme is not supported
|
|
142
184
|
#
|
|
143
185
|
# @rbs () -> void
|
|
144
186
|
def parse
|
|
145
|
-
@
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
when "tcp"
|
|
149
|
-
create_tcp_listeners(uri.host, uri.port)
|
|
150
|
-
when "unix"
|
|
151
|
-
create_unix_listeners(uri.path)
|
|
152
|
-
when "ssl"
|
|
153
|
-
create_ssl_listeners(uri.host, uri.port, URI.decode_www_form(uri.query || "").to_h)
|
|
187
|
+
@uri_listeners = @bind_uris.to_h do |bind_uri|
|
|
188
|
+
if filenos = @inherited_fds[bind_uri]
|
|
189
|
+
[bind_uri, restore_listeners(bind_uri, filenos)]
|
|
154
190
|
else
|
|
155
|
-
|
|
191
|
+
[bind_uri, create_listeners(bind_uri)]
|
|
156
192
|
end
|
|
157
|
-
end
|
|
193
|
+
end
|
|
194
|
+
@listeners = @uri_listeners.values.flatten
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Creates fresh listeners for the given bind URI.
|
|
198
|
+
#
|
|
199
|
+
# @param bind_uri [String] the URI to bind
|
|
200
|
+
# @return [Array<TCPServer, UNIXServer, SslListener>]
|
|
201
|
+
# @raise [UnknownBindSchemeError] if the URI scheme is not supported
|
|
202
|
+
#
|
|
203
|
+
# @rbs (String bind_uri) -> Array[TCPServer | UNIXServer | SslListener]
|
|
204
|
+
def create_listeners(bind_uri)
|
|
205
|
+
uri = URI.parse(bind_uri)
|
|
206
|
+
case uri.scheme
|
|
207
|
+
when "tcp"
|
|
208
|
+
create_tcp_listeners(uri.host, uri.port)
|
|
209
|
+
when "unix"
|
|
210
|
+
create_unix_listeners(uri.path)
|
|
211
|
+
when "ssl"
|
|
212
|
+
create_ssl_listeners(uri.host, uri.port, URI.decode_www_form(uri.query || "").to_h)
|
|
213
|
+
else
|
|
214
|
+
raise UnknownBindSchemeError.new(uri.scheme)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Reconstructs listeners for the given bind URI from inherited file
|
|
219
|
+
# descriptors.
|
|
220
|
+
#
|
|
221
|
+
# @param bind_uri [String] the URI the FDs were bound to
|
|
222
|
+
# @param filenos [Array<Integer>] file descriptors to wrap
|
|
223
|
+
# @return [Array<TCPServer, UNIXServer, SslListener>]
|
|
224
|
+
# @raise [UnknownBindSchemeError] if the URI scheme is not supported
|
|
225
|
+
#
|
|
226
|
+
# @rbs (String bind_uri, Array[Integer] filenos) -> Array[TCPServer | UNIXServer | SslListener]
|
|
227
|
+
def restore_listeners(bind_uri, filenos)
|
|
228
|
+
uri = URI.parse(bind_uri)
|
|
229
|
+
case uri.scheme
|
|
230
|
+
when "tcp"
|
|
231
|
+
filenos.map { |fileno| TCPServer.for_fd(fileno) }
|
|
232
|
+
when "unix"
|
|
233
|
+
register_unix_socket_cleanup(uri.path)
|
|
234
|
+
filenos.map { |fileno| UNIXServer.for_fd(fileno) }
|
|
235
|
+
when "ssl"
|
|
236
|
+
ssl_context = build_ssl_context(URI.decode_www_form(uri.query || "").to_h)
|
|
237
|
+
filenos.map { |fileno| SslListener.new(tcp_server: TCPServer.for_fd(fileno), ssl_context: ssl_context) }
|
|
238
|
+
else
|
|
239
|
+
raise UnknownBindSchemeError.new(uri.scheme)
|
|
240
|
+
end
|
|
158
241
|
end
|
|
159
242
|
|
|
160
243
|
# Creates TCP server sockets for the given host and port.
|
|
@@ -171,11 +254,15 @@ module Raptor
|
|
|
171
254
|
|
|
172
255
|
host = host[1..-2] if host&.start_with?("[")
|
|
173
256
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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)
|
|
178
263
|
|
|
264
|
+
tcp_server = TCPServer.for_fd(socket.fileno)
|
|
265
|
+
socket.autoclose = false
|
|
179
266
|
[tcp_server]
|
|
180
267
|
end
|
|
181
268
|
|
|
@@ -200,11 +287,22 @@ module Raptor
|
|
|
200
287
|
end
|
|
201
288
|
end
|
|
202
289
|
|
|
203
|
-
|
|
290
|
+
register_unix_socket_cleanup(path)
|
|
291
|
+
|
|
292
|
+
[UNIXServer.new(path)]
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Registers an `at_exit` hook that removes the Unix socket file on the
|
|
296
|
+
# owning master's clean exit. Each call records the current process so
|
|
297
|
+
# forked workers won't delete a socket their master still owns.
|
|
298
|
+
#
|
|
299
|
+
# @param path [String] filesystem path of the Unix socket
|
|
300
|
+
# @return [void]
|
|
301
|
+
#
|
|
302
|
+
# @rbs (String path) -> void
|
|
303
|
+
def register_unix_socket_cleanup(path)
|
|
204
304
|
master_pid = Process.pid
|
|
205
305
|
at_exit { File.delete(path) rescue nil if Process.pid == master_pid }
|
|
206
|
-
|
|
207
|
-
[server]
|
|
208
306
|
end
|
|
209
307
|
|
|
210
308
|
# Creates SSL server sockets for the given host, port, and SSL parameters.
|
|
@@ -220,18 +318,28 @@ module Raptor
|
|
|
220
318
|
#
|
|
221
319
|
# @rbs (String? host, Integer? port, Hash[String, String] ssl_params) -> Array[SslListener]
|
|
222
320
|
def create_ssl_listeners(host, port, ssl_params)
|
|
223
|
-
require "openssl"
|
|
224
|
-
|
|
225
321
|
tcp_servers = create_tcp_listeners(host, port)
|
|
322
|
+
ssl_context = build_ssl_context(ssl_params)
|
|
323
|
+
tcp_servers.map { |tcp_server| SslListener.new(tcp_server: tcp_server, ssl_context: ssl_context) }
|
|
324
|
+
end
|
|
226
325
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
326
|
+
# Builds a frozen `OpenSSL::SSL::SSLContext` configured for HTTP/2 and
|
|
327
|
+
# HTTP/1.1 ALPN negotiation.
|
|
328
|
+
#
|
|
329
|
+
# @param ssl_params [Hash<String, String>] SSL options ("cert" and "key" paths)
|
|
330
|
+
# @return [OpenSSL::SSL::SSLContext]
|
|
331
|
+
#
|
|
332
|
+
# @rbs (Hash[String, String] ssl_params) -> OpenSSL::SSL::SSLContext
|
|
333
|
+
def build_ssl_context(ssl_params)
|
|
334
|
+
require "openssl"
|
|
233
335
|
|
|
234
|
-
|
|
336
|
+
OpenSSL::SSL::SSLContext.new.tap do |ssl_context|
|
|
337
|
+
ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(ssl_params["cert"]))
|
|
338
|
+
ssl_context.key = OpenSSL::PKey.read(File.read(ssl_params["key"]))
|
|
339
|
+
ssl_context.alpn_protocols = ["h2", "http/1.1"]
|
|
340
|
+
ssl_context.alpn_select_cb = ->(protocols) { protocols.include?("h2") ? "h2" : "http/1.1" }
|
|
341
|
+
ssl_context.freeze
|
|
342
|
+
end
|
|
235
343
|
end
|
|
236
344
|
|
|
237
345
|
# Returns all available loopback IP addresses.
|
data/lib/raptor/cli.rb
CHANGED
|
@@ -26,24 +26,41 @@ module Raptor
|
|
|
26
26
|
class CLI
|
|
27
27
|
DEFAULT_WORKER_COUNT = Etc.nprocessors
|
|
28
28
|
|
|
29
|
+
NESTED_OPTION_KEYS = [:connection, :http1, :http2].freeze
|
|
30
|
+
|
|
29
31
|
DEFAULT_OPTIONS = {
|
|
30
32
|
binds: ["tcp://0.0.0.0:9292"].freeze,
|
|
33
|
+
socket_backlog: 1024,
|
|
34
|
+
drain_accept_queue: false,
|
|
31
35
|
workers: DEFAULT_WORKER_COUNT,
|
|
32
36
|
ractors: 1,
|
|
33
37
|
threads: 3,
|
|
34
38
|
rackup: "config.ru",
|
|
35
|
-
|
|
39
|
+
chdir: nil,
|
|
40
|
+
environment: nil,
|
|
41
|
+
connection: {
|
|
36
42
|
first_data_timeout: 30,
|
|
37
43
|
chunk_data_timeout: 10,
|
|
38
|
-
|
|
44
|
+
write_timeout: 5,
|
|
39
45
|
max_body_size: nil,
|
|
40
46
|
body_spool_threshold: 1024 * 1024,
|
|
41
47
|
},
|
|
42
|
-
|
|
48
|
+
http1: {
|
|
49
|
+
persistent_data_timeout: 65,
|
|
50
|
+
max_keepalive_requests: 100,
|
|
51
|
+
},
|
|
52
|
+
http2: {
|
|
53
|
+
max_concurrent_streams: 100,
|
|
54
|
+
},
|
|
43
55
|
worker_boot_timeout: 60,
|
|
56
|
+
worker_timeout: 60,
|
|
57
|
+
worker_drain_timeout: 25,
|
|
44
58
|
worker_shutdown_timeout: 30,
|
|
45
59
|
stats_file: "tmp/raptor.json",
|
|
46
60
|
pid_file: nil,
|
|
61
|
+
stdout_file: nil,
|
|
62
|
+
stderr_file: nil,
|
|
63
|
+
access_log_file: nil,
|
|
47
64
|
}.freeze
|
|
48
65
|
|
|
49
66
|
DEFAULT_CONFIG_PATHS = ["raptor.rb", "config/raptor.rb"].freeze
|
|
@@ -102,14 +119,17 @@ module Raptor
|
|
|
102
119
|
#
|
|
103
120
|
# @rbs (Array[String] argv) -> void
|
|
104
121
|
def initialize(argv)
|
|
122
|
+
@options = DEFAULT_OPTIONS.dup
|
|
123
|
+
NESTED_OPTION_KEYS.each { |key| @options[key] = @options[key].dup }
|
|
124
|
+
@options[:launch_command] = $PROGRAM_NAME
|
|
125
|
+
@options[:launch_argv] = argv.dup
|
|
126
|
+
|
|
105
127
|
if argv.first == "stats"
|
|
106
128
|
argv.shift
|
|
107
129
|
@command = :stats
|
|
108
130
|
else
|
|
109
131
|
@command = :server
|
|
110
132
|
end
|
|
111
|
-
@options = DEFAULT_OPTIONS.dup
|
|
112
|
-
@options[:client] = @options[:client].dup
|
|
113
133
|
|
|
114
134
|
apply_config_file(extract_config_path(argv) || self.class.default_config_path)
|
|
115
135
|
|
|
@@ -178,9 +198,6 @@ module Raptor
|
|
|
178
198
|
|
|
179
199
|
# Loads a config file and merges it into `@options` over the defaults.
|
|
180
200
|
#
|
|
181
|
-
# Top-level keys replace defaults; the nested `:client` hash is merged
|
|
182
|
-
# key-by-key so a config file does not need to restate every client option.
|
|
183
|
-
#
|
|
184
201
|
# @param path [String, nil] path to the config file, or nil to no-op
|
|
185
202
|
# @return [void]
|
|
186
203
|
#
|
|
@@ -190,8 +207,8 @@ module Raptor
|
|
|
190
207
|
|
|
191
208
|
config = self.class.load_config_file(path)
|
|
192
209
|
config.each do |key, value|
|
|
193
|
-
if key
|
|
194
|
-
@options[
|
|
210
|
+
if NESTED_OPTION_KEYS.include?(key) && value.is_a?(Hash)
|
|
211
|
+
@options[key] = @options[key].merge(value)
|
|
195
212
|
else
|
|
196
213
|
@options[key] = value
|
|
197
214
|
end
|
|
@@ -212,53 +229,85 @@ module Raptor
|
|
|
212
229
|
end
|
|
213
230
|
|
|
214
231
|
opts.on("-b", "--bind URI", String, "Bind address (default: tcp://0.0.0.0:9292)") do |bind|
|
|
215
|
-
if @options[:binds]
|
|
232
|
+
if @options[:binds].equal?(DEFAULT_OPTIONS[:binds])
|
|
216
233
|
@options[:binds] = [bind]
|
|
217
234
|
else
|
|
218
235
|
@options[:binds] << bind
|
|
219
236
|
end
|
|
220
237
|
end
|
|
221
238
|
|
|
239
|
+
opts.on("--socket-backlog NUM", Integer, "Socket listen backlog (default: 1024)") do |num|
|
|
240
|
+
@options[:socket_backlog] = num
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
opts.on("--[no-]drain-accept-queue", "Drain the kernel accept queue on shutdown (default: off)") do |bool|
|
|
244
|
+
@options[:drain_accept_queue] = bool
|
|
245
|
+
end
|
|
246
|
+
|
|
222
247
|
opts.on("-w", "--workers NUM", Integer, "Number of worker processes (default: #{DEFAULT_WORKER_COUNT})") do |num|
|
|
223
248
|
@options[:workers] = num
|
|
224
249
|
end
|
|
225
250
|
|
|
226
|
-
opts.on("-r", "--ractors NUM", Integer, "Number of ractors (default: 1)") do |num|
|
|
251
|
+
opts.on("-r", "--ractors NUM", Integer, "Number of pipeline ractors per worker (default: 1)") do |num|
|
|
227
252
|
@options[:ractors] = num
|
|
228
253
|
end
|
|
229
254
|
|
|
230
|
-
opts.on("-t", "--threads NUM", Integer, "Number of threads (default: 3)") do |num|
|
|
255
|
+
opts.on("-t", "--threads NUM", Integer, "Number of application threads per worker (default: 3)") do |num|
|
|
231
256
|
@options[:threads] = num
|
|
232
257
|
end
|
|
233
258
|
|
|
259
|
+
opts.on("-C", "--chdir PATH", String, "Change to PATH before loading the Rack application (default: none)") do |path|
|
|
260
|
+
@options[:chdir] = path
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
opts.on("-e", "--environment ENV", String, "Application environment label; falls back to $RAILS_ENV, then $RACK_ENV, then development") do |env|
|
|
264
|
+
@options[:environment] = env
|
|
265
|
+
end
|
|
266
|
+
|
|
234
267
|
opts.on("--first-data-timeout SECONDS", Integer, "First data timeout in seconds (default: 30)") do |timeout|
|
|
235
|
-
@options[:
|
|
268
|
+
@options[:connection][:first_data_timeout] = timeout
|
|
236
269
|
end
|
|
237
270
|
|
|
238
271
|
opts.on("--chunk-data-timeout SECONDS", Integer, "Chunk data timeout in seconds (default: 10)") do |timeout|
|
|
239
|
-
@options[:
|
|
272
|
+
@options[:connection][:chunk_data_timeout] = timeout
|
|
240
273
|
end
|
|
241
274
|
|
|
242
|
-
opts.on("--
|
|
243
|
-
@options[:
|
|
275
|
+
opts.on("--write-timeout SECONDS", Integer, "Per-write socket timeout in seconds (default: 5)") do |timeout|
|
|
276
|
+
@options[:connection][:write_timeout] = timeout
|
|
244
277
|
end
|
|
245
278
|
|
|
246
279
|
opts.on("--max-body-size BYTES", Integer, "Maximum request body size in bytes (default: unlimited)") do |bytes|
|
|
247
|
-
@options[:
|
|
280
|
+
@options[:connection][:max_body_size] = bytes
|
|
248
281
|
end
|
|
249
282
|
|
|
250
|
-
opts.on("--body-spool-threshold BYTES", Integer, "
|
|
251
|
-
@options[:
|
|
283
|
+
opts.on("--body-spool-threshold BYTES", Integer, "Request body spool threshold in bytes (default: #{1024 * 1024})") do |bytes|
|
|
284
|
+
@options[:connection][:body_spool_threshold] = bytes
|
|
252
285
|
end
|
|
253
286
|
|
|
254
|
-
opts.on("--
|
|
255
|
-
@options[:
|
|
287
|
+
opts.on("--http1-persistent-data-timeout SECONDS", Integer, "HTTP/1.1 keep-alive idle timeout in seconds (default: 65)") do |timeout|
|
|
288
|
+
@options[:http1][:persistent_data_timeout] = timeout
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
opts.on("--http1-max-keepalive-requests NUM", Integer, "Maximum HTTP/1.1 requests per keep-alive connection (default: 100)") do |num|
|
|
292
|
+
@options[:http1][:max_keepalive_requests] = num
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
opts.on("--http2-max-concurrent-streams NUM", Integer, "Maximum HTTP/2 concurrent streams per connection (default: 100)") do |num|
|
|
296
|
+
@options[:http2][:max_concurrent_streams] = num
|
|
256
297
|
end
|
|
257
298
|
|
|
258
299
|
opts.on("--worker-boot-timeout SECONDS", Integer, "Worker boot timeout in seconds (default: 60)") do |timeout|
|
|
259
300
|
@options[:worker_boot_timeout] = timeout
|
|
260
301
|
end
|
|
261
302
|
|
|
303
|
+
opts.on("--worker-timeout SECONDS", Integer, "Worker check-in timeout in seconds (default: 60)") do |timeout|
|
|
304
|
+
@options[:worker_timeout] = timeout
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
opts.on("--worker-drain-timeout SECONDS", Integer, "Worker request-drain timeout in seconds (default: 25)") do |timeout|
|
|
308
|
+
@options[:worker_drain_timeout] = timeout
|
|
309
|
+
end
|
|
310
|
+
|
|
262
311
|
opts.on("--worker-shutdown-timeout SECONDS", Integer, "Worker shutdown timeout in seconds (default: 30)") do |timeout|
|
|
263
312
|
@options[:worker_shutdown_timeout] = timeout
|
|
264
313
|
end
|
|
@@ -271,6 +320,18 @@ module Raptor
|
|
|
271
320
|
@options[:pid_file] = path
|
|
272
321
|
end
|
|
273
322
|
|
|
323
|
+
opts.on("--stdout-file PATH", String, "Redirect stdout to PATH; reopened on SIGHUP (default: none)") do |path|
|
|
324
|
+
@options[:stdout_file] = path
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
opts.on("--stderr-file PATH", String, "Redirect stderr to PATH; reopened on SIGHUP (default: none)") do |path|
|
|
328
|
+
@options[:stderr_file] = path
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
opts.on("--access-log-file PATH", String, "Write Common Log Format access logs to PATH; reopened on SIGHUP (default: none)") do |path|
|
|
332
|
+
@options[:access_log_file] = path
|
|
333
|
+
end
|
|
334
|
+
|
|
274
335
|
opts.on("--help", "Show this help") do
|
|
275
336
|
puts opts
|
|
276
337
|
exit
|