pitchfork 0.14.0 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -36,12 +36,6 @@ void init_pitchfork_memory_page(VALUE);
36
36
 
37
37
  static unsigned int MAX_HEADER_LEN = 1024 * (80 + 32); /* same as Mongrel */
38
38
 
39
- /* this is only intended for use with Rainbows! */
40
- static VALUE set_maxhdrlen(VALUE self, VALUE len)
41
- {
42
- return UINT2NUM(MAX_HEADER_LEN = NUM2UINT(len));
43
- }
44
-
45
39
  /* keep this small for other servers (e.g. yahns) since every client has one */
46
40
  struct http_parser {
47
41
  int cs; /* Ragel internal state */
@@ -95,7 +89,7 @@ static inline unsigned int ulong2uint(unsigned long n)
95
89
  #define LEN(AT, FPC) (ulong2uint(FPC - buffer) - hp->AT)
96
90
  #define MARK(M,FPC) (hp->M = ulong2uint((FPC) - buffer))
97
91
  #define PTR_TO(F) (buffer + hp->F)
98
- #define STR_NEW(M,FPC) rb_str_new(PTR_TO(M), LEN(M, FPC))
92
+ #define STR_NEW(M,FPC) str_new(PTR_TO(M), LEN(M, FPC))
99
93
  #define STRIPPED_STR_NEW(M,FPC) stripped_str_new(PTR_TO(M), LEN(M, FPC))
100
94
 
101
95
  #define HP_FL_TEST(hp,fl) ((hp)->flags & (UH_FL_##fl))
@@ -108,13 +102,29 @@ static int is_lws(char c)
108
102
  return (c == ' ' || c == '\t');
109
103
  }
110
104
 
111
- static VALUE stripped_str_new(const char *str, long len)
105
+ static inline VALUE str_new(const char *str, long len)
106
+ {
107
+ VALUE rb_str = rb_str_new(str, len);
108
+ /* The Rack spec states:
109
+ > If the string values for CGI keys contain non-ASCII characters, they should use ASCII-8BIT encoding.
110
+ If they are ASCII, only the server is free to encode them as it wishes.
111
+ We chose to encode them as UTF-8 as any reasonable application would expect that today, and having the
112
+ same encoding as literal strings makes for slightly faster comparisons.
113
+ We'd like to also encode other strings that would be valid UTF-8 into UTF-8, but that would
114
+ violate the spec, so we leave them in BINARY aka ASCII-8BIT. */
115
+ if (rb_enc_str_asciionly_p(rb_str)) {
116
+ RB_ENCODING_SET_INLINED(rb_str, rb_utf8_encindex());
117
+ }
118
+ return rb_str;
119
+ }
120
+
121
+ static inline VALUE stripped_str_new(const char *str, long len)
112
122
  {
113
123
  long end;
114
124
 
115
125
  for (end = len - 1; end >= 0 && is_lws(str[end]); end--);
116
126
 
117
- return rb_str_new(str, end + 1);
127
+ return str_new(str, end + 1);
118
128
  }
119
129
 
120
130
  /*
@@ -330,24 +340,18 @@ static void write_value(VALUE self, struct http_parser *hp,
330
340
  }
331
341
  action host { rb_hash_aset(hp->env, g_http_host, STR_NEW(mark, fpc)); }
332
342
  action request_uri {
333
- VALUE str;
334
-
335
343
  VALIDATE_MAX_URI_LENGTH(LEN(mark, fpc), REQUEST_URI);
336
- str = rb_hash_aset(hp->env, g_request_uri, STR_NEW(mark, fpc));
337
- /*
338
- * "OPTIONS * HTTP/1.1\r\n" is a valid request, but we can't have '*'
339
- * in REQUEST_PATH or PATH_INFO or else Rack::Lint will complain
340
- */
344
+ rb_hash_aset(hp->env, g_request_uri, STR_NEW(mark, fpc));
345
+ }
346
+ action fragment {
347
+ VALIDATE_MAX_URI_LENGTH(LEN(mark, fpc), FRAGMENT);
348
+ VALUE str = rb_hash_aset(hp->env, g_fragment, STR_NEW(mark, fpc));
341
349
  if (STR_CSTR_EQ(str, "*")) {
342
- str = rb_str_new(NULL, 0);
350
+ VALUE str = rb_str_new("*", 1);
343
351
  rb_hash_aset(hp->env, g_path_info, str);
344
352
  rb_hash_aset(hp->env, g_request_path, str);
345
353
  }
346
354
  }
347
- action fragment {
348
- VALIDATE_MAX_URI_LENGTH(LEN(mark, fpc), FRAGMENT);
349
- rb_hash_aset(hp->env, g_fragment, STR_NEW(mark, fpc));
350
- }
351
355
  action start_query {MARK(start.query, fpc); }
352
356
  action query_string {
353
357
  VALIDATE_MAX_URI_LENGTH(LEN(start.query, fpc), QUERY_STRING);
@@ -612,6 +616,12 @@ static VALUE HttpParser_alloc(VALUE klass)
612
616
  return TypedData_Make_Struct(klass, struct http_parser, &hp_type, hp);
613
617
  }
614
618
 
619
+ #ifndef HAVE_RB_HASH_NEW_CAPA
620
+ static inline VALUE rb_hash_new_capa(long capa) {
621
+ return rb_hash_new();
622
+ }
623
+ #endif
624
+
615
625
  /**
616
626
  * call-seq:
617
627
  * parser.new => parser
@@ -624,7 +634,7 @@ static VALUE HttpParser_init(VALUE self)
624
634
 
625
635
  http_parser_init(hp);
626
636
  RB_OBJ_WRITE(self, &hp->buf, rb_str_new(NULL, 0));
627
- RB_OBJ_WRITE(self, &hp->env, rb_hash_new());
637
+ RB_OBJ_WRITE(self, &hp->env, rb_hash_new_capa(32)); // Even the simplest request will have 10 keys
628
638
 
629
639
  return self;
630
640
  }
@@ -1010,8 +1020,6 @@ RUBY_FUNC_EXPORTED void Init_pitchfork_http(void)
1010
1020
  */
1011
1021
  rb_define_const(cHttpParser, "LENGTH_MAX", OFFT2NUM(UH_OFF_T_MAX));
1012
1022
 
1013
- rb_define_singleton_method(cHttpParser, "max_header_len=", set_maxhdrlen, 1);
1014
-
1015
1023
  init_common_fields();
1016
1024
  SET_GLOBAL(g_http_host, "HOST");
1017
1025
  SET_GLOBAL(g_http_trailer, "TRAILER");
@@ -104,12 +104,15 @@ module Pitchfork
104
104
  def call(env)
105
105
  status, headers, body = response = @app.call(env)
106
106
 
107
- if chunkable_version?(env[Rack::SERVER_PROTOCOL]) &&
107
+ if !env.key?('rack.hijack_io') && # full highjack
108
+ !headers['rack.hijack'] && # partial hijack
109
+ body.respond_to?(:each) && # body must be enumerable (i.e. non-streaming)
110
+ chunkable_version?(env[Rack::SERVER_PROTOCOL]) &&
108
111
  !STATUS_WITH_NO_ENTITY_BODY.key?(status.to_i) &&
109
112
  !headers[Rack::CONTENT_LENGTH] &&
110
- !headers[Rack::TRANSFER_ENCODING]
113
+ !headers["transfer-encoding"]
111
114
 
112
- headers[Rack::TRANSFER_ENCODING] = 'chunked'
115
+ headers["transfer-encoding"] = 'chunked'
113
116
  if headers['trailer']
114
117
  response[2] = TrailerBody.new(body)
115
118
  else
@@ -230,7 +230,7 @@ module Pitchfork
230
230
  def listen(address, options = {})
231
231
  address = expand_addr(address)
232
232
  if String === address
233
- [ :umask, :backlog, :sndbuf, :rcvbuf, :tries ].each do |key|
233
+ [ :umask, :backlog, :sndbuf, :rcvbuf, :tries, :queues, :queues_per_worker].each do |key|
234
234
  value = options[key] or next
235
235
  Integer === value or
236
236
  raise ArgumentError, "not an integer: #{key}=#{value.inspect}"
@@ -1,4 +1,3 @@
1
- # -*- encoding: binary -*-
2
1
  # frozen_string_literal: true
3
2
  # :enddoc:
4
3
  # no stable API here
@@ -71,8 +71,10 @@ module Pitchfork
71
71
  if hijack
72
72
  req.hijacked!
73
73
  hijack.call(socket)
74
- else
74
+ elsif body.respond_to?(:each)
75
75
  body.each { |chunk| socket.write(chunk) }
76
+ else
77
+ body.call(socket)
76
78
  end
77
79
  end
78
80
  end
@@ -2,6 +2,7 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'pitchfork/pitchfork_http'
5
+ require 'pitchfork/listeners'
5
6
  require 'pitchfork/flock'
6
7
  require 'pitchfork/soft_timeout'
7
8
  require 'pitchfork/shared_memory'
@@ -79,8 +80,7 @@ module Pitchfork
79
80
  attr_accessor :app, :timeout, :timeout_signal, :soft_timeout, :cleanup_timeout, :spawn_timeout, :worker_processes,
80
81
  :before_fork, :after_worker_fork, :after_mold_fork, :before_service_worker_ready, :before_service_worker_exit,
81
82
  :listener_opts, :children,
82
- :orig_app, :config, :ready_pipe,
83
- :default_middleware, :early_hints
83
+ :orig_app, :config, :ready_pipe, :early_hints
84
84
  attr_writer :after_worker_exit, :before_worker_exit, :after_worker_ready, :after_request_complete,
85
85
  :refork_condition, :after_worker_timeout, :after_worker_hard_timeout, :after_monitor_ready
86
86
 
@@ -91,7 +91,7 @@ module Pitchfork
91
91
  # all bound listener sockets
92
92
  # note: this is public used by raindrops, but not recommended for use
93
93
  # in new projects
94
- LISTENERS = []
94
+ LISTENERS = Listeners.new
95
95
 
96
96
  NOOP = '.'
97
97
 
@@ -196,6 +196,10 @@ module Pitchfork
196
196
  # replaces current listener set with +listeners+. This will
197
197
  # close the socket if it will not exist in the new listener set
198
198
  def listeners=(listeners)
199
+ unless LISTENERS.empty?
200
+ raise "Listeners can only be initialized once"
201
+ end
202
+
199
203
  cur_names, dead_names = [], []
200
204
  listener_names.each do |name|
201
205
  if name.start_with?('/')
@@ -205,19 +209,7 @@ module Pitchfork
205
209
  cur_names << name
206
210
  end
207
211
  end
208
- set_names = listener_names(listeners)
209
- dead_names.concat(cur_names - set_names).uniq!
210
-
211
- LISTENERS.delete_if do |io|
212
- if dead_names.include?(sock_name(io))
213
- (io.close rescue nil).nil? # true
214
- else
215
- set_server_sockopt(io, listener_opts[sock_name(io)])
216
- false
217
- end
218
- end
219
-
220
- (set_names - cur_names).each { |addr| listen(addr) }
212
+ listener_names(listeners).each { |addr| listen(addr) }
221
213
  end
222
214
 
223
215
  def logger=(obj)
@@ -233,12 +225,16 @@ module Pitchfork
233
225
  # A negative value for +:tries+ indicates the listen will be
234
226
  # retried indefinitely, this is useful when workers belonging to
235
227
  # different masters are spawned during a transparent upgrade.
236
- def listen(address, opt = {}.merge(listener_opts[address] || {}))
228
+ def listen(address, opt = listener_opts[address] || {})
237
229
  address = config.expand_addr(address)
238
230
  return if String === address && listener_names.include?(address)
239
231
 
232
+ opt = opt.dup
240
233
  delay = opt[:delay] || 0.5
241
234
  tries = opt[:tries] || 5
235
+ queues = opt[:queues] ||= 1
236
+ opt[:reuseport] = true if queues > 1
237
+
242
238
  begin
243
239
  io = bind_listen(address, opt)
244
240
  unless TCPServer === io || UNIXServer === io
@@ -247,7 +243,7 @@ module Pitchfork
247
243
  end
248
244
  logger.info "listening on addr=#{sock_name(io)} fd=#{io.fileno}"
249
245
  Info.keep_io(io)
250
- LISTENERS << io
246
+ LISTENERS << io unless queues > 1
251
247
  io
252
248
  rescue Errno::EADDRINUSE => err
253
249
  logger.error "adding listener failed addr=#{address} (in use)"
@@ -261,6 +257,29 @@ module Pitchfork
261
257
  logger.fatal "error adding listener addr=#{address}"
262
258
  raise err
263
259
  end
260
+
261
+ if queues > 1
262
+ ios = [io]
263
+
264
+ (queues - 1).times do
265
+ io = bind_listen(address, opt)
266
+ unless TCPServer === io || UNIXServer === io
267
+ io.autoclose = false
268
+ io = server_cast(io)
269
+ end
270
+ logger.info "listening on addr=#{sock_name(io)} fd=#{io.fileno} (SO_REUSEPORT)"
271
+ Info.keep_io(io)
272
+ ios << io
273
+ rescue => err
274
+ logger.fatal "error adding listener addr=#{address}"
275
+ raise err
276
+ end
277
+
278
+ io = Listeners::Group.new(ios, queues_per_worker: opt[:queues_per_worker] || queues - 1)
279
+ LISTENERS << io
280
+ end
281
+
282
+ io
264
283
  end
265
284
 
266
285
  # monitors children and receives signals forever
@@ -371,7 +390,8 @@ module Pitchfork
371
390
  @respawn = false
372
391
  SharedMemory.shutting_down!
373
392
  wait_for_pending_workers
374
- self.listeners = []
393
+ LISTENERS.each(&:close).clear
394
+
375
395
  limit = Pitchfork.time_now + timeout
376
396
  until @children.empty? || Pitchfork.time_now > limit
377
397
  if graceful
@@ -897,7 +917,7 @@ module Pitchfork
897
917
 
898
918
  @config = nil
899
919
  @listener_opts = @orig_app = nil
900
- readers = LISTENERS.dup
920
+ readers = LISTENERS.for_worker(worker.nr)
901
921
  readers << worker
902
922
  trap(:QUIT) { nuke_listeners!(readers) }
903
923
  trap(:TERM) { nuke_listeners!(readers) }
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pitchfork
4
+
5
+ class Listeners
6
+ class Group
7
+ def initialize(listeners, queues_per_worker:)
8
+ @listeners = listeners
9
+ @queues_per_worker = queues_per_worker
10
+ end
11
+
12
+ def each(&block)
13
+ @listeners.each(&block)
14
+ end
15
+
16
+ def for_worker(nr)
17
+ index = nr % @listeners.size
18
+
19
+ listeners = @listeners.slice(index..-1) + @listeners.slice(0...index)
20
+ listeners.take(@queues_per_worker)
21
+ end
22
+ end
23
+
24
+ include Enumerable
25
+
26
+ def initialize(listeners = [])
27
+ @listeners = listeners
28
+ end
29
+
30
+ def for_worker(nr)
31
+ ios = []
32
+ @listeners.each do |listener|
33
+ if listener.is_a?(Group)
34
+ ios += listener.for_worker(nr)
35
+ else
36
+ ios << listener
37
+ end
38
+ end
39
+ ios
40
+ end
41
+
42
+ def each(&block)
43
+ @listeners.each do |listener|
44
+ if listener.is_a?(Group)
45
+ listener.each(&block)
46
+ else
47
+ yield listener
48
+ end
49
+ end
50
+ self
51
+ end
52
+
53
+ def clear
54
+ @listeners.clear
55
+ end
56
+
57
+ def <<(listener)
58
+ @listeners << listener
59
+ end
60
+
61
+ def empty?
62
+ @listeners.empty?
63
+ end
64
+ end
65
+ end
@@ -26,6 +26,8 @@ module Pitchfork
26
26
  :tcp_nodelay => true,
27
27
  }
28
28
 
29
+ private
30
+
29
31
  # configure platform-specific options (only tested on Linux 2.6 so far)
30
32
  def accf_arg(af_name)
31
33
  [ af_name, nil ].pack('a16a240')
@@ -64,7 +66,7 @@ module Pitchfork
64
66
  elsif respond_to?(:accf_arg)
65
67
  name = opt[:accept_filter]
66
68
  name = DEFAULTS[:accept_filter] if name.nil?
67
- sock.listen(opt[:backlog])
69
+ sock.listen(compute_backlog(opt))
68
70
  got = (sock.getsockopt(:SOL_SOCKET, :SO_ACCEPTFILTER) rescue nil).to_s
69
71
  arg = accf_arg(name)
70
72
  begin
@@ -89,11 +91,19 @@ module Pitchfork
89
91
  sock.setsockopt(:SOL_SOCKET, :SO_SNDBUF, sndbuf) if sndbuf
90
92
  log_buffer_sizes(sock, " after: ")
91
93
  end
92
- sock.listen(opt[:backlog])
94
+ sock.listen(compute_backlog(opt))
93
95
  rescue => e
94
96
  Pitchfork.log_error(logger, "#{sock_name(sock)} #{opt.inspect}", e)
95
97
  end
96
98
 
99
+ def compute_backlog(opt)
100
+ backlog = opt[:backlog]
101
+ if backlog > 0 && opt[:queues]
102
+ return backlog / opt[:queues]
103
+ end
104
+ backlog
105
+ end
106
+
97
107
  def log_buffer_sizes(sock, pfx = '')
98
108
  rcvbuf = sock.getsockopt(:SOL_SOCKET, :SO_RCVBUF).int
99
109
  sndbuf = sock.getsockopt(:SOL_SOCKET, :SO_SNDBUF).int
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pitchfork
4
- VERSION = "0.14.0"
4
+ VERSION = "0.15.0"
5
5
  module Const
6
6
  UNICORN_VERSION = '6.1.0'
7
7
  end
@@ -110,11 +110,6 @@ module Pitchfork
110
110
  @to_io.to_io
111
111
  end
112
112
 
113
- # master fakes SIGQUIT using this
114
- def quit # :nodoc:
115
- @master = @master.close if @master
116
- end
117
-
118
113
  def master=(socket)
119
114
  @master = MessageSocket.new(socket)
120
115
  end
data/pitchfork.gemspec CHANGED
@@ -19,7 +19,8 @@ Gem::Specification.new do |s|
19
19
 
20
20
  s.required_ruby_version = ">= 2.5.0"
21
21
 
22
- s.add_dependency(%q<rack>, '>= 2.0')
22
+ s.add_dependency('rack', '>= 2.0')
23
+ s.add_dependency('logger')
23
24
 
24
25
  # Note: To avoid ambiguity, we intentionally avoid the SPDX-compatible
25
26
  # 'Ruby' here since Ruby 1.9.3 switched to BSD-2-Clause, but we
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pitchfork
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.15.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jean Boussier
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-05-29 00:00:00.000000000 Z
11
+ date: 2024-07-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: logger
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  description: |-
28
42
  `pitchfork` is a preforking HTTP server for Rack applications designed
29
43
  to minimize memory usage by maximizing Copy-on-Write performance.
@@ -45,7 +59,6 @@ files:
45
59
  - COPYING
46
60
  - Dockerfile
47
61
  - Gemfile
48
- - Gemfile.lock
49
62
  - LICENSE
50
63
  - README.md
51
64
  - Rakefile
@@ -94,6 +107,7 @@ files:
94
107
  - lib/pitchfork/http_server.rb
95
108
  - lib/pitchfork/info.rb
96
109
  - lib/pitchfork/launcher.rb
110
+ - lib/pitchfork/listeners.rb
97
111
  - lib/pitchfork/mem_info.rb
98
112
  - lib/pitchfork/message.rb
99
113
  - lib/pitchfork/refork_condition.rb
@@ -127,7 +141,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
127
141
  - !ruby/object:Gem::Version
128
142
  version: '0'
129
143
  requirements: []
130
- rubygems_version: 3.5.9
144
+ rubygems_version: 3.5.11
131
145
  signing_key:
132
146
  specification_version: 4
133
147
  summary: Rack HTTP server for fast clients and Unix
data/Gemfile.lock DELETED
@@ -1,32 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- pitchfork (0.14.0)
5
- rack (>= 2.0)
6
-
7
- GEM
8
- remote: https://rubygems.org/
9
- specs:
10
- minitest (5.22.2)
11
- nio4r (2.7.0)
12
- puma (6.4.2)
13
- nio4r (~> 2.0)
14
- rack (3.0.11)
15
- rake (13.0.6)
16
- rake-compiler (1.2.1)
17
- rake
18
-
19
- PLATFORMS
20
- aarch64-linux
21
- arm64-darwin
22
- x86_64-linux
23
-
24
- DEPENDENCIES
25
- minitest
26
- pitchfork!
27
- puma
28
- rake
29
- rake-compiler
30
-
31
- BUNDLED WITH
32
- 2.3.27