puma 3.11.4 → 4.2.0

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +130 -1
  3. data/README.md +100 -44
  4. data/docs/architecture.md +1 -0
  5. data/docs/deployment.md +24 -4
  6. data/docs/restart.md +4 -2
  7. data/docs/systemd.md +27 -9
  8. data/ext/puma_http11/PumaHttp11Service.java +2 -0
  9. data/ext/puma_http11/extconf.rb +8 -0
  10. data/ext/puma_http11/http11_parser.c +37 -62
  11. data/ext/puma_http11/http11_parser_common.rl +3 -3
  12. data/ext/puma_http11/mini_ssl.c +96 -5
  13. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
  14. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +21 -4
  15. data/lib/puma/accept_nonblock.rb +7 -1
  16. data/lib/puma/app/status.rb +35 -29
  17. data/lib/puma/binder.rb +47 -11
  18. data/lib/puma/cli.rb +21 -7
  19. data/lib/puma/client.rb +227 -191
  20. data/lib/puma/cluster.rb +70 -31
  21. data/lib/puma/commonlogger.rb +2 -0
  22. data/lib/puma/configuration.rb +6 -3
  23. data/lib/puma/const.rb +24 -18
  24. data/lib/puma/control_cli.rb +33 -14
  25. data/lib/puma/convenient.rb +2 -0
  26. data/lib/puma/delegation.rb +2 -0
  27. data/lib/puma/detect.rb +2 -0
  28. data/lib/puma/dsl.rb +308 -76
  29. data/lib/puma/events.rb +6 -1
  30. data/lib/puma/io_buffer.rb +3 -6
  31. data/lib/puma/jruby_restart.rb +2 -0
  32. data/lib/puma/launcher.rb +102 -55
  33. data/lib/puma/minissl.rb +41 -19
  34. data/lib/puma/null_io.rb +2 -0
  35. data/lib/puma/plugin/tmp_restart.rb +2 -0
  36. data/lib/puma/plugin.rb +7 -2
  37. data/lib/puma/rack/builder.rb +4 -1
  38. data/lib/puma/rack/urlmap.rb +2 -0
  39. data/lib/puma/rack_default.rb +2 -0
  40. data/lib/puma/reactor.rb +220 -34
  41. data/lib/puma/runner.rb +14 -4
  42. data/lib/puma/server.rb +82 -40
  43. data/lib/puma/single.rb +15 -3
  44. data/lib/puma/state_file.rb +2 -0
  45. data/lib/puma/tcp_logger.rb +2 -0
  46. data/lib/puma/thread_pool.rb +59 -36
  47. data/lib/puma/util.rb +2 -6
  48. data/lib/puma.rb +8 -0
  49. data/lib/rack/handler/puma.rb +6 -3
  50. data/tools/docker/Dockerfile +16 -0
  51. data/tools/jungle/init.d/puma +6 -6
  52. data/tools/trickletest.rb +0 -1
  53. metadata +22 -10
  54. data/lib/puma/compat.rb +0 -14
  55. data/lib/puma/daemon_ext.rb +0 -31
  56. data/lib/puma/java_io_buffer.rb +0 -45
  57. 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'
@@ -5,9 +7,18 @@ require 'puma/plugin'
5
7
  require 'time'
6
8
 
7
9
  module Puma
10
+ # This class is instantiated by the `Puma::Launcher` and used
11
+ # to boot and serve a Ruby application when puma "workers" are needed
12
+ # i.e. when using multi-processes. For example `$ puma -w 5`
13
+ #
14
+ # At the core of this class is running an instance of `Puma::Server` which
15
+ # gets created via the `start_server` method from the `Puma::Runner` class
16
+ # that this inherits from.
17
+ #
18
+ # An instance of this class will spawn the number of processes passed in
19
+ # via the `spawn_workers` method call. Each worker will have it's own
20
+ # instance of a `Puma::Server`.
8
21
  class Cluster < Runner
9
- WORKER_CHECK_INTERVAL = 5
10
-
11
22
  def initialize(cli, events)
12
23
  super cli, events
13
24
 
@@ -24,7 +35,11 @@ module Puma
24
35
  @workers.each { |x| x.term }
25
36
 
26
37
  begin
27
- @workers.each { |w| Process.waitpid(w.pid) }
38
+ loop do
39
+ wait_workers
40
+ break if @workers.empty?
41
+ sleep 0.2
42
+ end
28
43
  rescue Interrupt
29
44
  log "! Cancelled waiting for workers"
30
45
  end
@@ -56,12 +71,13 @@ module Puma
56
71
  @signal = "TERM"
57
72
  @options = options
58
73
  @first_term_sent = nil
74
+ @started_at = Time.now
59
75
  @last_checkin = Time.now
60
76
  @last_status = '{}'
61
- @dead = false
77
+ @term = false
62
78
  end
63
79
 
64
- attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status
80
+ attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status, :started_at
65
81
 
66
82
  def booted?
67
83
  @stage == :booted
@@ -72,12 +88,8 @@ module Puma
72
88
  @stage = :booted
73
89
  end
74
90
 
75
- def dead?
76
- @dead
77
- end
78
-
79
- def dead!
80
- @dead = true
91
+ def term?
92
+ @term
81
93
  end
82
94
 
83
95
  def ping!(status)
@@ -94,9 +106,9 @@ module Puma
94
106
  if @first_term_sent && (Time.now - @first_term_sent) > @options[:worker_shutdown_timeout]
95
107
  @signal = "KILL"
96
108
  else
109
+ @term ||= true
97
110
  @first_term_sent ||= Time.now
98
111
  end
99
-
100
112
  Process.kill @signal, @pid
101
113
  rescue Errno::ESRCH
102
114
  end
@@ -170,7 +182,7 @@ module Puma
170
182
  def check_workers(force=false)
171
183
  return if !force && @next_check && @next_check >= Time.now
172
184
 
173
- @next_check = Time.now + WORKER_CHECK_INTERVAL
185
+ @next_check = Time.now + Const::WORKER_CHECK_INTERVAL
174
186
 
175
187
  any = false
176
188
 
@@ -187,15 +199,7 @@ module Puma
187
199
  # during this loop by giving the kernel time to kill them.
188
200
  sleep 1 if any
189
201
 
190
- while @workers.any?
191
- pid = Process.waitpid(-1, Process::WNOHANG)
192
- break unless pid
193
-
194
- @workers.delete_if { |w| w.pid == pid }
195
- end
196
-
197
- @workers.delete_if(&:dead?)
198
-
202
+ wait_workers
199
203
  cull_workers
200
204
  spawn_workers
201
205
 
@@ -212,8 +216,10 @@ module Puma
212
216
  log "- Stopping #{w.pid} for phased upgrade..."
213
217
  end
214
218
 
215
- w.term
216
- log "- #{w.signal} sent to #{w.pid}..."
219
+ unless w.term?
220
+ w.term
221
+ log "- #{w.signal} sent to #{w.pid}..."
222
+ end
217
223
  end
218
224
  end
219
225
  end
@@ -240,6 +246,7 @@ module Puma
240
246
  @suicide_pipe.close
241
247
 
242
248
  Thread.new do
249
+ Puma.set_thread_name "worker check pipe"
243
250
  IO.select [@check_pipe]
244
251
  log "! Detected parent died, dying"
245
252
  exit! 1
@@ -262,6 +269,7 @@ module Puma
262
269
  server = start_server
263
270
 
264
271
  Signal.trap "SIGTERM" do
272
+ @worker_write << "e#{Process.pid}\n" rescue nil
265
273
  server.stop
266
274
  end
267
275
 
@@ -274,14 +282,17 @@ module Puma
274
282
  end
275
283
 
276
284
  Thread.new(@worker_write) do |io|
285
+ Puma.set_thread_name "stat payload"
277
286
  base_payload = "p#{Process.pid}"
278
287
 
279
288
  while true
280
- sleep WORKER_CHECK_INTERVAL
289
+ sleep Const::WORKER_CHECK_INTERVAL
281
290
  begin
282
291
  b = server.backlog || 0
283
292
  r = server.running || 0
284
- payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r} }\n!
293
+ t = server.pool_capacity || 0
294
+ m = server.max_threads || 0
295
+ payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads": #{m} }\n!
285
296
  io << payload
286
297
  rescue IOError
287
298
  Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
@@ -337,11 +348,13 @@ module Puma
337
348
  Dir.chdir dir
338
349
  end
339
350
 
351
+ # Inside of a child process, this will return all zeroes, as @workers is only populated in
352
+ # the master process.
340
353
  def stats
341
354
  old_worker_count = @workers.count { |w| w.phase != @phase }
342
355
  booted_worker_count = @workers.count { |w| w.booted? }
343
- 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(",") + ']'
344
- %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} }!
345
358
  end
346
359
 
347
360
  def preload?
@@ -375,10 +388,13 @@ module Puma
375
388
  log "Early termination of worker"
376
389
  exit! 0
377
390
  else
391
+ @launcher.close_binder_listeners
392
+
378
393
  stop_workers
379
394
  stop
380
395
 
381
- raise SignalException, "SIGTERM"
396
+ raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
397
+ exit 0 # Clean exit, workers were stopped
382
398
  end
383
399
  end
384
400
  end
@@ -472,7 +488,7 @@ module Puma
472
488
 
473
489
  force_check = false
474
490
 
475
- res = IO.select([read], nil, nil, WORKER_CHECK_INTERVAL)
491
+ res = IO.select([read], nil, nil, Const::WORKER_CHECK_INTERVAL)
476
492
 
477
493
  if res
478
494
  req = read.read_nonblock(1)
@@ -488,8 +504,11 @@ module Puma
488
504
  w.boot!
489
505
  log "- Worker #{w.index} (pid: #{pid}) booted, phase: #{w.phase}"
490
506
  force_check = true
507
+ when "e"
508
+ # external term, see worker method, Signal.trap "SIGTERM"
509
+ w.instance_variable_set :@term, true
491
510
  when "t"
492
- w.dead!
511
+ w.term unless w.term?
493
512
  force_check = true
494
513
  when "p"
495
514
  w.ping!(result.sub(/^\d+/,'').chomp)
@@ -510,6 +529,26 @@ module Puma
510
529
  @suicide_pipe.close
511
530
  read.close
512
531
  @wakeup.close
532
+ @launcher.close_binder_unix_paths
533
+ end
534
+ end
535
+
536
+ private
537
+
538
+ # loops thru @workers, removing workers that exited, and calling
539
+ # `#term` if needed
540
+ def wait_workers
541
+ @workers.reject! do |w|
542
+ begin
543
+ if Process.wait(w.pid, Process::WNOHANG)
544
+ true
545
+ else
546
+ w.term if w.term?
547
+ nil
548
+ end
549
+ rescue Errno::ECHILD
550
+ true # child is already terminated
551
+ end
513
552
  end
514
553
  end
515
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.11.4".freeze
102
- CODE_NAME = "Love Song".freeze
103
+ PUMA_VERSION = VERSION = "4.2.0".freeze
104
+ CODE_NAME = "Distant Airhorns".freeze
103
105
  PUMA_SERVER_STRING = ['puma', PUMA_VERSION, CODE_NAME].join(' ').freeze
104
106
 
105
107
  FAST_TRACK_KA_TIMEOUT = 0.2
@@ -120,27 +122,24 @@ module Puma
120
122
  REQUEST_URI= 'REQUEST_URI'.freeze
121
123
  REQUEST_PATH = 'REQUEST_PATH'.freeze
122
124
  QUERY_STRING = 'QUERY_STRING'.freeze
125
+ CONTENT_LENGTH = "CONTENT_LENGTH".freeze
123
126
 
124
127
  PATH_INFO = 'PATH_INFO'.freeze
125
128
 
126
129
  PUMA_TMP_BASE = "puma".freeze
127
130
 
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
131
+ ERROR_RESPONSE = {
132
+ # Indicate that we couldn't parse the request
133
+ 400 => "HTTP/1.1 400 Bad Request\r\n\r\n".freeze,
134
+ # The standard empty 404 response for bad requests. Use Error4040Handler for custom stuff.
135
+ 404 => "HTTP/1.1 404 Not Found\r\nConnection: close\r\nServer: Puma #{PUMA_VERSION}\r\n\r\nNOT FOUND".freeze,
136
+ # The standard empty 408 response for requests that timed out.
137
+ 408 => "HTTP/1.1 408 Request Timeout\r\nConnection: close\r\nServer: Puma #{PUMA_VERSION}\r\n\r\n".freeze,
138
+ # Indicate that there was an internal error, obviously.
139
+ 500 => "HTTP/1.1 500 Internal Server Error\r\n\r\n".freeze,
140
+ # A common header for indicating the server is too busy. Not used yet.
141
+ 503 => "HTTP/1.1 503 Service Unavailable\r\n\r\nBUSY".freeze
142
+ }
144
143
 
145
144
  # The basic max request size we'll try to read.
146
145
  CHUNK_SIZE = 16 * 1024
@@ -158,6 +157,9 @@ module Puma
158
157
  LINE_END = "\r\n".freeze
159
158
  REMOTE_ADDR = "REMOTE_ADDR".freeze
160
159
  HTTP_X_FORWARDED_FOR = "HTTP_X_FORWARDED_FOR".freeze
160
+ HTTP_X_FORWARDED_SSL = "HTTP_X_FORWARDED_SSL".freeze
161
+ HTTP_X_FORWARDED_SCHEME = "HTTP_X_FORWARDED_SCHEME".freeze
162
+ HTTP_X_FORWARDED_PROTO = "HTTP_X_FORWARDED_PROTO".freeze
161
163
 
162
164
  SERVER_NAME = "SERVER_NAME".freeze
163
165
  SERVER_PORT = "SERVER_PORT".freeze
@@ -225,5 +227,9 @@ module Puma
225
227
  HIJACK_IO = "rack.hijack_io".freeze
226
228
 
227
229
  EARLY_HINTS = "rack.early_hints".freeze
230
+
231
+ # Mininum interval to checks worker health
232
+ WORKER_CHECK_INTERVAL = 5
233
+
228
234
  end
229
235
  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'] || "development"
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,10 @@ 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
+ if @config_file.nil?
86
+ @config_file = %W(config/puma/#{@environment}.rb config/puma.rb).find do |f|
87
+ File.exist?(f)
88
+ end
79
89
  end
80
90
 
81
91
  if @config_file
@@ -99,7 +109,6 @@ module Puma
99
109
 
100
110
  rescue => e
101
111
  @stdout.puts e.message
102
- @stdout.puts e.backtrace
103
112
  exit 1
104
113
  end
105
114
 
@@ -129,7 +138,7 @@ module Puma
129
138
  uri = URI.parse @control_url
130
139
 
131
140
  # create server object by scheme
132
- @server = case uri.scheme
141
+ server = case uri.scheme
133
142
  when "tcp"
134
143
  TCPSocket.new uri.host, uri.port
135
144
  when "unix"
@@ -147,9 +156,9 @@ module Puma
147
156
  url = url + "?token=#{@control_auth_token}"
148
157
  end
149
158
 
150
- @server << "GET #{url} HTTP/1.0\r\n\r\n"
159
+ server << "GET #{url} HTTP/1.0\r\n\r\n"
151
160
 
152
- unless data = @server.read
161
+ unless data = server.read
153
162
  raise "Server closed connection before responding"
154
163
  end
155
164
 
@@ -172,8 +181,8 @@ module Puma
172
181
  message "Command #{@command} sent success"
173
182
  message response.last if @command == "stats" || @command == "gc-stats"
174
183
  end
175
-
176
- @server.close
184
+ ensure
185
+ server.close if server && !server.closed?
177
186
  end
178
187
 
179
188
  def send_signal
@@ -204,6 +213,16 @@ module Puma
204
213
  when "phased-restart"
205
214
  Process.kill "SIGUSR1", @pid
206
215
 
216
+ when "status"
217
+ begin
218
+ Process.kill 0, @pid
219
+ puts "Puma is started"
220
+ rescue Errno::ESRCH
221
+ raise "Puma is not running"
222
+ end
223
+
224
+ return
225
+
207
226
  else
208
227
  return
209
228
  end
@@ -220,7 +239,7 @@ module Puma
220
239
  end
221
240
 
222
241
  def run
223
- start if @command == "start"
242
+ return start if @command == "start"
224
243
 
225
244
  prepare_configuration
226
245
 
@@ -232,7 +251,6 @@ module Puma
232
251
 
233
252
  rescue => e
234
253
  message e.message
235
- message e.backtrace
236
254
  exit 1
237
255
  end
238
256
 
@@ -248,6 +266,7 @@ module Puma
248
266
  run_args += ["--control-url", @control_url] if @control_url
249
267
  run_args += ["--control-token", @control_auth_token] if @control_auth_token
250
268
  run_args += ["-C", @config_file] if @config_file
269
+ run_args += ["-e", @environment] if @environment
251
270
 
252
271
  events = Puma::Events.new @stdout, @stderr
253
272
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'puma/launcher'
2
4
  require 'puma/configuration'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Puma
2
4
  module Delegation
3
5
  def forward(what, who)
data/lib/puma/detect.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Puma
2
4
  IS_JRUBY = defined?(JRUBY_VERSION)
3
5