raptor 0.8.0 → 0.10.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.
@@ -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";
@@ -2,6 +2,6 @@
2
2
 
3
3
  require "mkmf"
4
4
 
5
- append_cflags("-fvisibility=hidden")
5
+ append_cflags(["-fvisibility=hidden", "-Wno-type-limits"])
6
6
 
7
7
  create_makefile("raptor/raptor_http")
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  #include "ruby.h"
7
+ #include "ruby/encoding.h"
7
8
  #include <assert.h>
8
9
  #include <string.h>
9
10
  #include <ctype.h>
@@ -44,6 +45,75 @@ static VALUE global_server_protocol;
44
45
  static VALUE global_request_path;
45
46
  static VALUE global_fragment;
46
47
 
48
+ struct common_field {
49
+ const char *name;
50
+ size_t len;
51
+ VALUE interned;
52
+ };
53
+
54
+ #define FIELD(name) { name, sizeof(name) - 1, Qnil }
55
+
56
+ static struct common_field common_fields[] = {
57
+ FIELD("HTTP_HOST"),
58
+ FIELD("HTTP_USER_AGENT"),
59
+ FIELD("HTTP_CONNECTION"),
60
+ FIELD("HTTP_ACCEPT"),
61
+ FIELD("HTTP_ACCEPT_ENCODING"),
62
+ FIELD("HTTP_ACCEPT_LANGUAGE"),
63
+ FIELD("HTTP_ACCEPT_CHARSET"),
64
+ FIELD("HTTP_COOKIE"),
65
+ FIELD("HTTP_REFERER"),
66
+ FIELD("HTTP_CACHE_CONTROL"),
67
+ FIELD("HTTP_PRAGMA"),
68
+
69
+ FIELD("CONTENT_LENGTH"),
70
+ FIELD("CONTENT_TYPE"),
71
+ FIELD("HTTP_TRANSFER_ENCODING"),
72
+
73
+ FIELD("HTTP_AUTHORIZATION"),
74
+ FIELD("HTTP_ORIGIN"),
75
+ FIELD("HTTP_EXPECT"),
76
+
77
+ FIELD("HTTP_IF_MATCH"),
78
+ FIELD("HTTP_IF_NONE_MATCH"),
79
+ FIELD("HTTP_IF_MODIFIED_SINCE"),
80
+ FIELD("HTTP_IF_UNMODIFIED_SINCE"),
81
+ FIELD("HTTP_IF_RANGE"),
82
+ FIELD("HTTP_RANGE"),
83
+
84
+ FIELD("HTTP_UPGRADE"),
85
+ FIELD("HTTP_UPGRADE_INSECURE_REQUESTS"),
86
+
87
+ FIELD("HTTP_SEC_FETCH_DEST"),
88
+ FIELD("HTTP_SEC_FETCH_MODE"),
89
+ FIELD("HTTP_SEC_FETCH_SITE"),
90
+ FIELD("HTTP_SEC_FETCH_USER"),
91
+ FIELD("HTTP_SEC_CH_UA"),
92
+ FIELD("HTTP_SEC_CH_UA_MOBILE"),
93
+ FIELD("HTTP_SEC_CH_UA_PLATFORM"),
94
+ FIELD("HTTP_DNT"),
95
+
96
+ FIELD("HTTP_X_FORWARDED_FOR"),
97
+ FIELD("HTTP_X_FORWARDED_HOST"),
98
+ FIELD("HTTP_X_FORWARDED_PROTO"),
99
+ FIELD("HTTP_X_FORWARDED_SCHEME"),
100
+ FIELD("HTTP_X_FORWARDED_SSL"),
101
+ FIELD("HTTP_X_REAL_IP")
102
+ };
103
+
104
+ #undef FIELD
105
+
106
+ #define NUM_COMMON_FIELDS (sizeof(common_fields) / sizeof(common_fields[0]))
107
+
108
+ static VALUE raptor_http_intern_field(const char *buf, size_t len) {
109
+ for (size_t i = 0; i < NUM_COMMON_FIELDS; i++) {
110
+ if (common_fields[i].len == len && memcmp(common_fields[i].name, buf, len) == 0) {
111
+ return common_fields[i].interned;
112
+ }
113
+ }
114
+ return rb_enc_interned_str(buf, len, rb_utf8_encoding());
115
+ }
116
+
47
117
  static inline void upcase_header_char(char *c) {
48
118
  if (*c >= 'a' && *c <= 'z')
49
119
  *c &= ~0x20;
@@ -294,16 +364,19 @@ tr26:
294
364
  else if (parser->field_len == 12 && memcmp(field_ptr, "CONTENT_TYPE", 12) == 0)
295
365
  needs_http_prefix = 0;
296
366
 
367
+ size_t key_len;
297
368
  if (needs_http_prefix) {
298
369
  memcpy(parser->buf, "HTTP_", 5);
299
370
  memcpy(parser->buf + 5, field_ptr, parser->field_len);
300
- parser->buf[5 + parser->field_len] = '\0';
371
+ key_len = 5 + parser->field_len;
372
+ parser->buf[key_len] = '\0';
301
373
  } else {
302
374
  memcpy(parser->buf, field_ptr, parser->field_len);
303
- parser->buf[parser->field_len] = '\0';
375
+ key_len = parser->field_len;
376
+ parser->buf[key_len] = '\0';
304
377
  }
305
378
 
306
- VALUE key = rb_str_new2(parser->buf);
379
+ VALUE key = raptor_http_intern_field(parser->buf, key_len);
307
380
  VALUE value = rb_str_new(PTR_TO(mark), value_len);
308
381
 
309
382
  char *value_ptr = RSTRING_PTR(value);
@@ -351,16 +424,19 @@ tr29:
351
424
  else if (parser->field_len == 12 && memcmp(field_ptr, "CONTENT_TYPE", 12) == 0)
352
425
  needs_http_prefix = 0;
353
426
 
427
+ size_t key_len;
354
428
  if (needs_http_prefix) {
355
429
  memcpy(parser->buf, "HTTP_", 5);
356
430
  memcpy(parser->buf + 5, field_ptr, parser->field_len);
357
- parser->buf[5 + parser->field_len] = '\0';
431
+ key_len = 5 + parser->field_len;
432
+ parser->buf[key_len] = '\0';
358
433
  } else {
359
434
  memcpy(parser->buf, field_ptr, parser->field_len);
360
- parser->buf[parser->field_len] = '\0';
435
+ key_len = parser->field_len;
436
+ parser->buf[key_len] = '\0';
361
437
  }
362
438
 
363
- VALUE key = rb_str_new2(parser->buf);
439
+ VALUE key = raptor_http_intern_field(parser->buf, key_len);
364
440
  VALUE value = rb_str_new(PTR_TO(mark), value_len);
365
441
 
366
442
  char *value_ptr = RSTRING_PTR(value);
@@ -1237,6 +1313,11 @@ RUBY_FUNC_EXPORTED void Init_raptor_http(void) {
1237
1313
  global_request_path = rb_str_new2("PATH_INFO");
1238
1314
  global_fragment = rb_str_new2("FRAGMENT");
1239
1315
 
1316
+ for (size_t i = 0; i < NUM_COMMON_FIELDS; i++) {
1317
+ common_fields[i].interned = rb_enc_interned_str(common_fields[i].name, common_fields[i].len, rb_utf8_encoding());
1318
+ rb_global_variable(&common_fields[i].interned);
1319
+ }
1320
+
1240
1321
  rb_define_alloc_func(cHttpParser, parser_alloc);
1241
1322
  rb_define_method(cHttpParser, "execute", parser_execute, 3);
1242
1323
  rb_define_method(cHttpParser, "finished?", parser_finished_p, 0);
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+
5
+ append_cflags("-fvisibility=hidden")
6
+
7
+ create_makefile("raptor/raptor_native")
@@ -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(&current);
51
+ if (sched_getaffinity(0, sizeof(current), &current) < 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, &current)) 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,16 @@ module Raptor
244
254
 
245
255
  host = host[1..-2] if host&.start_with?("[")
246
256
 
247
- tcp_server = TCPServer.new(host, port)
248
- tcp_server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
249
- tcp_server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEPORT, true) if Socket.const_defined?(:SO_REUSEPORT)
250
- tcp_server.listen @socket_backlog
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.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
262
+ socket.bind(addrinfo)
263
+ socket.listen(@socket_backlog)
251
264
 
265
+ tcp_server = TCPServer.for_fd(socket.fileno)
266
+ socket.autoclose = false
252
267
  [tcp_server]
253
268
  end
254
269
 
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] == DEFAULT_OPTIONS[:binds]
232
+ if @options[:binds].equal?(DEFAULT_OPTIONS[:binds])
233
233
  @options[:binds] = [bind]
234
234
  else
235
235
  @options[:binds] << bind
@@ -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
- server = Server.new(@binder, reactor, thread_pool, http1, http2, connection_options: @connection_options, drain_accept_queue: @drain_accept_queue)
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
  #