raptor 0.23.0 → 0.25.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.
@@ -15,8 +15,8 @@
15
15
  #define MAX_FIELD_NAME 256
16
16
  #define MAX_FIELD_VALUE (80 * 1024)
17
17
 
18
- // Upper bound on chunk-size lines, extensions and CRLFs a chunked body may
19
- // carry beyond its payload before it is rejected as malformed.
18
+ // Bounds each chunk-size line and the terminal chunk/trailer section, including
19
+ // CRLFs. Also limits cumulative nonterminal overhead beyond the payload size.
20
20
  #define MAX_CHUNK_OVERHEAD (16 * 1024)
21
21
 
22
22
  #define FLAG_CHUNKED 0x1
@@ -34,6 +34,7 @@ typedef struct {
34
34
  int64_t content_len;
35
35
  unsigned int flags;
36
36
  VALUE request;
37
+ VALUE underscored_headers;
37
38
  // Scratch space for the env key: "HTTP_" prefix + field name + NUL.
38
39
  char buf[MAX_FIELD_NAME + 6];
39
40
  } raptor_parser;
@@ -125,13 +126,6 @@ static VALUE intern_field(const char *buf, size_t len) {
125
126
  return rb_enc_interned_str(buf, len, rb_utf8_encoding());
126
127
  }
127
128
 
128
- static inline void upcase_header_char(char *c) {
129
- if (*c >= 'a' && *c <= 'z')
130
- *c &= ~0x20;
131
- else if (*c == '-')
132
- *c = '_';
133
- }
134
-
135
129
  static int contains_chunked(const char *value, long len) {
136
130
  static const char chunked[] = "chunked";
137
131
  static const long chunked_len = sizeof(chunked) - 1;
@@ -159,6 +153,65 @@ static int64_t parse_content_length(const char *value, long len) {
159
153
  return content_len;
160
154
  }
161
155
 
156
+ static void parse_header(raptor_parser *parser, const char *field, const char *value, size_t value_len) {
157
+ size_t field_len = parser->field_len;
158
+ if (field_len == 0 || field_len > MAX_FIELD_NAME)
159
+ rb_raise(eHttpParserError, "invalid field name length");
160
+ if (value_len > MAX_FIELD_VALUE)
161
+ rb_raise(eHttpParserError, "field value too long");
162
+
163
+ int underscored = 0;
164
+ char *normalized = parser->buf + 5;
165
+ for (size_t i = 0; i < field_len; i++) {
166
+ char c = field[i];
167
+ if (c == '_') underscored = 1;
168
+ normalized[i] = c == '-' ? '_' : (c >= 'a' && c <= 'z' ? c & ~0x20 : c);
169
+ }
170
+ int content_length = field_len == 14 && memcmp(normalized, "CONTENT_LENGTH", 14) == 0;
171
+ int content_type = field_len == 12 && memcmp(normalized, "CONTENT_TYPE", 12) == 0;
172
+ int transfer_encoding = field_len == 17 && memcmp(normalized, "TRANSFER_ENCODING", 17) == 0;
173
+ if (underscored && (content_length || transfer_encoding))
174
+ rb_raise(eHttpParserError, "invalid framing header name");
175
+
176
+ size_t key_len = field_len;
177
+ if (content_length || content_type) {
178
+ memmove(parser->buf, normalized, field_len);
179
+ } else {
180
+ memcpy(parser->buf, "HTTP_", 5);
181
+ key_len += 5;
182
+ }
183
+ VALUE key = intern_field(parser->buf, key_len);
184
+ VALUE existing = rb_hash_aref(parser->request, key);
185
+ int existing_underscored = !NIL_P(parser->underscored_headers) &&
186
+ RTEST(rb_hash_aref(parser->underscored_headers, key));
187
+ // Canonical names win in either order; true duplicates still merge.
188
+ if (underscored) {
189
+ if (!NIL_P(existing) && !existing_underscored) return;
190
+ if (NIL_P(parser->underscored_headers)) parser->underscored_headers = rb_hash_new();
191
+ rb_hash_aset(parser->underscored_headers, key, Qtrue);
192
+ } else if (existing_underscored) {
193
+ existing = Qnil;
194
+ rb_hash_delete(parser->underscored_headers, key);
195
+ }
196
+
197
+ while (value_len > 0 && (value[value_len - 1] == ' ' || value[value_len - 1] == '\t'))
198
+ value_len--;
199
+ if (content_length) {
200
+ parser->content_len = parse_content_length(value, (long)value_len);
201
+ if (parser->content_len > 0) parser->flags |= FLAG_HAS_BODY;
202
+ } else if (transfer_encoding && contains_chunked(value, (long)value_len)) {
203
+ parser->flags |= FLAG_CHUNKED | FLAG_HAS_BODY;
204
+ parser->content_len = 0;
205
+ }
206
+
207
+ if (!NIL_P(existing)) {
208
+ rb_str_cat(existing, ", ", 2);
209
+ rb_str_cat(existing, value, (long)value_len);
210
+ } else {
211
+ rb_hash_aset(parser->request, key, rb_str_new(value, (long)value_len));
212
+ }
213
+ }
214
+
162
215
  static const int raptor_parser_start = 1;
163
216
 
164
217
  static size_t raptor_parser_execute(raptor_parser *parser, const char *buffer, size_t len) {
@@ -385,122 +438,12 @@ tr18:
385
438
  tr26:
386
439
  { MARK(mark, p); }
387
440
  {
388
- if (parser->field_len == 0 || parser->field_len > MAX_FIELD_NAME)
389
- rb_raise(eHttpParserError, "invalid field name length");
390
-
391
- size_t value_len = LEN(mark, p);
392
- if (value_len > MAX_FIELD_VALUE)
393
- rb_raise(eHttpParserError, "field value too long");
394
-
395
- const char *field_ptr = PTR_TO(field_start);
396
-
397
- int needs_http_prefix = 1;
398
- if (parser->field_len == 14 && memcmp(field_ptr, "CONTENT_LENGTH", 14) == 0)
399
- needs_http_prefix = 0;
400
- else if (parser->field_len == 12 && memcmp(field_ptr, "CONTENT_TYPE", 12) == 0)
401
- needs_http_prefix = 0;
402
-
403
- size_t key_len;
404
- if (needs_http_prefix) {
405
- memcpy(parser->buf, "HTTP_", 5);
406
- memcpy(parser->buf + 5, field_ptr, parser->field_len);
407
- key_len = 5 + parser->field_len;
408
- parser->buf[key_len] = '\0';
409
- } else {
410
- memcpy(parser->buf, field_ptr, parser->field_len);
411
- key_len = parser->field_len;
412
- parser->buf[key_len] = '\0';
413
- }
414
-
415
- VALUE key = intern_field(parser->buf, key_len);
416
- VALUE value = rb_str_new(PTR_TO(mark), value_len);
417
-
418
- char *value_ptr = RSTRING_PTR(value);
419
- long value_real_len = RSTRING_LEN(value);
420
- while (value_real_len > 0 && (value_ptr[value_real_len - 1] == ' ' ||
421
- value_ptr[value_real_len - 1] == '\t'))
422
- value_real_len--;
423
- rb_str_set_len(value, value_real_len);
424
-
425
- if (!needs_http_prefix && parser->field_len == 14) {
426
- parser->content_len = parse_content_length(value_ptr, value_real_len);
427
- if (parser->content_len > 0)
428
- parser->flags |= FLAG_HAS_BODY;
429
- } else if (needs_http_prefix && parser->field_len == 17 &&
430
- memcmp(field_ptr, "TRANSFER_ENCODING", 17) == 0) {
431
- if (contains_chunked(value_ptr, value_real_len)) {
432
- parser->flags |= FLAG_CHUNKED | FLAG_HAS_BODY;
433
- parser->content_len = 0;
434
- }
435
- }
436
-
437
- VALUE existing = rb_hash_aref(parser->request, key);
438
- if (!NIL_P(existing)) {
439
- rb_str_cat(existing, ", ", 2);
440
- rb_str_append(existing, value);
441
- } else {
442
- rb_hash_aset(parser->request, key, value);
443
- }
441
+ parse_header(parser, PTR_TO(field_start), PTR_TO(mark), LEN(mark, p));
444
442
  }
445
443
  goto st14;
446
444
  tr29:
447
445
  {
448
- if (parser->field_len == 0 || parser->field_len > MAX_FIELD_NAME)
449
- rb_raise(eHttpParserError, "invalid field name length");
450
-
451
- size_t value_len = LEN(mark, p);
452
- if (value_len > MAX_FIELD_VALUE)
453
- rb_raise(eHttpParserError, "field value too long");
454
-
455
- const char *field_ptr = PTR_TO(field_start);
456
-
457
- int needs_http_prefix = 1;
458
- if (parser->field_len == 14 && memcmp(field_ptr, "CONTENT_LENGTH", 14) == 0)
459
- needs_http_prefix = 0;
460
- else if (parser->field_len == 12 && memcmp(field_ptr, "CONTENT_TYPE", 12) == 0)
461
- needs_http_prefix = 0;
462
-
463
- size_t key_len;
464
- if (needs_http_prefix) {
465
- memcpy(parser->buf, "HTTP_", 5);
466
- memcpy(parser->buf + 5, field_ptr, parser->field_len);
467
- key_len = 5 + parser->field_len;
468
- parser->buf[key_len] = '\0';
469
- } else {
470
- memcpy(parser->buf, field_ptr, parser->field_len);
471
- key_len = parser->field_len;
472
- parser->buf[key_len] = '\0';
473
- }
474
-
475
- VALUE key = intern_field(parser->buf, key_len);
476
- VALUE value = rb_str_new(PTR_TO(mark), value_len);
477
-
478
- char *value_ptr = RSTRING_PTR(value);
479
- long value_real_len = RSTRING_LEN(value);
480
- while (value_real_len > 0 && (value_ptr[value_real_len - 1] == ' ' ||
481
- value_ptr[value_real_len - 1] == '\t'))
482
- value_real_len--;
483
- rb_str_set_len(value, value_real_len);
484
-
485
- if (!needs_http_prefix && parser->field_len == 14) {
486
- parser->content_len = parse_content_length(value_ptr, value_real_len);
487
- if (parser->content_len > 0)
488
- parser->flags |= FLAG_HAS_BODY;
489
- } else if (needs_http_prefix && parser->field_len == 17 &&
490
- memcmp(field_ptr, "TRANSFER_ENCODING", 17) == 0) {
491
- if (contains_chunked(value_ptr, value_real_len)) {
492
- parser->flags |= FLAG_CHUNKED | FLAG_HAS_BODY;
493
- parser->content_len = 0;
494
- }
495
- }
496
-
497
- VALUE existing = rb_hash_aref(parser->request, key);
498
- if (!NIL_P(existing)) {
499
- rb_str_cat(existing, ", ", 2);
500
- rb_str_append(existing, value);
501
- } else {
502
- rb_hash_aset(parser->request, key, value);
503
- }
446
+ parse_header(parser, PTR_TO(field_start), PTR_TO(mark), LEN(mark, p));
504
447
  }
505
448
  goto st14;
506
449
  st14:
@@ -560,10 +503,8 @@ case 46:
560
503
  goto st0;
561
504
  tr21:
562
505
  { MARK(field_start, p); }
563
- { upcase_header_char((char *)p); }
564
506
  goto st17;
565
507
  tr23:
566
- { upcase_header_char((char *)p); }
567
508
  goto st17;
568
509
  st17:
569
510
  if ( ++p == pe )
@@ -606,6 +547,7 @@ st18:
606
547
  goto _test_eof18;
607
548
  case 18:
608
549
  switch( (*p) ) {
550
+ case 9: goto tr27;
609
551
  case 13: goto tr26;
610
552
  case 32: goto tr27;
611
553
  case 127: goto st0;
@@ -1219,11 +1161,13 @@ static void raptor_parser_init(raptor_parser *parser) {
1219
1161
  parser->content_len = 0;
1220
1162
  parser->flags = 0;
1221
1163
  parser->request = Qnil;
1164
+ parser->underscored_headers = Qnil;
1222
1165
  }
1223
1166
 
1224
1167
  static void parser_mark(void *ptr) {
1225
1168
  raptor_parser *parser = ptr;
1226
1169
  rb_gc_mark(parser->request);
1170
+ rb_gc_mark(parser->underscored_headers);
1227
1171
  }
1228
1172
 
1229
1173
  static size_t parser_memsize(const void *ptr) {
@@ -1325,6 +1269,9 @@ static VALUE parser_decode_chunked(int argc, VALUE *argv, VALUE self) {
1325
1269
 
1326
1270
  while (offset < len) {
1327
1271
  const char *crlf = find_crlf(buf + offset, len - offset);
1272
+ size_t line_bytes = crlf ? (size_t)(crlf - (buf + offset)) + 2 : len - offset;
1273
+ if (line_bytes > MAX_CHUNK_OVERHEAD)
1274
+ return rb_ary_new_from_args(2, decoded, ID2SYM(id_malformed));
1328
1275
  if (!crlf) return rb_ary_new_from_args(2, decoded, ID2SYM(id_incomplete));
1329
1276
 
1330
1277
  size_t line_len = (size_t)(crlf - (buf + offset));
@@ -1354,6 +1301,9 @@ static VALUE parser_decode_chunked(int argc, VALUE *argv, VALUE self) {
1354
1301
  while (1) {
1355
1302
  if (trailer_offset >= len) return rb_ary_new_from_args(2, decoded, ID2SYM(id_incomplete));
1356
1303
  const char *tcrlf = find_crlf(buf + trailer_offset, len - trailer_offset);
1304
+ size_t terminal_bytes = tcrlf ? (size_t)(tcrlf - (buf + offset)) + 2 : len - offset;
1305
+ if (terminal_bytes > MAX_CHUNK_OVERHEAD)
1306
+ return rb_ary_new_from_args(2, decoded, ID2SYM(id_malformed));
1357
1307
  if (!tcrlf) return rb_ary_new_from_args(2, decoded, ID2SYM(id_incomplete));
1358
1308
  if (tcrlf == buf + trailer_offset) return rb_ary_new_from_args(3, decoded, ID2SYM(id_complete), SIZET2NUM(trailer_offset + 2));
1359
1309
  trailer_offset = (size_t)(tcrlf - buf) + 2;
@@ -1375,6 +1325,15 @@ static VALUE parser_decode_chunked(int argc, VALUE *argv, VALUE self) {
1375
1325
  size_t bytes_to_copy = (size_t)chunk_size < available ? (size_t)chunk_size : available;
1376
1326
  rb_str_cat(decoded, buf + data_offset, (long)bytes_to_copy);
1377
1327
 
1328
+ if (available <= (size_t)chunk_size)
1329
+ return rb_ary_new_from_args(2, decoded, ID2SYM(id_incomplete));
1330
+ if (buf[data_offset + (size_t)chunk_size] != '\r')
1331
+ return rb_ary_new_from_args(2, decoded, ID2SYM(id_malformed));
1332
+ if (available == (size_t)chunk_size + 1)
1333
+ return rb_ary_new_from_args(2, decoded, ID2SYM(id_incomplete));
1334
+ if (buf[data_offset + (size_t)chunk_size + 1] != '\n')
1335
+ return rb_ary_new_from_args(2, decoded, ID2SYM(id_malformed));
1336
+
1378
1337
  offset = data_offset + (size_t)chunk_size + 2;
1379
1338
  }
1380
1339
 
@@ -106,6 +106,7 @@ module Rackup
106
106
  result[:before_worker_boot] = config.fetch(:before_worker_boot, cli_defaults[:before_worker_boot])
107
107
  result[:before_worker_shutdown] = config.fetch(:before_worker_shutdown, cli_defaults[:before_worker_shutdown])
108
108
  result[:before_refork] = config.fetch(:before_refork, cli_defaults[:before_refork])
109
+ result[:plugins] = config.fetch(:plugins, cli_defaults[:plugins])
109
110
  result[:stats_file] = config.fetch(:stats_file, cli_defaults[:stats_file])
110
111
  result[:control_url] = config[:control_url] if config.key?(:control_url)
111
112
  result[:pid_file] = config[:pid_file] if config.key?(:pid_file)
@@ -113,7 +114,7 @@ module Rackup
113
114
  result[:stderr_file] = config[:stderr_file] if config.key?(:stderr_file)
114
115
  result[:access_log_file] = config[:access_log_file] if config.key?(:access_log_file)
115
116
  result[:on_error] = config[:on_error] if config.key?(:on_error)
116
- result
117
+ config.merge(result)
117
118
  end
118
119
  private_class_method :build_cluster_options
119
120
  end
data/lib/raptor/cli.rb CHANGED
@@ -70,6 +70,7 @@ module Raptor
70
70
  before_worker_boot: [].freeze,
71
71
  before_worker_shutdown: [].freeze,
72
72
  before_refork: [].freeze,
73
+ plugins: [].freeze,
73
74
  stats_file: "tmp/raptor.json",
74
75
  control_url: nil,
75
76
  pid_file: nil,
@@ -14,6 +14,7 @@ require_relative "control_server"
14
14
  require_relative "http1"
15
15
  require_relative "http2"
16
16
  require_relative "log"
17
+ require_relative "plugin"
17
18
  require_relative "raptor_native"
18
19
  require_relative "reactor"
19
20
  require_relative "reuseport_bpf"
@@ -126,6 +127,12 @@ module Raptor
126
127
  # @rbs @resp_r: IO?
127
128
  # @rbs @resp_w: IO?
128
129
  # @rbs @bpf_active: bool
130
+ # @rbs @plugins: Array[Plugin]
131
+
132
+ # Returns the cluster configuration available to plugins.
133
+ #
134
+ # @return [Hash{Symbol => untyped}]
135
+ attr_reader :options #: Hash[Symbol, untyped]
129
136
 
130
137
  # Creates a new Cluster with the given options.
131
138
  #
@@ -155,6 +162,7 @@ module Raptor
155
162
  # @option options [Array<Proc>] :before_worker_boot procs called with the worker index before it begins serving
156
163
  # @option options [Array<Proc>] :before_worker_shutdown procs called with the worker index before its graceful shutdown
157
164
  # @option options [Array<Proc>] :before_refork procs called in a worker before it transitions to a seed
165
+ # @option options [Array<String, Symbol>] :plugins plugins to load and start in order
158
166
  # @option options [String, nil] :stats_file path to write per-worker stats JSON, or nil to disable
159
167
  # @option options [String, nil] :control_url `unix://` URL serving cluster stats, or nil to disable
160
168
  # @option options [String, nil] :pid_file path to write the master PID to, or nil to disable
@@ -168,6 +176,8 @@ module Raptor
168
176
  #
169
177
  # @rbs (Hash[Symbol, untyped] options) -> void
170
178
  def initialize(options)
179
+ @options = options = options.dup
180
+ @plugins = []
171
181
  @drain_accept_queue = options[:drain_accept_queue]
172
182
  @worker_count = options[:workers]
173
183
  @http1_ractor_count = options[:http1][:ractors] || self.class.default_http1_ractor_count(@worker_count)
@@ -185,10 +195,10 @@ module Raptor
185
195
  @worker_timeout = options[:worker_timeout]
186
196
  @worker_drain_timeout = options[:worker_drain_timeout]
187
197
  @worker_shutdown_timeout = options[:worker_shutdown_timeout]
188
- @before_fork = Array(options[:before_fork])
189
- @before_worker_boot = Array(options[:before_worker_boot])
190
- @before_worker_shutdown = Array(options[:before_worker_shutdown])
191
- @before_refork = Array(options[:before_refork])
198
+ @before_fork = options[:before_fork] = Array(options[:before_fork]).dup
199
+ @before_worker_boot = options[:before_worker_boot] = Array(options[:before_worker_boot]).dup
200
+ @before_worker_shutdown = options[:before_worker_shutdown] = Array(options[:before_worker_shutdown]).dup
201
+ @before_refork = options[:before_refork] = Array(options[:before_refork]).dup
192
202
  @stats_file = options[:stats_file]
193
203
  @control_server = ControlServer.new(options[:control_url]) { stats_hash } if options[:control_url]
194
204
  @pid_file = options[:pid_file]
@@ -265,40 +275,61 @@ module Raptor
265
275
 
266
276
  @bpf_active = ReuseportBPF.setup(@worker_count, @binder.listeners.grep(TCPServer))
267
277
 
268
- @control_server&.bind
269
- @worker_count.times { |index| spawn_worker(index) }
270
- @control_server&.start
271
-
272
- stats_file_thread = if @stats_file
273
- Thread.new do
274
- Thread.current.name = "Stats File Writer"
275
-
276
- write_stats_file_loop
278
+ begin
279
+ Array(@options[:plugins]).each do |name|
280
+ plugin = Plugin.load(name)
281
+ @plugins << plugin
282
+ plugin.start(self)
277
283
  end
278
- end
279
284
 
280
- Systemd.notify("READY=1\nMAINPID=#{Process.pid}")
285
+ @control_server&.bind
286
+ @worker_count.times { |index| spawn_worker(index) }
287
+ @control_server&.start
281
288
 
282
- until @shutdown
283
- break if reap_workers == :no_children
289
+ stats_file_thread = if @stats_file
290
+ Thread.new do
291
+ Thread.current.name = "Stats File Writer"
284
292
 
285
- perform_hot_restart if @hot_restart_requested
286
- poll_seed_ready if @seed_pid && !@seed_ready
287
- perform_phased_restart if @phased_restart_requested && !@phased_restarting
288
- check_refork_trigger if @refork_thresholds.any? && !@phased_restarting
289
- timeout_hung_workers
293
+ write_stats_file_loop
294
+ end
295
+ end
290
296
 
291
- sleep 0.1
292
- end
297
+ ready = false
298
+
299
+ until @shutdown
300
+ break if reap_workers == :no_children
301
+
302
+ perform_hot_restart if @hot_restart_requested
303
+ poll_seed_ready if @seed_pid && !@seed_ready
304
+ perform_phased_restart if @phased_restart_requested && !@phased_restarting
305
+ check_refork_trigger if @refork_thresholds.any? && !@phased_restarting
306
+ timeout_hung_workers
307
+
308
+ unless ready || @shutdown
309
+ stats = @stats.all
310
+ if @workers.size == @worker_count && @workers.all? { |index, pid|
311
+ stat = stats[index]
312
+ stat[:pid] == pid && stat[:phase] == @phase && stat[:booted] && !@timed_out.include?(pid)
313
+ }
314
+ Systemd.notify("READY=1\nMAINPID=#{Process.pid}")
315
+ ready = true
316
+ end
317
+ end
293
318
 
294
- Systemd.notify("STOPPING=1")
295
- stop_workers
296
- stats_file_thread&.join
297
- @control_server&.shutdown
298
- File.delete(@stats_file) rescue nil if @stats_file
299
- File.delete(@pid_file) rescue nil if @pid_file
300
- @stats.unmap
301
- @binder.close
319
+ sleep 0.1
320
+ end
321
+ ensure
322
+ @shutdown = true
323
+ Systemd.notify("STOPPING=1")
324
+ stop_workers
325
+ stats_file_thread&.join
326
+ stop_plugins
327
+ @control_server&.shutdown
328
+ File.delete(@stats_file) rescue nil if @stats_file
329
+ File.delete(@pid_file) rescue nil if @pid_file
330
+ @stats.unmap
331
+ @binder.close
332
+ end
302
333
  end
303
334
 
304
335
  # Returns stats for all worker processes.
@@ -312,11 +343,9 @@ module Raptor
312
343
  @stats.all
313
344
  end
314
345
 
315
- private
316
-
317
- # Returns cluster statistics for the control server. For an adaptive
318
- # pool, `max_threads` is the number of threads currently running so
319
- # utilization reflects the capacity available at that moment.
346
+ # Returns cluster statistics for plugins and the control server.
347
+ # For an adaptive pool, `max_threads` is the number of threads currently
348
+ # running so utilization reflects the capacity available at that moment.
320
349
  #
321
350
  # @return [Hash] cluster statistics
322
351
  #
@@ -358,6 +387,22 @@ module Raptor
358
387
  }
359
388
  end
360
389
 
390
+ private
391
+
392
+ # Stops plugins in reverse startup order.
393
+ #
394
+ # @rbs () -> void
395
+ def stop_plugins
396
+ until @plugins.empty?
397
+ plugin = @plugins.pop
398
+ begin
399
+ plugin.shutdown
400
+ rescue StandardError => error
401
+ Log.rescued_error(error)
402
+ end
403
+ end
404
+ end
405
+
361
406
  # @rbs (Numeric time) -> String
362
407
  def timestamp(time)
363
408
  Time.at(time).utc.iso8601(6)
@@ -767,6 +812,7 @@ module Raptor
767
812
  Systemd.notify("RELOADING=1\nMONOTONIC_USEC=#{monotonic_usec}")
768
813
  @shutdown = true
769
814
  stop_workers
815
+ stop_plugins
770
816
  @binder.clear_close_on_exec
771
817
  ENV[INHERITED_FDS_ENV] = JSON.generate(@binder.inheritable_fds)
772
818
  @control_server&.shutdown
@@ -819,6 +865,8 @@ module Raptor
819
865
  booted: false
820
866
  )
821
867
 
868
+ @before_worker_boot.each { |hook| hook.parameters.empty? ? hook.call : hook.call(index) }
869
+
822
870
  reactor = nil
823
871
 
824
872
  counting_app = ->(env) {
@@ -902,8 +950,6 @@ module Raptor
902
950
  )
903
951
  server_thread = server.run
904
952
 
905
- @before_worker_boot.each { |hook| hook.parameters.empty? ? hook.call : hook.call(index) }
906
-
907
953
  Log.info "Worker #{index} booted"
908
954
 
909
955
  stats_thread = Thread.new do
@@ -71,6 +71,8 @@ module Raptor
71
71
  #
72
72
  # @rbs () -> void
73
73
  def shutdown
74
+ return unless @server
75
+
74
76
  client = nil
75
77
  @client.swap do |current|
76
78
  client = current
data/lib/raptor/http.rb CHANGED
@@ -16,7 +16,6 @@ module Raptor
16
16
 
17
17
  CONTENT_LENGTH = "CONTENT_LENGTH"
18
18
  CONTENT_TYPE = "CONTENT_TYPE"
19
- HTTP_VERSION = "HTTP_VERSION"
20
19
  REMOTE_ADDR = "REMOTE_ADDR"
21
20
  REQUEST_URI = "REQUEST_URI"
22
21
  SERVER_SOFTWARE = "SERVER_SOFTWARE"