pitchfork 0.14.0 → 0.15.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.
@@ -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