puma 4.1.1 → 5.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 +4 -4
  2. data/History.md +149 -10
  3. data/LICENSE +23 -20
  4. data/README.md +30 -46
  5. data/docs/architecture.md +3 -3
  6. data/docs/deployment.md +9 -3
  7. data/docs/fork_worker.md +31 -0
  8. data/docs/jungle/README.md +13 -0
  9. data/{tools → docs}/jungle/rc.d/README.md +0 -0
  10. data/{tools → docs}/jungle/rc.d/puma +0 -0
  11. data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
  12. data/{tools → docs}/jungle/upstart/README.md +0 -0
  13. data/{tools → docs}/jungle/upstart/puma-manager.conf +0 -0
  14. data/{tools → docs}/jungle/upstart/puma.conf +0 -0
  15. data/docs/plugins.md +20 -10
  16. data/docs/signals.md +7 -6
  17. data/docs/systemd.md +1 -63
  18. data/ext/puma_http11/PumaHttp11Service.java +2 -4
  19. data/ext/puma_http11/extconf.rb +6 -0
  20. data/ext/puma_http11/http11_parser.c +40 -63
  21. data/ext/puma_http11/http11_parser.java.rl +21 -37
  22. data/ext/puma_http11/http11_parser.rl +3 -1
  23. data/ext/puma_http11/http11_parser_common.rl +3 -3
  24. data/ext/puma_http11/mini_ssl.c +15 -2
  25. data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
  26. data/ext/puma_http11/org/jruby/puma/Http11.java +108 -116
  27. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +91 -106
  28. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +77 -18
  29. data/ext/puma_http11/puma_http11.c +9 -38
  30. data/lib/puma.rb +23 -0
  31. data/lib/puma/app/status.rb +46 -30
  32. data/lib/puma/binder.rb +112 -124
  33. data/lib/puma/cli.rb +11 -15
  34. data/lib/puma/client.rb +250 -209
  35. data/lib/puma/cluster.rb +203 -85
  36. data/lib/puma/commonlogger.rb +2 -2
  37. data/lib/puma/configuration.rb +31 -42
  38. data/lib/puma/const.rb +24 -19
  39. data/lib/puma/control_cli.rb +46 -17
  40. data/lib/puma/detect.rb +17 -0
  41. data/lib/puma/dsl.rb +162 -70
  42. data/lib/puma/error_logger.rb +97 -0
  43. data/lib/puma/events.rb +35 -31
  44. data/lib/puma/io_buffer.rb +9 -2
  45. data/lib/puma/jruby_restart.rb +0 -58
  46. data/lib/puma/launcher.rb +117 -58
  47. data/lib/puma/minissl.rb +60 -18
  48. data/lib/puma/minissl/context_builder.rb +73 -0
  49. data/lib/puma/null_io.rb +1 -1
  50. data/lib/puma/plugin.rb +6 -12
  51. data/lib/puma/rack/builder.rb +0 -4
  52. data/lib/puma/reactor.rb +16 -9
  53. data/lib/puma/runner.rb +11 -32
  54. data/lib/puma/server.rb +173 -193
  55. data/lib/puma/single.rb +7 -64
  56. data/lib/puma/state_file.rb +6 -3
  57. data/lib/puma/thread_pool.rb +104 -81
  58. data/lib/rack/handler/puma.rb +1 -5
  59. data/tools/Dockerfile +16 -0
  60. data/tools/trickletest.rb +0 -1
  61. metadata +23 -24
  62. data/ext/puma_http11/io_buffer.c +0 -155
  63. data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
  64. data/lib/puma/convenient.rb +0 -25
  65. data/lib/puma/daemon_ext.rb +0 -33
  66. data/lib/puma/delegation.rb +0 -13
  67. data/lib/puma/tcp_logger.rb +0 -41
  68. data/tools/jungle/README.md +0 -19
  69. data/tools/jungle/init.d/README.md +0 -61
  70. data/tools/jungle/init.d/puma +0 -421
  71. data/tools/jungle/init.d/run-puma +0 -18
@@ -14,11 +14,9 @@ module Puma
14
14
  # that this inherits from.
15
15
  class Single < Runner
16
16
  def stats
17
- b = @server.backlog || 0
18
- r = @server.running || 0
19
- t = @server.pool_capacity || 0
20
- m = @server.max_threads || 0
21
- %Q!{ "started_at": "#{@started_at.utc.iso8601}", "backlog": #{b}, "running": #{r}, "pool_capacity": #{t}, "max_threads": #{m} }!
17
+ {
18
+ started_at: @started_at.utc.iso8601
19
+ }.merge(@server.stats)
22
20
  end
23
21
 
24
22
  def restart
@@ -39,64 +37,10 @@ module Puma
39
37
  @server.stop(true) if @server
40
38
  end
41
39
 
42
- def jruby_daemon?
43
- daemon? and Puma.jruby?
44
- end
45
-
46
- def jruby_daemon_start
47
- require 'puma/jruby_restart'
48
- JRubyRestart.daemon_start(@restart_dir, @launcher.restart_args)
49
- end
50
-
51
40
  def run
52
- already_daemon = false
53
-
54
- if jruby_daemon?
55
- require 'puma/jruby_restart'
56
-
57
- if JRubyRestart.daemon?
58
- # load and bind before redirecting IO so errors show up on stdout/stderr
59
- load_and_bind
60
- redirect_io
61
- end
62
-
63
- already_daemon = JRubyRestart.daemon_init
64
- end
65
-
66
41
  output_header "single"
67
42
 
68
- if jruby_daemon?
69
- if already_daemon
70
- JRubyRestart.perm_daemonize
71
- else
72
- pid = nil
73
-
74
- Signal.trap "SIGUSR2" do
75
- log "* Started new process #{pid} as daemon..."
76
-
77
- # Must use exit! so we don't unwind and run the ensures
78
- # that will be run by the new child (such as deleting the
79
- # pidfile)
80
- exit!(true)
81
- end
82
-
83
- Signal.trap "SIGCHLD" do
84
- log "! Error starting new process as daemon, exiting"
85
- exit 1
86
- end
87
-
88
- jruby_daemon_start
89
- sleep
90
- end
91
- else
92
- if daemon?
93
- log "* Daemonizing..."
94
- Process.daemon(true)
95
- redirect_io
96
- end
97
-
98
- load_and_bind
99
- end
43
+ load_and_bind
100
44
 
101
45
  Plugins.fire_background
102
46
 
@@ -106,10 +50,9 @@ module Puma
106
50
 
107
51
  @server = server = start_server
108
52
 
109
- unless daemon?
110
- log "Use Ctrl-C to stop"
111
- redirect_io
112
- end
53
+
54
+ log "Use Ctrl-C to stop"
55
+ redirect_io
113
56
 
114
57
  @launcher.events.fire_on_booted!
115
58
 
@@ -8,15 +8,18 @@ module Puma
8
8
  @options = {}
9
9
  end
10
10
 
11
- def save(path)
12
- File.write path, YAML.dump(@options)
11
+ def save(path, permission = nil)
12
+ File.open(path, "w") do |file|
13
+ file.chmod(permission) if permission
14
+ file.write(YAML.dump(@options))
15
+ end
13
16
  end
14
17
 
15
18
  def load(path)
16
19
  @options = YAML.load File.read(path)
17
20
  end
18
21
 
19
- FIELDS = %w!control_url control_auth_token pid!
22
+ FIELDS = %w!control_url control_auth_token pid running_from!
20
23
 
21
24
  FIELDS.each do |f|
22
25
  define_method f do
@@ -47,6 +47,7 @@ module Puma
47
47
  @shutdown = false
48
48
 
49
49
  @trim_requested = 0
50
+ @out_of_band_pending = false
50
51
 
51
52
  @workers = []
52
53
 
@@ -54,7 +55,10 @@ module Puma
54
55
  @reaper = nil
55
56
 
56
57
  @mutex.synchronize do
57
- @min.times { spawn_thread }
58
+ @min.times do
59
+ spawn_thread
60
+ @not_full.wait(@mutex)
61
+ end
58
62
  end
59
63
 
60
64
  @clean_thread_locals = false
@@ -62,6 +66,7 @@ module Puma
62
66
 
63
67
  attr_reader :spawned, :trim_requested, :waiting
64
68
  attr_accessor :clean_thread_locals
69
+ attr_accessor :out_of_band_hook # @version 5.0.0
65
70
 
66
71
  def self.clean_thread_locals
67
72
  Thread.current.keys.each do |key| # rubocop: disable Performance/HashEachMethods
@@ -72,13 +77,18 @@ module Puma
72
77
  # How many objects have yet to be processed by the pool?
73
78
  #
74
79
  def backlog
75
- @mutex.synchronize { @todo.size }
80
+ with_mutex { @todo.size }
76
81
  end
77
82
 
78
83
  def pool_capacity
79
84
  waiting + (@max - spawned)
80
85
  end
81
86
 
87
+ # @version 5.0.0
88
+ def busy_threads
89
+ with_mutex { @spawned - @waiting + @todo.size }
90
+ end
91
+
82
92
  # :nodoc:
83
93
  #
84
94
  # Must be called with @mutex held!
@@ -87,8 +97,7 @@ module Puma
87
97
  @spawned += 1
88
98
 
89
99
  th = Thread.new(@spawned) do |spawned|
90
- # Thread name is new in Ruby 2.3
91
- Thread.current.name = 'puma %03i' % spawned if Thread.current.respond_to?(:name=)
100
+ Puma.set_thread_name 'threadpool %03i' % spawned
92
101
  todo = @todo
93
102
  block = @block
94
103
  mutex = @mutex
@@ -100,48 +109,40 @@ module Puma
100
109
  while true
101
110
  work = nil
102
111
 
103
- continue = true
104
-
105
112
  mutex.synchronize do
106
113
  while todo.empty?
107
114
  if @trim_requested > 0
108
115
  @trim_requested -= 1
109
- continue = false
110
- not_full.signal
111
- break
112
- end
113
-
114
- if @shutdown
115
- continue = false
116
- break
116
+ @spawned -= 1
117
+ @workers.delete th
118
+ Thread.exit
117
119
  end
118
120
 
119
121
  @waiting += 1
122
+ if @out_of_band_pending && trigger_out_of_band_hook
123
+ @out_of_band_pending = false
124
+ end
120
125
  not_full.signal
121
- not_empty.wait mutex
122
- @waiting -= 1
126
+ begin
127
+ not_empty.wait mutex
128
+ ensure
129
+ @waiting -= 1
130
+ end
123
131
  end
124
132
 
125
- work = todo.shift if continue
133
+ work = todo.shift
126
134
  end
127
135
 
128
- break unless continue
129
-
130
136
  if @clean_thread_locals
131
137
  ThreadPool.clean_thread_locals
132
138
  end
133
139
 
134
140
  begin
135
- block.call(work, *extra)
141
+ @out_of_band_pending = true if block.call(work, *extra)
136
142
  rescue Exception => e
137
143
  STDERR.puts "Error reached top of thread-pool: #{e.message} (#{e.class})"
138
144
  end
139
145
  end
140
-
141
- mutex.synchronize do
142
- @spawned -= 1
143
- @workers.delete th
144
- end
145
146
  end
146
147
 
147
148
  @workers << th
@@ -151,9 +152,32 @@ module Puma
151
152
 
152
153
  private :spawn_thread
153
154
 
155
+ # @version 5.0.0
156
+ def trigger_out_of_band_hook
157
+ return false unless out_of_band_hook && out_of_band_hook.any?
158
+
159
+ # we execute on idle hook when all threads are free
160
+ return false unless @spawned == @waiting
161
+
162
+ out_of_band_hook.each(&:call)
163
+ true
164
+ rescue Exception => e
165
+ STDERR.puts "Exception calling out_of_band_hook: #{e.message} (#{e.class})"
166
+ true
167
+ end
168
+
169
+ private :trigger_out_of_band_hook
170
+
171
+ # @version 5.0.0
172
+ def with_mutex(&block)
173
+ @mutex.owned? ?
174
+ yield :
175
+ @mutex.synchronize(&block)
176
+ end
177
+
154
178
  # Add +work+ to the todo list for a Thread to pickup and process.
155
179
  def <<(work)
156
- @mutex.synchronize do
180
+ with_mutex do
157
181
  if @shutdown
158
182
  raise "Unable to add work while shutting down"
159
183
  end
@@ -190,15 +214,12 @@ module Puma
190
214
  # request, it might not be added to the `@todo` array right away.
191
215
  # For example if a slow client has only sent a header, but not a body
192
216
  # then the `@todo` array would stay the same size as the reactor works
193
- # to try to buffer the request. In tha scenario the next call to this
217
+ # to try to buffer the request. In that scenario the next call to this
194
218
  # method would not block and another request would be added into the reactor
195
219
  # by the server. This would continue until a fully bufferend request
196
220
  # makes it through the reactor and can then be processed by the thread pool.
197
- #
198
- # Returns the current number of busy threads, or +nil+ if shutting down.
199
- #
200
221
  def wait_until_not_full
201
- @mutex.synchronize do
222
+ with_mutex do
202
223
  while true
203
224
  return if @shutdown
204
225
 
@@ -206,21 +227,41 @@ module Puma
206
227
  # is work queued that cannot be handled by waiting
207
228
  # threads, then accept more work until we would
208
229
  # spin up the max number of threads.
209
- busy_threads = @spawned - @waiting + @todo.size
210
- return busy_threads if @max > busy_threads
230
+ return if busy_threads < @max
211
231
 
212
232
  @not_full.wait @mutex
213
233
  end
214
234
  end
215
235
  end
216
236
 
217
- # If too many threads are in the pool, tell one to finish go ahead
237
+ # @version 5.0.0
238
+ def wait_for_less_busy_worker(delay_s)
239
+ # Ruby MRI does GVL, this can result
240
+ # in processing contention when multiple threads
241
+ # (requests) are running concurrently
242
+ return unless Puma.mri?
243
+ return unless delay_s > 0
244
+
245
+ with_mutex do
246
+ return if @shutdown
247
+
248
+ # do not delay, if we are not busy
249
+ return unless busy_threads > 0
250
+
251
+ # this will be signaled once a request finishes,
252
+ # which can happen earlier than delay
253
+ @not_full.wait @mutex, delay_s
254
+ end
255
+ end
256
+
257
+ # If there are any free threads in the pool, tell one to go ahead
218
258
  # and exit. If +force+ is true, then a trim request is requested
219
259
  # even if all threads are being utilized.
220
260
  #
221
261
  def trim(force=false)
222
- @mutex.synchronize do
223
- if (force or @waiting > 0) and @spawned - @trim_requested > @min
262
+ with_mutex do
263
+ free = @waiting - @todo.size
264
+ if (force or free > 0) and @spawned - @trim_requested > @min
224
265
  @trim_requested += 1
225
266
  @not_empty.signal
226
267
  end
@@ -230,7 +271,7 @@ module Puma
230
271
  # If there are dead threads in the pool make them go away while decreasing
231
272
  # spawned counter so that new healthy threads could be created again.
232
273
  def reap
233
- @mutex.synchronize do
274
+ with_mutex do
234
275
  dead_workers = @workers.reject(&:alive?)
235
276
 
236
277
  dead_workers.each do |worker|
@@ -244,10 +285,12 @@ module Puma
244
285
  end
245
286
  end
246
287
 
247
- class AutoTrim
248
- def initialize(pool, timeout)
288
+ class Automaton
289
+ def initialize(pool, timeout, thread_name, message)
249
290
  @pool = pool
250
291
  @timeout = timeout
292
+ @thread_name = thread_name
293
+ @message = message
251
294
  @running = false
252
295
  end
253
296
 
@@ -255,8 +298,9 @@ module Puma
255
298
  @running = true
256
299
 
257
300
  @thread = Thread.new do
301
+ Puma.set_thread_name @thread_name
258
302
  while @running
259
- @pool.trim
303
+ @pool.public_send(@message)
260
304
  sleep @timeout
261
305
  end
262
306
  end
@@ -269,44 +313,24 @@ module Puma
269
313
  end
270
314
 
271
315
  def auto_trim!(timeout=30)
272
- @auto_trim = AutoTrim.new(self, timeout)
316
+ @auto_trim = Automaton.new(self, timeout, "threadpool trimmer", :trim)
273
317
  @auto_trim.start!
274
318
  end
275
319
 
276
- class Reaper
277
- def initialize(pool, timeout)
278
- @pool = pool
279
- @timeout = timeout
280
- @running = false
281
- end
282
-
283
- def start!
284
- @running = true
285
-
286
- @thread = Thread.new do
287
- while @running
288
- @pool.reap
289
- sleep @timeout
290
- end
291
- end
292
- end
293
-
294
- def stop
295
- @running = false
296
- @thread.wakeup
297
- end
298
- end
299
-
300
320
  def auto_reap!(timeout=5)
301
- @reaper = Reaper.new(self, timeout)
321
+ @reaper = Automaton.new(self, timeout, "threadpool reaper", :reap)
302
322
  @reaper.start!
303
323
  end
304
324
 
305
325
  # Tell all threads in the pool to exit and wait for them to finish.
326
+ # Wait +timeout+ seconds then raise +ForceShutdown+ in remaining threads.
327
+ # Next, wait an extra +grace+ seconds then force-kill remaining threads.
328
+ # Finally, wait +kill_grace+ seconds for remaining threads to exit.
306
329
  #
307
330
  def shutdown(timeout=-1)
308
- threads = @mutex.synchronize do
331
+ threads = with_mutex do
309
332
  @shutdown = true
333
+ @trim_requested = @spawned
310
334
  @not_empty.broadcast
311
335
  @not_full.broadcast
312
336
 
@@ -320,27 +344,26 @@ module Puma
320
344
  # Wait for threads to finish without force shutdown.
321
345
  threads.each(&:join)
322
346
  else
323
- # Wait for threads to finish after n attempts (+timeout+).
324
- # If threads are still running, it will forcefully kill them.
325
- timeout.times do
326
- threads.delete_if do |t|
327
- t.join 1
328
- end
329
-
330
- if threads.empty?
331
- break
332
- else
333
- sleep 1
347
+ join = ->(inner_timeout) do
348
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
349
+ threads.reject! do |t|
350
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
351
+ t.join inner_timeout - elapsed
334
352
  end
335
353
  end
336
354
 
355
+ # Wait +timeout+ seconds for threads to finish.
356
+ join.call(timeout)
357
+
358
+ # If threads are still running, raise ForceShutdown and wait to finish.
337
359
  threads.each do |t|
338
360
  t.raise ForceShutdown
339
361
  end
362
+ join.call(SHUTDOWN_GRACE_TIME)
340
363
 
341
- threads.each do |t|
342
- t.join SHUTDOWN_GRACE_TIME
343
- end
364
+ # If threads are _still_ running, forcefully kill them and wait to finish.
365
+ threads.each(&:kill)
366
+ join.call(1)
344
367
  end
345
368
 
346
369
  @spawned = 0