puma 3.11.3 → 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 (54) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +61 -0
  3. data/README.md +41 -11
  4. data/docs/architecture.md +2 -1
  5. data/docs/deployment.md +24 -4
  6. data/docs/restart.md +5 -3
  7. data/docs/systemd.md +37 -9
  8. data/ext/puma_http11/PumaHttp11Service.java +2 -0
  9. data/ext/puma_http11/mini_ssl.c +42 -5
  10. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +72 -0
  11. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +17 -4
  12. data/lib/puma.rb +8 -0
  13. data/lib/puma/app/status.rb +3 -2
  14. data/lib/puma/binder.rb +22 -10
  15. data/lib/puma/cli.rb +18 -7
  16. data/lib/puma/client.rb +54 -22
  17. data/lib/puma/cluster.rb +54 -15
  18. data/lib/puma/commonlogger.rb +2 -0
  19. data/lib/puma/configuration.rb +4 -1
  20. data/lib/puma/const.rb +8 -2
  21. data/lib/puma/control_cli.rb +23 -11
  22. data/lib/puma/convenient.rb +2 -0
  23. data/lib/puma/daemon_ext.rb +2 -0
  24. data/lib/puma/delegation.rb +2 -0
  25. data/lib/puma/detect.rb +2 -0
  26. data/lib/puma/dsl.rb +63 -11
  27. data/lib/puma/events.rb +2 -0
  28. data/lib/puma/io_buffer.rb +3 -6
  29. data/lib/puma/jruby_restart.rb +2 -0
  30. data/lib/puma/launcher.rb +15 -13
  31. data/lib/puma/minissl.rb +20 -4
  32. data/lib/puma/null_io.rb +2 -0
  33. data/lib/puma/plugin.rb +2 -0
  34. data/lib/puma/rack/builder.rb +2 -1
  35. data/lib/puma/reactor.rb +215 -30
  36. data/lib/puma/runner.rb +11 -2
  37. data/lib/puma/server.rb +63 -26
  38. data/lib/puma/single.rb +14 -3
  39. data/lib/puma/state_file.rb +2 -0
  40. data/lib/puma/tcp_logger.rb +2 -0
  41. data/lib/puma/thread_pool.rb +50 -5
  42. data/lib/puma/util.rb +2 -6
  43. data/lib/rack/handler/puma.rb +4 -0
  44. data/tools/jungle/README.md +10 -4
  45. data/tools/jungle/init.d/README.md +2 -0
  46. data/tools/jungle/init.d/puma +7 -7
  47. data/tools/jungle/init.d/run-puma +1 -1
  48. data/tools/jungle/rc.d/README.md +74 -0
  49. data/tools/jungle/rc.d/puma +61 -0
  50. data/tools/jungle/rc.d/puma.conf +10 -0
  51. metadata +23 -9
  52. data/lib/puma/compat.rb +0 -14
  53. data/lib/puma/java_io_buffer.rb +0 -45
  54. 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,35 @@ module Puma
24
35
  @workers.each { |x| x.term }
25
36
 
26
37
  begin
27
- @workers.each { |w| Process.waitpid(w.pid) }
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
@@ -277,11 +311,13 @@ module Puma
277
311
  base_payload = "p#{Process.pid}"
278
312
 
279
313
  while true
280
- sleep WORKER_CHECK_INTERVAL
314
+ sleep Const::WORKER_CHECK_INTERVAL
281
315
  begin
282
316
  b = server.backlog || 0
283
317
  r = server.running || 0
284
- payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r} }\n!
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!
285
321
  io << payload
286
322
  rescue IOError
287
323
  Thread.current.purge_interrupt_queue if Thread.current.respond_to? :purge_interrupt_queue
@@ -375,10 +411,13 @@ module Puma
375
411
  log "Early termination of worker"
376
412
  exit! 0
377
413
  else
414
+ @launcher.close_binder_listeners
415
+
378
416
  stop_workers
379
417
  stop
380
418
 
381
- raise SignalException, "SIGTERM"
419
+ raise(SignalException, "SIGTERM") if @options[:raise_exception_on_sigterm]
420
+ exit 0 # Clean exit, workers were stopped
382
421
  end
383
422
  end
384
423
  end
@@ -472,7 +511,7 @@ module Puma
472
511
 
473
512
  force_check = false
474
513
 
475
- res = IO.select([read], nil, nil, WORKER_CHECK_INTERVAL)
514
+ res = IO.select([read], nil, nil, Const::WORKER_CHECK_INTERVAL)
476
515
 
477
516
  if res
478
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'
@@ -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.3".freeze
102
- CODE_NAME = "Love Song".freeze
103
+ PUMA_VERSION = VERSION = "4.0.0".freeze
104
+ CODE_NAME = "4 Fast 4 Furious".freeze
103
105
  PUMA_SERVER_STRING = ['puma', PUMA_VERSION, CODE_NAME].join(' ').freeze
104
106
 
105
107
  FAST_TRACK_KA_TIMEOUT = 0.2
@@ -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
 
@@ -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
 
@@ -171,8 +174,8 @@ module Puma
171
174
  message "Command #{@command} sent success"
172
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
 
@@ -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
@@ -138,7 +155,7 @@ module Puma
138
155
  # Define the TCP port to bind to. Use +bind+ for more advanced options.
139
156
  #
140
157
  def port(port, host=nil)
141
- host ||= Configuration::DefaultTCPHost
158
+ host ||= default_host
142
159
  bind "tcp://#{host}:#{port}"
143
160
  end
144
161
 
@@ -146,13 +163,13 @@ module Puma
146
163
  # them
147
164
  #
148
165
  def persistent_timeout(seconds)
149
- @options[:persistent_timeout] = seconds
166
+ @options[:persistent_timeout] = Integer(seconds)
150
167
  end
151
168
 
152
169
  # Define how long the tcp socket stays open, if no data has been received
153
170
  #
154
171
  def first_data_timeout(seconds)
155
- @options[:first_data_timeout] = seconds
172
+ @options[:first_data_timeout] = Integer(seconds)
156
173
  end
157
174
 
158
175
  # Work around leaky apps that leave garbage in Thread locals
@@ -169,7 +186,7 @@ module Puma
169
186
  end
170
187
 
171
188
  # When shutting down, drain the accept socket of pending
172
- # connections and proces them. This loops over the accept
189
+ # connections and process them. This loops over the accept
173
190
  # socket until there are no more read events and then stops
174
191
  # looking and waits for the requests to finish.
175
192
  def drain_on_shutdown(which=true)
@@ -285,12 +302,15 @@ module Puma
285
302
 
286
303
  def ssl_bind(host, port, opts)
287
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)
288
307
 
289
308
  if defined?(JRUBY_VERSION)
290
309
  keystore_additions = "keystore=#{opts[:keystore]}&keystore-pass=#{opts[:keystore_pass]}"
291
- 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}"
292
311
  else
293
- 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}"
294
314
  end
295
315
  end
296
316
 
@@ -365,6 +385,21 @@ module Puma
365
385
 
366
386
  alias_method :after_worker_boot, :after_worker_fork
367
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
+
368
403
  # The directory to operate out of.
369
404
  def directory(dir)
370
405
  @options[:directory] = dir.to_s
@@ -414,6 +449,16 @@ module Puma
414
449
  @options[:prune_bundler] = answer
415
450
  end
416
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
+
417
462
  # Additional text to display in process listing
418
463
  def tag(string)
419
464
  @options[:tag] = string.to_s
@@ -424,17 +469,24 @@ module Puma
424
469
  # that have not checked in within the given +timeout+.
425
470
  # This mitigates hung processes. Default value is 60 seconds.
426
471
  def worker_timeout(timeout)
427
- @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)
428
480
  end
429
481
 
430
482
  # *Cluster mode only* Set the timeout for workers to boot
431
483
  def worker_boot_timeout(timeout)
432
- @options[:worker_boot_timeout] = timeout
484
+ @options[:worker_boot_timeout] = Integer(timeout)
433
485
  end
434
486
 
435
487
  # *Cluster mode only* Set the timeout for worker shutdown
436
488
  def worker_shutdown_timeout(timeout)
437
- @options[:worker_shutdown_timeout] = timeout
489
+ @options[:worker_shutdown_timeout] = Integer(timeout)
438
490
  end
439
491
 
440
492
  # When set to true (the default), workers accept all requests
@@ -493,7 +545,7 @@ module Puma
493
545
  when Hash
494
546
  if hdr = val[:header]
495
547
  @options[:remote_address] = :header
496
- @options[:remote_address_header] = "HTTP_" + hdr.upcase.gsub("-", "_")
548
+ @options[:remote_address_header] = "HTTP_" + hdr.upcase.tr("-", "_")
497
549
  else
498
550
  raise "Invalid value for set_remote_address - #{val.inspect}"
499
551
  end