puma 3.8.2 → 4.0.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 (71) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +157 -0
  3. data/README.md +155 -225
  4. data/docs/architecture.md +37 -0
  5. data/{DEPLOYMENT.md → docs/deployment.md} +24 -4
  6. data/docs/images/puma-connection-flow-no-reactor.png +0 -0
  7. data/docs/images/puma-connection-flow.png +0 -0
  8. data/docs/images/puma-general-arch.png +0 -0
  9. data/docs/plugins.md +28 -0
  10. data/docs/restart.md +41 -0
  11. data/docs/signals.md +56 -3
  12. data/docs/systemd.md +130 -37
  13. data/ext/puma_http11/PumaHttp11Service.java +2 -0
  14. data/ext/puma_http11/http11_parser.c +84 -84
  15. data/ext/puma_http11/http11_parser.rl +9 -9
  16. data/ext/puma_http11/mini_ssl.c +51 -9
  17. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +13 -16
  18. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
  19. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +26 -6
  20. data/lib/puma.rb +8 -0
  21. data/lib/puma/app/status.rb +9 -0
  22. data/lib/puma/binder.rb +31 -18
  23. data/lib/puma/cli.rb +22 -7
  24. data/lib/puma/client.rb +67 -18
  25. data/lib/puma/cluster.rb +64 -19
  26. data/lib/puma/commonlogger.rb +2 -0
  27. data/lib/puma/configuration.rb +22 -14
  28. data/lib/puma/const.rb +13 -2
  29. data/lib/puma/control_cli.rb +26 -14
  30. data/lib/puma/convenient.rb +2 -0
  31. data/lib/puma/daemon_ext.rb +2 -0
  32. data/lib/puma/delegation.rb +2 -0
  33. data/lib/puma/detect.rb +2 -0
  34. data/lib/puma/dsl.rb +91 -12
  35. data/lib/puma/events.rb +3 -2
  36. data/lib/puma/io_buffer.rb +3 -6
  37. data/lib/puma/jruby_restart.rb +2 -1
  38. data/lib/puma/launcher.rb +51 -30
  39. data/lib/puma/minissl.rb +79 -28
  40. data/lib/puma/null_io.rb +2 -0
  41. data/lib/puma/plugin.rb +2 -0
  42. data/lib/puma/plugin/tmp_restart.rb +0 -1
  43. data/lib/puma/rack/builder.rb +2 -1
  44. data/lib/puma/reactor.rb +218 -30
  45. data/lib/puma/runner.rb +17 -4
  46. data/lib/puma/server.rb +113 -49
  47. data/lib/puma/single.rb +16 -5
  48. data/lib/puma/state_file.rb +2 -0
  49. data/lib/puma/tcp_logger.rb +2 -0
  50. data/lib/puma/thread_pool.rb +59 -6
  51. data/lib/puma/util.rb +2 -6
  52. data/lib/rack/handler/puma.rb +13 -2
  53. data/tools/jungle/README.md +12 -2
  54. data/tools/jungle/init.d/README.md +2 -0
  55. data/tools/jungle/init.d/puma +7 -7
  56. data/tools/jungle/init.d/run-puma +1 -1
  57. data/tools/jungle/rc.d/README.md +74 -0
  58. data/tools/jungle/rc.d/puma +61 -0
  59. data/tools/jungle/rc.d/puma.conf +10 -0
  60. data/tools/trickletest.rb +1 -1
  61. metadata +25 -87
  62. data/.github/issue_template.md +0 -20
  63. data/Gemfile +0 -12
  64. data/Manifest.txt +0 -78
  65. data/Rakefile +0 -158
  66. data/Release.md +0 -9
  67. data/gemfiles/2.1-Gemfile +0 -12
  68. data/lib/puma/compat.rb +0 -14
  69. data/lib/puma/java_io_buffer.rb +0 -45
  70. data/lib/puma/rack/backports/uri/common_193.rb +0 -33
  71. data/puma.gemspec +0 -52
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,35 @@ module Puma
24
35
  @workers.each { |x| x.term }
25
36
 
26
37
  begin
27
- Process.waitall
38
+ if RUBY_VERSION < '2.6'
39
+ @workers.each do |w|
40
+ begin
41
+ Process.waitpid(w.pid)
42
+ rescue Errno::ECHILD
43
+ # child is already terminated
44
+ end
45
+ end
46
+ else
47
+ # below code is for a bug in Ruby 2.6+, above waitpid call hangs
48
+ t_st = Process.clock_gettime(Process::CLOCK_MONOTONIC)
49
+ pids = @workers.map(&:pid)
50
+ loop do
51
+ pids.reject! do |w_pid|
52
+ begin
53
+ if Process.waitpid(w_pid, Process::WNOHANG)
54
+ log " worker status: #{$?}"
55
+ true
56
+ end
57
+ rescue Errno::ECHILD
58
+ true # child is already terminated
59
+ end
60
+ end
61
+ break if pids.empty?
62
+ sleep 0.5
63
+ end
64
+ t_end = Process.clock_gettime(Process::CLOCK_MONOTONIC)
65
+ log format(" worker shutdown time: %6.2f", t_end - t_st)
66
+ end
28
67
  rescue Interrupt
29
68
  log "! Cancelled waiting for workers"
30
69
  end
@@ -170,7 +209,7 @@ module Puma
170
209
  def check_workers(force=false)
171
210
  return if !force && @next_check && @next_check >= Time.now
172
211
 
173
- @next_check = Time.now + WORKER_CHECK_INTERVAL
212
+ @next_check = Time.now + Const::WORKER_CHECK_INTERVAL
174
213
 
175
214
  any = false
176
215
 
@@ -187,14 +226,9 @@ module Puma
187
226
  # during this loop by giving the kernel time to kill them.
188
227
  sleep 1 if any
189
228
 
190
- while @workers.any?
191
- pid = Process.waitpid(-1, Process::WNOHANG)
192
- break unless pid
229
+ @workers.reject! { |w| Process.waitpid(w.pid, Process::WNOHANG) }
193
230
 
194
- @workers.delete_if { |w| w.pid == pid }
195
- end
196
-
197
- @workers.delete_if(&:dead?)
231
+ @workers.reject!(&:dead?)
198
232
 
199
233
  cull_workers
200
234
  spawn_workers
@@ -224,12 +258,13 @@ module Puma
224
258
  begin
225
259
  @wakeup.write "!" unless @wakeup.closed?
226
260
  rescue SystemCallError, IOError
261
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
227
262
  end
228
263
  end
229
264
 
230
265
  def worker(index, master)
231
- title = "puma: cluster worker #{index}: #{master}"
232
- title << " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
266
+ title = "puma: cluster worker #{index}: #{master}"
267
+ title += " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
233
268
  $0 = title
234
269
 
235
270
  Signal.trap "SIGINT", "IGNORE"
@@ -267,6 +302,7 @@ module Puma
267
302
  begin
268
303
  @worker_write << "b#{Process.pid}\n"
269
304
  rescue SystemCallError, IOError
305
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
270
306
  STDERR.puts "Master seems to have exited, exiting."
271
307
  return
272
308
  end
@@ -275,13 +311,16 @@ module Puma
275
311
  base_payload = "p#{Process.pid}"
276
312
 
277
313
  while true
278
- sleep WORKER_CHECK_INTERVAL
314
+ sleep Const::WORKER_CHECK_INTERVAL
279
315
  begin
280
- b = server.backlog
281
- r = server.running
282
- payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r} }\n!
316
+ b = server.backlog || 0
317
+ r = server.running || 0
318
+ t = server.pool_capacity || 0
319
+ m = server.max_threads || 0
320
+ payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r}, "pool_capacity":#{t}, "max_threads": #{m} }\n!
283
321
  io << payload
284
322
  rescue IOError
323
+ Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
285
324
  break
286
325
  end
287
326
  end
@@ -337,7 +376,7 @@ module Puma
337
376
  def stats
338
377
  old_worker_count = @workers.count { |w| w.phase != @phase }
339
378
  booted_worker_count = @workers.count { |w| w.booted? }
340
- 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(",") + ']'
379
+ 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(",") + ']'
341
380
  %Q!{ "workers": #{@workers.size}, "phase": #{@phase}, "booted_workers": #{booted_worker_count}, "old_workers": #{old_worker_count}, "worker_status": #{worker_status} }!
342
381
  end
343
382
 
@@ -372,7 +411,13 @@ module Puma
372
411
  log "Early termination of worker"
373
412
  exit! 0
374
413
  else
414
+ @launcher.close_binder_listeners
415
+
416
+ stop_workers
375
417
  stop
418
+
419
+ raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
420
+ exit 0 # Clean exit, workers were stopped
376
421
  end
377
422
  end
378
423
  end
@@ -466,7 +511,7 @@ module Puma
466
511
 
467
512
  force_check = false
468
513
 
469
- res = IO.select([read], nil, nil, WORKER_CHECK_INTERVAL)
514
+ res = IO.select([read], nil, nil, Const::WORKER_CHECK_INTERVAL)
470
515
 
471
516
  if res
472
517
  req = read.read_nonblock(1)
@@ -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'
@@ -180,30 +182,32 @@ module Puma
180
182
  :worker_shutdown_timeout => DefaultWorkerShutdownTimeout,
181
183
  :remote_address => :socket,
182
184
  :tag => method(:infer_tag),
183
- :environment => ->{ ENV['RACK_ENV'] || "development" },
185
+ :environment => -> { ENV['RACK_ENV'] || "development" },
184
186
  :rackup => DefaultRackup,
185
187
  :logger => STDOUT,
186
- :persistent_timeout => Const::PERSISTENT_TIMEOUT
188
+ :persistent_timeout => Const::PERSISTENT_TIMEOUT,
189
+ :first_data_timeout => Const::FIRST_DATA_TIMEOUT,
190
+ :raise_exception_on_sigterm => true
187
191
  }
188
192
  end
189
193
 
190
194
  def load
195
+ config_files.each { |config_file| @file_dsl._load_from(config_file) }
196
+
197
+ @options
198
+ end
199
+
200
+ def config_files
191
201
  files = @options.all_of(:config_files)
192
202
 
193
- if files.empty?
194
- imp = %W(config/puma/#{@options[:environment]}.rb config/puma.rb).find { |f|
195
- File.exist?(f)
196
- }
203
+ return [] if files == ['-']
204
+ return files if files.any?
197
205
 
198
- files << imp
199
- elsif files == ["-"]
200
- files = []
206
+ first_default_file = %W(config/puma/#{environment_str}.rb config/puma.rb).find do |f|
207
+ File.exist?(f)
201
208
  end
202
209
 
203
- files.each do |f|
204
- @file_dsl._load_from(f)
205
- end
206
- @options
210
+ [first_default_file]
207
211
  end
208
212
 
209
213
  # Call once all configuration (included from rackup files)
@@ -263,6 +267,10 @@ module Puma
263
267
  @options[:environment]
264
268
  end
265
269
 
270
+ def environment_str
271
+ environment.respond_to?(:call) ? environment.call : environment
272
+ end
273
+
266
274
  def load_plugin(name)
267
275
  @plugins.create name
268
276
  end
@@ -340,7 +348,7 @@ module Puma
340
348
  end
341
349
 
342
350
  if bytes
343
- token = ""
351
+ token = "".dup
344
352
  bytes.each_byte { |b| token << b.to_s(16) }
345
353
  else
346
354
  token = (0..count).to_a.map { rand(255).to_s(16) }.join
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
@@ -53,6 +55,8 @@ module Puma
53
55
  415 => 'Unsupported Media Type',
54
56
  416 => 'Range Not Satisfiable',
55
57
  417 => 'Expectation Failed',
58
+ 418 => 'I\'m A Teapot',
59
+ 421 => 'Misdirected Request',
56
60
  422 => 'Unprocessable Entity',
57
61
  423 => 'Locked',
58
62
  424 => 'Failed Dependency',
@@ -60,6 +64,7 @@ module Puma
60
64
  428 => 'Precondition Required',
61
65
  429 => 'Too Many Requests',
62
66
  431 => 'Request Header Fields Too Large',
67
+ 451 => 'Unavailable For Legal Reasons',
63
68
  500 => 'Internal Server Error',
64
69
  501 => 'Not Implemented',
65
70
  502 => 'Bad Gateway',
@@ -95,8 +100,8 @@ module Puma
95
100
  # too taxing on performance.
96
101
  module Const
97
102
 
98
- PUMA_VERSION = VERSION = "3.8.2".freeze
99
- CODE_NAME = "Sassy Salamander".freeze
103
+ PUMA_VERSION = VERSION = "4.0.0".freeze
104
+ CODE_NAME = "4 Fast 4 Furious".freeze
100
105
  PUMA_SERVER_STRING = ['puma', PUMA_VERSION, CODE_NAME].join(' ').freeze
101
106
 
102
107
  FAST_TRACK_KA_TIMEOUT = 0.2
@@ -220,5 +225,11 @@ module Puma
220
225
  HIJACK_P = "rack.hijack?".freeze
221
226
  HIJACK = "rack.hijack".freeze
222
227
  HIJACK_IO = "rack.hijack_io".freeze
228
+
229
+ EARLY_HINTS = "rack.early_hints".freeze
230
+
231
+ # Mininum interval to checks worker health
232
+ WORKER_CHECK_INTERVAL = 5
233
+
223
234
  end
224
235
  end
@@ -1,15 +1,17 @@
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
 
9
11
  module Puma
10
12
  class ControlCLI
11
13
 
12
- COMMANDS = %w{halt restart phased-restart start stats status stop reload-worker-directory}
14
+ COMMANDS = %w{halt restart phased-restart start stats status stop reload-worker-directory gc gc-stats}
13
15
 
14
16
  def initialize(argv, stdout=STDOUT, stderr=STDERR)
15
17
  @state = nil
@@ -69,6 +71,7 @@ module Puma
69
71
  end
70
72
 
71
73
  opts.order!(argv) { |a| opts.terminate a }
74
+ opts.parse!
72
75
 
73
76
  @command = argv.shift
74
77
 
@@ -128,7 +131,7 @@ module Puma
128
131
  uri = URI.parse @control_url
129
132
 
130
133
  # create server object by scheme
131
- @server = case uri.scheme
134
+ server = case uri.scheme
132
135
  when "tcp"
133
136
  TCPSocket.new uri.host, uri.port
134
137
  when "unix"
@@ -146,9 +149,9 @@ module Puma
146
149
  url = url + "?token=#{@control_auth_token}"
147
150
  end
148
151
 
149
- @server << "GET #{url} HTTP/1.0\r\n\r\n"
152
+ server << "GET #{url} HTTP/1.0\r\n\r\n"
150
153
 
151
- unless data = @server.read
154
+ unless data = server.read
152
155
  raise "Server closed connection before responding"
153
156
  end
154
157
 
@@ -169,10 +172,10 @@ module Puma
169
172
  end
170
173
 
171
174
  message "Command #{@command} sent success"
172
- message response.last if @command == "stats"
175
+ message response.last if @command == "stats" || @command == "gc-stats"
173
176
  end
174
-
175
- @server.close
177
+ ensure
178
+ server.close if server && !server.closed?
176
179
  end
177
180
 
178
181
  def send_signal
@@ -203,8 +206,17 @@ module Puma
203
206
  when "phased-restart"
204
207
  Process.kill "SIGUSR1", @pid
205
208
 
209
+ when "status"
210
+ begin
211
+ Process.kill 0, @pid
212
+ puts "Puma is started"
213
+ rescue Errno::ESRCH
214
+ raise "Puma is not running"
215
+ end
216
+
217
+ return
218
+
206
219
  else
207
- message "Puma is started"
208
220
  return
209
221
  end
210
222
 
@@ -220,7 +232,7 @@ module Puma
220
232
  end
221
233
 
222
234
  def run
223
- start if @command == "start"
235
+ return start if @command == "start"
224
236
 
225
237
  prepare_configuration
226
238
 
@@ -245,7 +257,7 @@ module Puma
245
257
  run_args += ["-S", @state] if @state
246
258
  run_args += ["-q"] if @quiet
247
259
  run_args += ["--pidfile", @pidfile] if @pidfile
248
- run_args += ["--control", @control_url] if @control_url
260
+ run_args += ["--control-url", @control_url] if @control_url
249
261
  run_args += ["--control-token", @control_auth_token] if @control_auth_token
250
262
  run_args += ["-C", @config_file] if @config_file
251
263
 
@@ -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 Process
2
4
 
3
5
  # This overrides the default version because it is broken if it
@@ -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
 
data/lib/puma/dsl.rb CHANGED
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'puma/const'
4
+
1
5
  module Puma
2
6
  # The methods that are available for use inside the config file.
3
7
  # These same methods are used in Puma cli and the rack handler
@@ -55,6 +59,14 @@ module Puma
55
59
  @plugins.clear
56
60
  end
57
61
 
62
+ def set_default_host(host)
63
+ @options[:default_host] = host
64
+ end
65
+
66
+ def default_host
67
+ @options[:default_host] || Configuration::DefaultTCPHost
68
+ end
69
+
58
70
  def inject(&blk)
59
71
  instance_eval(&blk)
60
72
  end
@@ -93,7 +105,12 @@ module Puma
93
105
  end
94
106
 
95
107
  if opts[:no_token]
96
- auth_token = :none
108
+ # We need to use 'none' rather than :none because this value will be
109
+ # passed on to an instance of OptionParser, which doesn't support
110
+ # symbols as option values.
111
+ #
112
+ # See: https://github.com/puma/puma/issues/1193#issuecomment-305995488
113
+ auth_token = 'none'
97
114
  else
98
115
  auth_token = opts[:auth_token]
99
116
  auth_token ||= Configuration.random_token
@@ -110,18 +127,35 @@ module Puma
110
127
  @options[:config_files] << file
111
128
  end
112
129
 
113
- # Bind the server to +url+. tcp:// and unix:// are the only accepted
114
- # protocols.
130
+ # Adds a binding for the server to +url+. tcp://, unix://, and ssl:// are the only accepted
131
+ # protocols. Use query parameters within the url to specify options.
132
+ #
133
+ # @note multiple urls can be bound to, calling `bind` does not overwrite previous bindings.
134
+ #
135
+ # @example Explicitly the socket backlog depth (default is 1024)
136
+ # bind('unix:///var/run/puma.sock?backlog=2048')
115
137
  #
138
+ # @example Set up ssl cert
139
+ # bind('ssl://127.0.0.1:9292?key=key.key&cert=cert.pem')
140
+ #
141
+ # @example Prefer low-latency over higher throughput (via `Socket::TCP_NODELAY`)
142
+ # bind('tcp://0.0.0.0:9292?low_latency=true')
143
+ #
144
+ # @example Set socket permissions
145
+ # bind('unix:///var/run/puma.sock?umask=0111')
116
146
  def bind(url)
117
147
  @options[:binds] ||= []
118
148
  @options[:binds] << url
119
149
  end
120
150
 
151
+ def clear_binds!
152
+ @options[:binds] = []
153
+ end
154
+
121
155
  # Define the TCP port to bind to. Use +bind+ for more advanced options.
122
156
  #
123
157
  def port(port, host=nil)
124
- host ||= Configuration::DefaultTCPHost
158
+ host ||= default_host
125
159
  bind "tcp://#{host}:#{port}"
126
160
  end
127
161
 
@@ -129,7 +163,13 @@ module Puma
129
163
  # them
130
164
  #
131
165
  def persistent_timeout(seconds)
132
- @options[:persistent_timeout] = seconds
166
+ @options[:persistent_timeout] = Integer(seconds)
167
+ end
168
+
169
+ # Define how long the tcp socket stays open, if no data has been received
170
+ #
171
+ def first_data_timeout(seconds)
172
+ @options[:first_data_timeout] = Integer(seconds)
133
173
  end
134
174
 
135
175
  # Work around leaky apps that leave garbage in Thread locals
@@ -146,7 +186,7 @@ module Puma
146
186
  end
147
187
 
148
188
  # When shutting down, drain the accept socket of pending
149
- # connections and proces them. This loops over the accept
189
+ # connections and process them. This loops over the accept
150
190
  # socket until there are no more read events and then stops
151
191
  # looking and waits for the requests to finish.
152
192
  def drain_on_shutdown(which=true)
@@ -231,6 +271,10 @@ module Puma
231
271
  @options[:mode] = :tcp
232
272
  end
233
273
 
274
+ def early_hints(answer=true)
275
+ @options[:early_hints] = answer
276
+ end
277
+
234
278
  # Redirect STDOUT and STDERR to files specified.
235
279
  def stdout_redirect(stdout=nil, stderr=nil, append=false)
236
280
  @options[:redirect_stdout] = stdout
@@ -258,12 +302,15 @@ module Puma
258
302
 
259
303
  def ssl_bind(host, port, opts)
260
304
  verify = opts.fetch(:verify_mode, 'none')
305
+ no_tlsv1 = opts.fetch(:no_tlsv1, 'false')
306
+ ca_additions = "&ca=#{opts[:ca]}" if ['peer', 'force_peer'].include?(verify)
261
307
 
262
308
  if defined?(JRUBY_VERSION)
263
309
  keystore_additions = "keystore=#{opts[:keystore]}&keystore-pass=#{opts[:keystore_pass]}"
264
- bind "ssl://#{host}:#{port}?cert=#{opts[:cert]}&key=#{opts[:key]}&#{keystore_additions}&verify_mode=#{verify}"
310
+ bind "ssl://#{host}:#{port}?cert=#{opts[:cert]}&key=#{opts[:key]}&#{keystore_additions}&verify_mode=#{verify}&no_tlsv1=#{no_tlsv1}#{ca_additions}"
265
311
  else
266
- bind "ssl://#{host}:#{port}?cert=#{opts[:cert]}&key=#{opts[:key]}&verify_mode=#{verify}"
312
+ ssl_cipher_filter = "&ssl_cipher_filter=#{opts[:ssl_cipher_filter]}" if opts[:ssl_cipher_filter]
313
+ bind "ssl://#{host}:#{port}?cert=#{opts[:cert]}&key=#{opts[:key]}#{ssl_cipher_filter}&verify_mode=#{verify}&no_tlsv1=#{no_tlsv1}#{ca_additions}"
267
314
  end
268
315
  end
269
316
 
@@ -338,6 +385,21 @@ module Puma
338
385
 
339
386
  alias_method :after_worker_boot, :after_worker_fork
340
387
 
388
+ # Code to run out-of-band when the worker is idle.
389
+ # These hooks run immediately after a request has finished
390
+ # processing and there are no busy threads on the worker.
391
+ # The worker doesn't accept new requests until this code finishes.
392
+ #
393
+ # This hook is useful for running out-of-band garbage collection
394
+ # or scheduling asynchronous tasks to execute after a response.
395
+ #
396
+ # This can be called multiple times to add hooks.
397
+ #
398
+ def out_of_band(&block)
399
+ @options[:out_of_band] ||= []
400
+ @options[:out_of_band] << block
401
+ end
402
+
341
403
  # The directory to operate out of.
342
404
  def directory(dir)
343
405
  @options[:directory] = dir.to_s
@@ -387,6 +449,16 @@ module Puma
387
449
  @options[:prune_bundler] = answer
388
450
  end
389
451
 
452
+ # In environments where SIGTERM is something expected, instructing
453
+ # puma to shutdown gracefully ( for example in Kubernetes, where
454
+ # rolling restart is guaranteed usually on infrastructure level )
455
+ # SignalException should not be raised for SIGTERM
456
+ #
457
+ # When set to false, if puma process receives SIGTERM, it won't raise SignalException
458
+ def raise_exception_on_sigterm(answer=true)
459
+ @options[:raise_exception_on_sigterm] = answer
460
+ end
461
+
390
462
  # Additional text to display in process listing
391
463
  def tag(string)
392
464
  @options[:tag] = string.to_s
@@ -397,17 +469,24 @@ module Puma
397
469
  # that have not checked in within the given +timeout+.
398
470
  # This mitigates hung processes. Default value is 60 seconds.
399
471
  def worker_timeout(timeout)
400
- @options[:worker_timeout] = timeout
472
+ timeout = Integer(timeout)
473
+ min = Const::WORKER_CHECK_INTERVAL
474
+
475
+ if timeout <= min
476
+ raise "The minimum worker_timeout must be greater than the worker reporting interval (#{min})"
477
+ end
478
+
479
+ @options[:worker_timeout] = Integer(timeout)
401
480
  end
402
481
 
403
482
  # *Cluster mode only* Set the timeout for workers to boot
404
483
  def worker_boot_timeout(timeout)
405
- @options[:worker_boot_timeout] = timeout
484
+ @options[:worker_boot_timeout] = Integer(timeout)
406
485
  end
407
486
 
408
487
  # *Cluster mode only* Set the timeout for worker shutdown
409
488
  def worker_shutdown_timeout(timeout)
410
- @options[:worker_shutdown_timeout] = timeout
489
+ @options[:worker_shutdown_timeout] = Integer(timeout)
411
490
  end
412
491
 
413
492
  # When set to true (the default), workers accept all requests
@@ -466,7 +545,7 @@ module Puma
466
545
  when Hash
467
546
  if hdr = val[:header]
468
547
  @options[:remote_address] = :header
469
- @options[:remote_address_header] = "HTTP_" + hdr.upcase.gsub("-", "_")
548
+ @options[:remote_address_header] = "HTTP_" + hdr.upcase.tr("-", "_")
470
549
  else
471
550
  raise "Invalid value for set_remote_address - #{val.inspect}"
472
551
  end