puma 4.3.6 → 5.0.0.beta1

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +60 -12
  3. data/LICENSE +23 -20
  4. data/README.md +17 -11
  5. data/docs/deployment.md +3 -1
  6. data/docs/fork_worker.md +31 -0
  7. data/docs/jungle/README.md +13 -0
  8. data/{tools → docs}/jungle/rc.d/README.md +0 -0
  9. data/{tools → docs}/jungle/rc.d/puma +0 -0
  10. data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
  11. data/{tools → docs}/jungle/upstart/README.md +0 -0
  12. data/{tools → docs}/jungle/upstart/puma-manager.conf +0 -0
  13. data/{tools → docs}/jungle/upstart/puma.conf +0 -0
  14. data/docs/signals.md +1 -0
  15. data/docs/systemd.md +1 -63
  16. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  17. data/ext/puma_http11/extconf.rb +4 -3
  18. data/ext/puma_http11/http11_parser.c +1 -3
  19. data/ext/puma_http11/http11_parser.rl +1 -3
  20. data/ext/puma_http11/org/jruby/puma/Http11.java +3 -3
  21. data/ext/puma_http11/puma_http11.c +2 -38
  22. data/lib/puma.rb +4 -0
  23. data/lib/puma/app/status.rb +16 -5
  24. data/lib/puma/binder.rb +62 -60
  25. data/lib/puma/cli.rb +7 -15
  26. data/lib/puma/client.rb +30 -15
  27. data/lib/puma/cluster.rb +179 -74
  28. data/lib/puma/configuration.rb +30 -42
  29. data/lib/puma/const.rb +2 -3
  30. data/lib/puma/control_cli.rb +27 -17
  31. data/lib/puma/detect.rb +8 -0
  32. data/lib/puma/dsl.rb +70 -34
  33. data/lib/puma/io_buffer.rb +9 -2
  34. data/lib/puma/jruby_restart.rb +0 -58
  35. data/lib/puma/launcher.rb +41 -29
  36. data/lib/puma/minissl.rb +13 -8
  37. data/lib/puma/null_io.rb +1 -1
  38. data/lib/puma/plugin.rb +1 -10
  39. data/lib/puma/rack/builder.rb +0 -4
  40. data/lib/puma/reactor.rb +6 -1
  41. data/lib/puma/runner.rb +5 -34
  42. data/lib/puma/server.rb +62 -177
  43. data/lib/puma/single.rb +7 -64
  44. data/lib/puma/state_file.rb +5 -2
  45. data/lib/puma/thread_pool.rb +85 -47
  46. data/lib/rack/handler/puma.rb +1 -3
  47. data/tools/{docker/Dockerfile → Dockerfile} +0 -0
  48. metadata +19 -23
  49. data/docs/tcp_mode.md +0 -96
  50. data/ext/puma_http11/io_buffer.c +0 -155
  51. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  52. data/lib/puma/tcp_logger.rb +0 -41
  53. data/tools/jungle/README.md +0 -19
  54. data/tools/jungle/init.d/README.md +0 -61
  55. data/tools/jungle/init.d/puma +0 -421
  56. data/tools/jungle/init.d/run-puma +0 -18
@@ -54,9 +54,7 @@ module Puma
54
54
  attr_reader :user_options, :file_options, :default_options
55
55
 
56
56
  def [](key)
57
- return user_options[key] if user_options.key?(key)
58
- return file_options[key] if file_options.key?(key)
59
- return default_options[key] if default_options.key?(key)
57
+ fetch(key)
60
58
  end
61
59
 
62
60
  def []=(key, value)
@@ -64,7 +62,11 @@ module Puma
64
62
  end
65
63
 
66
64
  def fetch(key, default_value = nil)
67
- self[key] || default_value
65
+ return user_options[key] if user_options.key?(key)
66
+ return file_options[key] if file_options.key?(key)
67
+ return default_options[key] if default_options.key?(key)
68
+
69
+ default_value
68
70
  end
69
71
 
70
72
  def all_of(key)
@@ -106,7 +108,7 @@ module Puma
106
108
  #
107
109
  # It also handles loading plugins.
108
110
  #
109
- # > Note: `:port` and `:host` are not valid keys. By they time they make it to the
111
+ # > Note: `:port` and `:host` are not valid keys. By the time they make it to the
110
112
  # configuration options they are expected to be incorporated into a `:binds` key.
111
113
  # Under the hood the DSL maps `port` and `host` calls to `:binds`
112
114
  #
@@ -137,6 +139,10 @@ module Puma
137
139
  @file_dsl = DSL.new(@options.file_options, self)
138
140
  @default_dsl = DSL.new(@options.default_options, self)
139
141
 
142
+ if !@options[:prune_bundler]
143
+ default_options[:preload_app] = (@options[:workers] > 1) && Puma.forkable?
144
+ end
145
+
140
146
  if block
141
147
  configure(&block)
142
148
  end
@@ -167,22 +173,25 @@ module Puma
167
173
  self
168
174
  end
169
175
 
176
+ def default_max_threads
177
+ Puma.mri? ? 5 : 16
178
+ end
179
+
170
180
  def puma_default_options
171
181
  {
172
- :min_threads => 0,
173
- :max_threads => 16,
182
+ :min_threads => Integer(ENV['PUMA_MIN_THREADS'] || ENV['MIN_THREADS'] || 0),
183
+ :max_threads => Integer(ENV['PUMA_MAX_THREADS'] || ENV['MAX_THREADS'] || default_max_threads),
174
184
  :log_requests => false,
175
185
  :debug => false,
176
186
  :binds => ["tcp://#{DefaultTCPHost}:#{DefaultTCPPort}"],
177
- :workers => 0,
178
- :daemon => false,
187
+ :workers => Integer(ENV['WEB_CONCURRENCY'] || 0),
179
188
  :mode => :http,
180
189
  :worker_timeout => DefaultWorkerTimeout,
181
190
  :worker_boot_timeout => DefaultWorkerTimeout,
182
191
  :worker_shutdown_timeout => DefaultWorkerShutdownTimeout,
183
192
  :remote_address => :socket,
184
193
  :tag => method(:infer_tag),
185
- :environment => -> { ENV['RACK_ENV'] || "development" },
194
+ :environment => -> { ENV['RACK_ENV'] || ENV['RAILS_ENV'] || "development" },
186
195
  :rackup => DefaultRackup,
187
196
  :logger => STDOUT,
188
197
  :persistent_timeout => Const::PERSISTENT_TIMEOUT,
@@ -245,14 +254,6 @@ module Puma
245
254
  def app
246
255
  found = options[:app] || load_rackup
247
256
 
248
- if @options[:mode] == :tcp
249
- require 'puma/tcp_logger'
250
-
251
- logger = @options[:logger]
252
- quiet = !@options[:log_requests]
253
- return TCPLogger.new(logger, found, quiet)
254
- end
255
-
256
257
  if @options[:log_requests]
257
258
  require 'puma/commonlogger'
258
259
  logger = @options[:logger]
@@ -275,8 +276,15 @@ module Puma
275
276
  @plugins.create name
276
277
  end
277
278
 
278
- def run_hooks(key, arg)
279
- @options.all_of(key).each { |b| b.call arg }
279
+ def run_hooks(key, arg, events)
280
+ @options.all_of(key).each do |b|
281
+ begin
282
+ b.call arg
283
+ rescue => e
284
+ events.log "WARNING hook #{key} failed with exception (#{e.class}) #{e.message}"
285
+ events.debug e.backtrace.join("\n")
286
+ end
287
+ end
280
288
  end
281
289
 
282
290
  def self.temp_path
@@ -332,29 +340,9 @@ module Puma
332
340
  end
333
341
 
334
342
  def self.random_token
335
- begin
336
- require 'openssl'
337
- rescue LoadError
338
- end
339
-
340
- count = 16
341
-
342
- bytes = nil
343
-
344
- if defined? OpenSSL::Random
345
- bytes = OpenSSL::Random.random_bytes(count)
346
- elsif File.exist?("/dev/urandom")
347
- File.open('/dev/urandom') { |f| bytes = f.read(count) }
348
- end
349
-
350
- if bytes
351
- token = "".dup
352
- bytes.each_byte { |b| token << b.to_s(16) }
353
- else
354
- token = (0..count).to_a.map { rand(255).to_s(16) }.join
355
- end
343
+ require 'securerandom' unless defined?(SecureRandom)
356
344
 
357
- return token
345
+ SecureRandom.hex(16)
358
346
  end
359
347
  end
360
348
  end
@@ -100,8 +100,8 @@ module Puma
100
100
  # too taxing on performance.
101
101
  module Const
102
102
 
103
- PUMA_VERSION = VERSION = "4.3.6".freeze
104
- CODE_NAME = "Mysterious Traveller".freeze
103
+ PUMA_VERSION = VERSION = "5.0.0.beta1".freeze
104
+ CODE_NAME = "Spoony Bard".freeze
105
105
  PUMA_SERVER_STRING = ['puma', PUMA_VERSION, CODE_NAME].join(' ').freeze
106
106
 
107
107
  FAST_TRACK_KA_TIMEOUT = 0.2
@@ -175,7 +175,6 @@ module Puma
175
175
  PORT_443 = "443".freeze
176
176
  LOCALHOST = "localhost".freeze
177
177
  LOCALHOST_IP = "127.0.0.1".freeze
178
- LOCALHOST_ADDR = "127.0.0.1:0".freeze
179
178
 
180
179
  SERVER_PROTOCOL = "SERVER_PROTOCOL".freeze
181
180
  HTTP_11 = "HTTP/1.1".freeze
@@ -11,7 +11,8 @@ require 'socket'
11
11
  module Puma
12
12
  class ControlCLI
13
13
 
14
- COMMANDS = %w{halt restart phased-restart start stats status stop reload-worker-directory gc gc-stats}
14
+ COMMANDS = %w{halt restart phased-restart start stats status stop reload-worker-directory gc gc-stats thread-backtraces refork}
15
+ PRINTABLE_COMMANDS = %w{gc-stats stats thread-backtraces}
15
16
 
16
17
  def initialize(argv, stdout=STDOUT, stderr=STDERR)
17
18
  @state = nil
@@ -22,7 +23,7 @@ module Puma
22
23
  @control_auth_token = nil
23
24
  @config_file = nil
24
25
  @command = nil
25
- @environment = ENV['RACK_ENV']
26
+ @environment = ENV['RACK_ENV'] || ENV['RAILS_ENV']
26
27
 
27
28
  @argv = argv.dup
28
29
  @stdout = stdout
@@ -81,6 +82,15 @@ module Puma
81
82
 
82
83
  @command = argv.shift
83
84
 
85
+ # check presence of command
86
+ unless @command
87
+ raise "Available commands: #{COMMANDS.join(", ")}"
88
+ end
89
+
90
+ unless COMMANDS.include? @command
91
+ raise "Invalid command: #{@command}"
92
+ end
93
+
84
94
  unless @config_file == '-'
85
95
  environment = @environment || 'development'
86
96
 
@@ -99,16 +109,6 @@ module Puma
99
109
  @pidfile ||= config.options[:pidfile]
100
110
  end
101
111
  end
102
-
103
- # check present of command
104
- unless @command
105
- raise "Available commands: #{COMMANDS.join(", ")}"
106
- end
107
-
108
- unless COMMANDS.include? @command
109
- raise "Invalid command: #{@command}"
110
- end
111
-
112
112
  rescue => e
113
113
  @stdout.puts e.message
114
114
  exit 1
@@ -145,8 +145,9 @@ module Puma
145
145
  require 'openssl'
146
146
  OpenSSL::SSL::SSLSocket.new(
147
147
  TCPSocket.new(uri.host, uri.port),
148
- OpenSSL::SSL::SSLContext.new
149
- ).tap(&:connect)
148
+ OpenSSL::SSL::SSLContext.new)
149
+ .tap { |ssl| ssl.sync_close = true } # default is false
150
+ .tap(&:connect)
150
151
  when "tcp"
151
152
  TCPSocket.new uri.host, uri.port
152
153
  when "unix"
@@ -187,10 +188,16 @@ module Puma
187
188
  end
188
189
 
189
190
  message "Command #{@command} sent success"
190
- message response.last if @command == "stats" || @command == "gc-stats"
191
+ message response.last if PRINTABLE_COMMANDS.include?(@command)
191
192
  end
192
193
  ensure
193
- server.close if server && !server.closed?
194
+ if server
195
+ if uri.scheme == "ssl"
196
+ server.sysclose
197
+ else
198
+ server.close unless server.closed?
199
+ end
200
+ end
194
201
  end
195
202
 
196
203
  def send_signal
@@ -231,6 +238,9 @@ module Puma
231
238
 
232
239
  return
233
240
 
241
+ when "refork"
242
+ Process.kill "SIGURG", @pid
243
+
234
244
  else
235
245
  return
236
246
  end
@@ -262,7 +272,7 @@ module Puma
262
272
  exit 1
263
273
  end
264
274
 
265
- private
275
+ private
266
276
  def start
267
277
  require 'puma/cli'
268
278
 
@@ -12,4 +12,12 @@ module Puma
12
12
  def self.windows?
13
13
  IS_WINDOWS
14
14
  end
15
+
16
+ def self.mri?
17
+ RUBY_ENGINE == 'ruby' || RUBY_ENGINE.nil?
18
+ end
19
+
20
+ def self.forkable?
21
+ ::Process.respond_to?(:fork)
22
+ end
15
23
  end
@@ -210,20 +210,6 @@ module Puma
210
210
  @options[:clean_thread_locals] = which
211
211
  end
212
212
 
213
- # Daemonize the server into the background. It's highly recommended to
214
- # use this in combination with +pidfile+ and +stdout_redirect+.
215
- #
216
- # The default is "false".
217
- #
218
- # @example
219
- # daemonize
220
- #
221
- # @example
222
- # daemonize false
223
- def daemonize(which=true)
224
- @options[:daemon] = which
225
- end
226
-
227
213
  # When shutting down, drain the accept socket of pending
228
214
  # connections and process them. This loops over the accept
229
215
  # socket until there are no more read events and then stops
@@ -257,7 +243,7 @@ module Puma
257
243
  when :immediately
258
244
  0
259
245
  else
260
- Integer(val)
246
+ Float(val)
261
247
  end
262
248
 
263
249
  @options[:force_shutdown_after] = i
@@ -322,13 +308,7 @@ module Puma
322
308
  # @example
323
309
  # rackup '/u/apps/lolcat/config.ru'
324
310
  def rackup(path)
325
- @options[:rackup] = path.to_s
326
- end
327
-
328
- # Run Puma in TCP mode
329
- #
330
- def tcp_mode!
331
- @options[:mode] = :tcp
311
+ @options[:rackup] ||= path.to_s
332
312
  end
333
313
 
334
314
  def early_hints(answer=true)
@@ -419,8 +399,16 @@ module Puma
419
399
  @options[:state] = path.to_s
420
400
  end
421
401
 
402
+ # Use +permission+ to restrict permissions for the state file.
403
+ #
404
+ # @example
405
+ # state_permission 0600
406
+ def state_permission(permission)
407
+ @options[:state_permission] = permission
408
+ end
409
+
422
410
  # How many worker processes to run. Typically this is set to
423
- # to the number of available cores.
411
+ # the number of available cores.
424
412
  #
425
413
  # The default is 0.
426
414
  #
@@ -512,6 +500,28 @@ module Puma
512
500
 
513
501
  alias_method :after_worker_boot, :after_worker_fork
514
502
 
503
+ # When `fork_worker` is enabled, code to run in Worker 0
504
+ # before all other workers are re-forked from this process,
505
+ # after the server has temporarily stopped serving requests
506
+ # (once per complete refork cycle).
507
+ #
508
+ # This can be used to trigger extra garbage-collection to maximize
509
+ # copy-on-write efficiency, or close any connections to remote servers
510
+ # (database, Redis, ...) that were opened while the server was running.
511
+ #
512
+ # This can be called multiple times to add several hooks.
513
+ #
514
+ # @note Cluster mode with `fork_worker` enabled only.
515
+ # @example
516
+ # on_refork do
517
+ # 3.times {GC.start}
518
+ # end
519
+
520
+ def on_refork(&block)
521
+ @options[:before_refork] ||= []
522
+ @options[:before_refork] << block
523
+ end
524
+
515
525
  # Code to run out-of-band when the worker is idle.
516
526
  # These hooks run immediately after a request has finished
517
527
  # processing and there are no busy threads on the worker.
@@ -536,17 +546,6 @@ module Puma
536
546
  @options[:directory] = dir.to_s
537
547
  end
538
548
 
539
- # DEPRECATED: The directory to operate out of.
540
- def worker_directory(dir)
541
- $stderr.puts "worker_directory is deprecated. Please use `directory`"
542
- directory dir
543
- end
544
-
545
- # Run the app as a raw TCP app instead of an HTTP rack app.
546
- def tcp_mode
547
- @options[:mode] = :tcp
548
- end
549
-
550
549
  # Preload the application before starting the workers; this conflicts with
551
550
  # phased restart feature. This is off by default.
552
551
  #
@@ -695,6 +694,16 @@ module Puma
695
694
  @options[:shutdown_debug] = val
696
695
  end
697
696
 
697
+
698
+ # Attempts to route traffic to less-busy workers by causing them to delay
699
+ # listening on the socket, allowing workers which are not processing any
700
+ # requests to pick up new requests first.
701
+ #
702
+ # Only works on MRI. For all other interpreters, this setting does nothing.
703
+ def wait_for_less_busy_worker(val=0.005)
704
+ @options[:wait_for_less_busy_worker] = val.to_f
705
+ end
706
+
698
707
  # Control how the remote address of the connection is set. This
699
708
  # is configurable because to calculate the true socket peer address
700
709
  # a kernel syscall is required which for very fast rack handlers
@@ -736,5 +745,32 @@ module Puma
736
745
  end
737
746
  end
738
747
 
748
+ # When enabled, workers will be forked from worker 0 instead of from the master process.
749
+ # This option is similar to `preload_app` because the app is preloaded before forking,
750
+ # but it is compatible with phased restart.
751
+ #
752
+ # This option also enables the `refork` command (SIGURG), which optimizes copy-on-write performance
753
+ # in a running app.
754
+ #
755
+ # A refork will automatically trigger once after the specified number of requests
756
+ # (default 1000), or pass 0 to disable auto refork.
757
+ #
758
+ # @note Cluster mode only.
759
+ def fork_worker(after_requests=1000)
760
+ @options[:fork_worker] = Integer(after_requests)
761
+ end
762
+
763
+ # When enabled, Puma will GC 4 times before forking workers.
764
+ # If available (Ruby 2.7+), we will also call GC.compact.
765
+ # Not recommended for non-MRI Rubies.
766
+ #
767
+ # Based on the work of Koichi Sasada and Aaron Patterson, this option may
768
+ # decrease memory utilization of preload-enabled cluster-mode Pumas. It will
769
+ # also increase time to boot and fork. See your logs for details on how much
770
+ # time this adds to your boot process. For most apps, it will be less than one
771
+ # second.
772
+ def nakayoshi_fork(enabled=false)
773
+ @options[:nakayoshi_fork] = enabled
774
+ end
739
775
  end
740
776
  end
@@ -1,4 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'puma/detect'
4
- require 'puma/puma_http11'
3
+ module Puma
4
+ class IOBuffer < String
5
+ def append(*args)
6
+ args.each { |a| concat(a) }
7
+ end
8
+
9
+ alias reset clear
10
+ end
11
+ end
@@ -22,63 +22,5 @@ module Puma
22
22
  execlp(cmd, *argv)
23
23
  raise SystemCallError.new(FFI.errno)
24
24
  end
25
-
26
- PermKey = 'PUMA_DAEMON_PERM'
27
- RestartKey = 'PUMA_DAEMON_RESTART'
28
-
29
- # Called to tell things "Your now always in daemon mode,
30
- # don't try to reenter it."
31
- #
32
- def self.perm_daemonize
33
- ENV[PermKey] = "1"
34
- end
35
-
36
- def self.daemon?
37
- ENV.key?(PermKey) || ENV.key?(RestartKey)
38
- end
39
-
40
- def self.daemon_init
41
- return true if ENV.key?(PermKey)
42
-
43
- return false unless ENV.key? RestartKey
44
-
45
- master = ENV[RestartKey]
46
-
47
- # In case the master disappears early
48
- begin
49
- Process.kill "SIGUSR2", master.to_i
50
- rescue SystemCallError => e
51
- end
52
-
53
- ENV[RestartKey] = ""
54
-
55
- setsid
56
-
57
- null = File.open "/dev/null", "w+"
58
- STDIN.reopen null
59
- STDOUT.reopen null
60
- STDERR.reopen null
61
-
62
- true
63
- end
64
-
65
- def self.daemon_start(dir, argv)
66
- ENV[RestartKey] = Process.pid.to_s
67
-
68
- if k = ENV['PUMA_JRUBY_DAEMON_OPTS']
69
- ENV['JRUBY_OPTS'] = k
70
- end
71
-
72
- cmd = argv.first
73
- argv = ([:string] * argv.size).zip(argv).flatten
74
- argv << :string
75
- argv << nil
76
-
77
- chdir(dir)
78
- ret = fork
79
- return ret if ret != 0
80
- execlp(cmd, *argv)
81
- raise SystemCallError.new(FFI.errno)
82
- end
83
25
  end
84
26
  end