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.
@@ -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")
@@ -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,15 @@ 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.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] == 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
  #
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
- data = begin
198
- socket.read_nonblock(READ_BUFFER_SIZE)
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
- data = begin
534
- socket.read_nonblock(READ_BUFFER_SIZE)
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
- # Builds the HTTP status line string.
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
- cache[status].dup
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, optionally buffering it with the headers.
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
- socket_write(socket, response)
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
- socket_write(socket, response)
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