puma 3.12.0 → 4.3.8

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of puma might be problematic. Click here for more details.

Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +164 -0
  3. data/README.md +76 -48
  4. data/docs/architecture.md +1 -0
  5. data/docs/deployment.md +24 -4
  6. data/docs/plugins.md +20 -10
  7. data/docs/restart.md +4 -2
  8. data/docs/systemd.md +27 -9
  9. data/docs/tcp_mode.md +96 -0
  10. data/ext/puma_http11/PumaHttp11Service.java +2 -0
  11. data/ext/puma_http11/extconf.rb +13 -0
  12. data/ext/puma_http11/http11_parser.c +40 -63
  13. data/ext/puma_http11/http11_parser.java.rl +21 -37
  14. data/ext/puma_http11/http11_parser.rl +3 -1
  15. data/ext/puma_http11/http11_parser_common.rl +3 -3
  16. data/ext/puma_http11/mini_ssl.c +86 -4
  17. data/ext/puma_http11/org/jruby/puma/Http11.java +106 -114
  18. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +91 -106
  19. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
  20. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +15 -4
  21. data/ext/puma_http11/puma_http11.c +3 -0
  22. data/lib/puma.rb +8 -0
  23. data/lib/puma/accept_nonblock.rb +7 -1
  24. data/lib/puma/app/status.rb +37 -29
  25. data/lib/puma/binder.rb +47 -68
  26. data/lib/puma/cli.rb +6 -0
  27. data/lib/puma/client.rb +244 -199
  28. data/lib/puma/cluster.rb +55 -30
  29. data/lib/puma/commonlogger.rb +2 -0
  30. data/lib/puma/configuration.rb +6 -3
  31. data/lib/puma/const.rb +32 -18
  32. data/lib/puma/control_cli.rb +41 -14
  33. data/lib/puma/detect.rb +2 -0
  34. data/lib/puma/dsl.rb +311 -77
  35. data/lib/puma/events.rb +6 -1
  36. data/lib/puma/io_buffer.rb +3 -6
  37. data/lib/puma/jruby_restart.rb +2 -0
  38. data/lib/puma/launcher.rb +99 -55
  39. data/lib/puma/minissl.rb +37 -17
  40. data/lib/puma/minissl/context_builder.rb +76 -0
  41. data/lib/puma/null_io.rb +2 -0
  42. data/lib/puma/plugin.rb +7 -2
  43. data/lib/puma/plugin/tmp_restart.rb +2 -0
  44. data/lib/puma/rack/builder.rb +4 -1
  45. data/lib/puma/rack/urlmap.rb +2 -0
  46. data/lib/puma/rack_default.rb +2 -0
  47. data/lib/puma/reactor.rb +112 -57
  48. data/lib/puma/runner.rb +13 -3
  49. data/lib/puma/server.rb +119 -48
  50. data/lib/puma/single.rb +5 -3
  51. data/lib/puma/state_file.rb +2 -0
  52. data/lib/puma/tcp_logger.rb +2 -0
  53. data/lib/puma/thread_pool.rb +17 -33
  54. data/lib/puma/util.rb +2 -6
  55. data/lib/rack/handler/puma.rb +6 -3
  56. data/tools/docker/Dockerfile +16 -0
  57. data/tools/jungle/init.d/puma +6 -6
  58. data/tools/trickletest.rb +0 -1
  59. metadata +26 -14
  60. data/lib/puma/compat.rb +0 -14
  61. data/lib/puma/convenient.rb +0 -23
  62. data/lib/puma/daemon_ext.rb +0 -31
  63. data/lib/puma/delegation.rb +0 -11
  64. data/lib/puma/java_io_buffer.rb +0 -45
  65. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
data/lib/puma/cluster.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'puma/runner'
2
4
  require 'puma/util'
3
5
  require 'puma/plugin'
@@ -17,8 +19,6 @@ module Puma
17
19
  # via the `spawn_workers` method call. Each worker will have it's own
18
20
  # instance of a `Puma::Server`.
19
21
  class Cluster < Runner
20
- WORKER_CHECK_INTERVAL = 5
21
-
22
22
  def initialize(cli, events)
23
23
  super cli, events
24
24
 
@@ -35,7 +35,11 @@ module Puma
35
35
  @workers.each { |x| x.term }
36
36
 
37
37
  begin
38
- @workers.each { |w| Process.waitpid(w.pid) }
38
+ loop do
39
+ wait_workers
40
+ break if @workers.empty?
41
+ sleep 0.2
42
+ end
39
43
  rescue Interrupt
40
44
  log "! Cancelled waiting for workers"
41
45
  end
@@ -67,12 +71,13 @@ module Puma
67
71
  @signal = "TERM"
68
72
  @options = options
69
73
  @first_term_sent = nil
74
+ @started_at = Time.now
70
75
  @last_checkin = Time.now
71
76
  @last_status = '{}'
72
- @dead = false
77
+ @term = false
73
78
  end
74
79
 
75
- attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status
80
+ attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
76
81
 
77
82
  def booted?
78
83
  @stage == :booted
@@ -83,12 +88,8 @@ module Puma
83
88
  @stage = :booted
84
89
  end
85
90
 
86
- def dead?
87
- @dead
88
- end
89
-
90
- def dead!
91
- @dead = true
91
+ def term?
92
+ @term
92
93
  end
93
94
 
94
95
  def ping!(status)
@@ -105,9 +106,9 @@ module Puma
105
106
  if @first_term_sent && (Time.now - @first_term_sent) > @options[:worker_shutdown_timeout]
106
107
  @signal = "KILL"
107
108
  else
109
+ @term ||= true
108
110
  @first_term_sent ||= Time.now
109
111
  end
110
-
111
112
  Process.kill @signal, @pid
112
113
  rescue Errno::ESRCH
113
114
  end
@@ -181,7 +182,7 @@ module Puma
181
182
  def check_workers(force=false)
182
183
  return if !force && @next_check && @next_check >= Time.now
183
184
 
184
- @next_check = Time.now + WORKER_CHECK_INTERVAL
185
+ @next_check = Time.now + Const::WORKER_CHECK_INTERVAL
185
186
 
186
187
  any = false
187
188
 
@@ -198,15 +199,7 @@ module Puma
198
199
  # during this loop by giving the kernel time to kill them.
199
200
  sleep 1 if any
200
201
 
201
- while @workers.any?
202
- pid = Process.waitpid(-1, Process::WNOHANG)
203
- break unless pid
204
-
205
- @workers.delete_if { |w| w.pid == pid }
206
- end
207
-
208
- @workers.delete_if(&:dead?)
209
-
202
+ wait_workers
210
203
  cull_workers
211
204
  spawn_workers
212
205
 
@@ -223,8 +216,10 @@ module Puma
223
216
  log "- Stopping #{w.pid} for phased upgrade..."
224
217
  end
225
218
 
226
- w.term
227
- log "- #{w.signal} sent to #{w.pid}..."
219
+ unless w.term?
220
+ w.term
221
+ log "- #{w.signal} sent to #{w.pid}..."
222
+ end
228
223
  end
229
224
  end
230
225
  end
@@ -251,6 +246,7 @@ module Puma
251
246
  @suicide_pipe.close
252
247
 
253
248
  Thread.new do
249
+ Puma.set_thread_name "worker check pipe"
254
250
  IO.select [@check_pipe]
255
251
  log "! Detected parent died, dying"
256
252
  exit! 1
@@ -273,6 +269,7 @@ module Puma
273
269
  server = start_server
274
270
 
275
271
  Signal.trap "SIGTERM" do
272
+ @worker_write << "e#{Process.pid}\n" rescue nil
276
273
  server.stop
277
274
  end
278
275
 
@@ -285,10 +282,11 @@ module Puma
285
282
  end
286
283
 
287
284
  Thread.new(@worker_write) do |io|
285
+ Puma.set_thread_name "stat payload"
288
286
  base_payload = "p#{Process.pid}"
289
287
 
290
288
  while true
291
- sleep WORKER_CHECK_INTERVAL
289
+ sleep Const::WORKER_CHECK_INTERVAL
292
290
  begin
293
291
  b = server.backlog || 0
294
292
  r = server.running || 0
@@ -350,11 +348,13 @@ module Puma
350
348
  Dir.chdir dir
351
349
  end
352
350
 
351
+ # Inside of a child process, this will return all zeroes, as @workers is only populated in
352
+ # the master process.
353
353
  def stats
354
354
  old_worker_count = @workers.count { |w| w.phase != @phase }
355
355
  booted_worker_count = @workers.count { |w| w.booted? }
356
- worker_status = '[' + @workers.map { |w| %Q!{ "pid": #{w.pid}, "index": #{w.index}, "phase": #{w.phase}, "booted": #{w.booted?}, "last_checkin": "#{w.last_checkin.utc.iso8601}", "last_status": #{w.last_status} }!}.join(",") + ']'
357
- %Q!{ "workers": #{@workers.size}, "phase": #{@phase}, "booted_workers": #{booted_worker_count}, "old_workers": #{old_worker_count}, "worker_status": #{worker_status} }!
356
+ worker_status = '[' + @workers.map { |w| %Q!{ "started_at": "#{w.started_at.utc.iso8601}", "pid": #{w.pid}, "index": #{w.index}, "phase": #{w.phase}, "booted": #{w.booted?}, "last_checkin": "#{w.last_checkin.utc.iso8601}", "last_status": #{w.last_status} }!}.join(",") + ']'
357
+ %Q!{ "started_at": "#{@started_at.utc.iso8601}", "workers": #{@workers.size}, "phase": #{@phase}, "booted_workers": #{booted_worker_count}, "old_workers": #{old_worker_count}, "worker_status": #{worker_status} }!
358
358
  end
359
359
 
360
360
  def preload?
@@ -388,10 +388,13 @@ module Puma
388
388
  log "Early termination of worker"
389
389
  exit! 0
390
390
  else
391
+ @launcher.close_binder_listeners
392
+
391
393
  stop_workers
392
394
  stop
393
395
 
394
- raise SignalException, "SIGTERM"
396
+ raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
397
+ exit 0 # Clean exit, workers were stopped
395
398
  end
396
399
  end
397
400
  end
@@ -485,7 +488,7 @@ module Puma
485
488
 
486
489
  force_check = false
487
490
 
488
- res = IO.select([read], nil, nil, WORKER_CHECK_INTERVAL)
491
+ res = IO.select([read], nil, nil, Const::WORKER_CHECK_INTERVAL)
489
492
 
490
493
  if res
491
494
  req = read.read_nonblock(1)
@@ -501,8 +504,11 @@ module Puma
501
504
  w.boot!
502
505
  log "- Worker #{w.index} (pid: #{pid}) booted, phase: #{w.phase}"
503
506
  force_check = true
507
+ when "e"
508
+ # external term, see worker method, Signal.trap "SIGTERM"
509
+ w.instance_variable_set :@term, true
504
510
  when "t"
505
- w.dead!
511
+ w.term unless w.term?
506
512
  force_check = true
507
513
  when "p"
508
514
  w.ping!(result.sub(/^\d+/,'').chomp)
@@ -525,5 +531,24 @@ module Puma
525
531
  @wakeup.close
526
532
  end
527
533
  end
534
+
535
+ private
536
+
537
+ # loops thru @workers, removing workers that exited, and calling
538
+ # `#term` if needed
539
+ def wait_workers
540
+ @workers.reject! do |w|
541
+ begin
542
+ if Process.wait(w.pid, Process::WNOHANG)
543
+ true
544
+ else
545
+ w.term if w.term?
546
+ nil
547
+ end
548
+ rescue Errno::ECHILD
549
+ true # child is already terminated
550
+ end
551
+ end
552
+ end
528
553
  end
529
554
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Puma
2
4
  # Rack::CommonLogger forwards every request to the given +app+, and
3
5
  # logs a line in the
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'puma/rack/builder'
2
4
  require 'puma/plugin'
3
5
  require 'puma/const'
@@ -18,7 +20,7 @@ module Puma
18
20
  # In this class any "user" specified options take precedence over any
19
21
  # "file" specified options, take precedence over any "default" options.
20
22
  #
21
- # User input is prefered over "defaults":
23
+ # User input is preferred over "defaults":
22
24
  # user_options = { foo: "bar" }
23
25
  # default_options = { foo: "zoo" }
24
26
  # options = UserFileDefaultOptions.new(user_options, default_options)
@@ -30,7 +32,7 @@ module Puma
30
32
  # puts options.all_of(:foo)
31
33
  # # => ["bar", "zoo"]
32
34
  #
33
- # A "file" option can be set. This config will be prefered over "default" options
35
+ # A "file" option can be set. This config will be preferred over "default" options
34
36
  # but will defer to any available "user" specified options.
35
37
  #
36
38
  # user_options = { foo: "bar" }
@@ -184,7 +186,8 @@ module Puma
184
186
  :rackup => DefaultRackup,
185
187
  :logger => STDOUT,
186
188
  :persistent_timeout => Const::PERSISTENT_TIMEOUT,
187
- :first_data_timeout => Const::FIRST_DATA_TIMEOUT
189
+ :first_data_timeout => Const::FIRST_DATA_TIMEOUT,
190
+ :raise_exception_on_sigterm => true
188
191
  }
189
192
  end
190
193
 
data/lib/puma/const.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  #encoding: utf-8
2
+ # frozen_string_literal: true
3
+
2
4
  module Puma
3
5
  class UnsupportedOption < RuntimeError
4
6
  end
@@ -98,8 +100,8 @@ module Puma
98
100
  # too taxing on performance.
99
101
  module Const
100
102
 
101
- PUMA_VERSION = VERSION = "3.12.0".freeze
102
- CODE_NAME = "Llamas in Pajamas".freeze
103
+ PUMA_VERSION = VERSION = "4.3.8".freeze
104
+ CODE_NAME = "Mysterious Traveller".freeze
103
105
  PUMA_SERVER_STRING = ['puma', PUMA_VERSION, CODE_NAME].join(' ').freeze
104
106
 
105
107
  FAST_TRACK_KA_TIMEOUT = 0.2
@@ -116,31 +118,35 @@ module Puma
116
118
  # sending data back
117
119
  WRITE_TIMEOUT = 10
118
120
 
121
+ # How many requests to attempt inline before sending a client back to
122
+ # the reactor to be subject to normal ordering. The idea here is that
123
+ # we amortize the cost of going back to the reactor for a well behaved
124
+ # but very "greedy" client across 10 requests. This prevents a not
125
+ # well behaved client from monopolizing the thread forever.
126
+ MAX_FAST_INLINE = 10
127
+
119
128
  # The original URI requested by the client.
120
129
  REQUEST_URI= 'REQUEST_URI'.freeze
121
130
  REQUEST_PATH = 'REQUEST_PATH'.freeze
122
131
  QUERY_STRING = 'QUERY_STRING'.freeze
132
+ CONTENT_LENGTH = "CONTENT_LENGTH".freeze
123
133
 
124
134
  PATH_INFO = 'PATH_INFO'.freeze
125
135
 
126
136
  PUMA_TMP_BASE = "puma".freeze
127
137
 
128
- # Indicate that we couldn't parse the request
129
- ERROR_400_RESPONSE = "HTTP/1.1 400 Bad Request\r\n\r\n".freeze
130
-
131
- # The standard empty 404 response for bad requests. Use Error4040Handler for custom stuff.
132
- ERROR_404_RESPONSE = "HTTP/1.1 404 Not Found\r\nConnection: close\r\nServer: Puma #{PUMA_VERSION}\r\n\r\nNOT FOUND".freeze
133
-
134
- # The standard empty 408 response for requests that timed out.
135
- ERROR_408_RESPONSE = "HTTP/1.1 408 Request Timeout\r\nConnection: close\r\nServer: Puma #{PUMA_VERSION}\r\n\r\n".freeze
136
-
137
- CONTENT_LENGTH = "CONTENT_LENGTH".freeze
138
-
139
- # Indicate that there was an internal error, obviously.
140
- ERROR_500_RESPONSE = "HTTP/1.1 500 Internal Server Error\r\n\r\n".freeze
141
-
142
- # A common header for indicating the server is too busy. Not used yet.
143
- ERROR_503_RESPONSE = "HTTP/1.1 503 Service Unavailable\r\n\r\nBUSY".freeze
138
+ ERROR_RESPONSE = {
139
+ # Indicate that we couldn't parse the request
140
+ 400 => "HTTP/1.1 400 Bad Request\r\n\r\n".freeze,
141
+ # The standard empty 404 response for bad requests. Use Error4040Handler for custom stuff.
142
+ 404 => "HTTP/1.1 404 Not Found\r\nConnection: close\r\nServer: Puma #{PUMA_VERSION}\r\n\r\nNOT FOUND".freeze,
143
+ # The standard empty 408 response for requests that timed out.
144
+ 408 => "HTTP/1.1 408 Request Timeout\r\nConnection: close\r\nServer: Puma #{PUMA_VERSION}\r\n\r\n".freeze,
145
+ # Indicate that there was an internal error, obviously.
146
+ 500 => "HTTP/1.1 500 Internal Server Error\r\n\r\n".freeze,
147
+ # A common header for indicating the server is too busy. Not used yet.
148
+ 503 => "HTTP/1.1 503 Service Unavailable\r\n\r\nBUSY".freeze
149
+ }
144
150
 
145
151
  # The basic max request size we'll try to read.
146
152
  CHUNK_SIZE = 16 * 1024
@@ -158,6 +164,9 @@ module Puma
158
164
  LINE_END = "\r\n".freeze
159
165
  REMOTE_ADDR = "REMOTE_ADDR".freeze
160
166
  HTTP_X_FORWARDED_FOR = "HTTP_X_FORWARDED_FOR".freeze
167
+ HTTP_X_FORWARDED_SSL = "HTTP_X_FORWARDED_SSL".freeze
168
+ HTTP_X_FORWARDED_SCHEME = "HTTP_X_FORWARDED_SCHEME".freeze
169
+ HTTP_X_FORWARDED_PROTO = "HTTP_X_FORWARDED_PROTO".freeze
161
170
 
162
171
  SERVER_NAME = "SERVER_NAME".freeze
163
172
  SERVER_PORT = "SERVER_PORT".freeze
@@ -219,11 +228,16 @@ module Puma
219
228
  COLON = ": ".freeze
220
229
 
221
230
  NEWLINE = "\n".freeze
231
+ HTTP_INJECTION_REGEX = /[\r\n]/.freeze
222
232
 
223
233
  HIJACK_P = "rack.hijack?".freeze
224
234
  HIJACK = "rack.hijack".freeze
225
235
  HIJACK_IO = "rack.hijack_io".freeze
226
236
 
227
237
  EARLY_HINTS = "rack.early_hints".freeze
238
+
239
+ # Mininum interval to checks worker health
240
+ WORKER_CHECK_INTERVAL = 5
241
+
228
242
  end
229
243
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'optparse'
2
- require 'puma/state_file'
3
- require 'puma/const'
4
- require 'puma/detect'
5
- require 'puma/configuration'
4
+ require_relative 'state_file'
5
+ require_relative 'const'
6
+ require_relative 'detect'
7
+ require_relative 'configuration'
6
8
  require 'uri'
7
9
  require 'socket'
8
10
 
@@ -20,6 +22,7 @@ module Puma
20
22
  @control_auth_token = nil
21
23
  @config_file = nil
22
24
  @command = nil
25
+ @environment = ENV['RACK_ENV']
23
26
 
24
27
  @argv = argv.dup
25
28
  @stdout = stdout
@@ -57,6 +60,11 @@ module Puma
57
60
  @config_file = arg
58
61
  end
59
62
 
63
+ o.on "-e", "--environment ENVIRONMENT",
64
+ "The environment to run the Rack app on (default development)" do |arg|
65
+ @environment = arg
66
+ end
67
+
60
68
  o.on_tail("-H", "--help", "Show this message") do
61
69
  @stdout.puts o
62
70
  exit
@@ -74,8 +82,12 @@ module Puma
74
82
  @command = argv.shift
75
83
 
76
84
  unless @config_file == '-'
77
- if @config_file.nil? and File.exist?('config/puma.rb')
78
- @config_file = 'config/puma.rb'
85
+ environment = @environment || 'development'
86
+
87
+ if @config_file.nil?
88
+ @config_file = %W(config/puma/#{environment}.rb config/puma.rb).find do |f|
89
+ File.exist?(f)
90
+ end
79
91
  end
80
92
 
81
93
  if @config_file
@@ -99,7 +111,6 @@ module Puma
99
111
 
100
112
  rescue => e
101
113
  @stdout.puts e.message
102
- @stdout.puts e.backtrace
103
114
  exit 1
104
115
  end
105
116
 
@@ -121,7 +132,7 @@ module Puma
121
132
  @pid = sf.pid
122
133
  elsif @pidfile
123
134
  # get pid from pid_file
124
- @pid = File.open(@pidfile).gets.to_i
135
+ File.open(@pidfile) { |f| @pid = f.read.to_i }
125
136
  end
126
137
  end
127
138
 
@@ -129,7 +140,13 @@ module Puma
129
140
  uri = URI.parse @control_url
130
141
 
131
142
  # create server object by scheme
132
- @server = case uri.scheme
143
+ server = case uri.scheme
144
+ when "ssl"
145
+ require 'openssl'
146
+ OpenSSL::SSL::SSLSocket.new(
147
+ TCPSocket.new(uri.host, uri.port),
148
+ OpenSSL::SSL::SSLContext.new
149
+ ).tap(&:connect)
133
150
  when "tcp"
134
151
  TCPSocket.new uri.host, uri.port
135
152
  when "unix"
@@ -147,9 +164,9 @@ module Puma
147
164
  url = url + "?token=#{@control_auth_token}"
148
165
  end
149
166
 
150
- @server << "GET #{url} HTTP/1.0\r\n\r\n"
167
+ server << "GET #{url} HTTP/1.0\r\n\r\n"
151
168
 
152
- unless data = @server.read
169
+ unless data = server.read
153
170
  raise "Server closed connection before responding"
154
171
  end
155
172
 
@@ -172,8 +189,8 @@ module Puma
172
189
  message "Command #{@command} sent success"
173
190
  message response.last if @command == "stats" || @command == "gc-stats"
174
191
  end
175
-
176
- @server.close
192
+ ensure
193
+ server.close if server && !server.closed?
177
194
  end
178
195
 
179
196
  def send_signal
@@ -204,6 +221,16 @@ module Puma
204
221
  when "phased-restart"
205
222
  Process.kill "SIGUSR1", @pid
206
223
 
224
+ when "status"
225
+ begin
226
+ Process.kill 0, @pid
227
+ puts "Puma is started"
228
+ rescue Errno::ESRCH
229
+ raise "Puma is not running"
230
+ end
231
+
232
+ return
233
+
207
234
  else
208
235
  return
209
236
  end
@@ -232,7 +259,6 @@ module Puma
232
259
 
233
260
  rescue => e
234
261
  message e.message
235
- message e.backtrace
236
262
  exit 1
237
263
  end
238
264
 
@@ -248,6 +274,7 @@ module Puma
248
274
  run_args += ["--control-url", @control_url] if @control_url
249
275
  run_args += ["--control-token", @control_auth_token] if @control_auth_token
250
276
  run_args += ["-C", @config_file] if @config_file
277
+ run_args += ["-e", @environment] if @environment
251
278
 
252
279
  events = Puma::Events.new @stdout, @stderr
253
280