puma 2.7.0 → 3.1.1

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 (79) hide show
  1. checksums.yaml +5 -13
  2. data/DEPLOYMENT.md +91 -0
  3. data/Gemfile +3 -2
  4. data/History.txt +624 -1
  5. data/Manifest.txt +15 -3
  6. data/README.md +129 -14
  7. data/Rakefile +3 -3
  8. data/bin/puma-wild +31 -0
  9. data/bin/pumactl +1 -1
  10. data/docs/nginx.md +1 -1
  11. data/docs/signals.md +43 -0
  12. data/ext/puma_http11/extconf.rb +7 -2
  13. data/ext/puma_http11/http11_parser.java.rl +5 -5
  14. data/ext/puma_http11/io_buffer.c +1 -1
  15. data/ext/puma_http11/mini_ssl.c +233 -18
  16. data/ext/puma_http11/org/jruby/puma/Http11.java +12 -3
  17. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +39 -39
  18. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +245 -195
  19. data/ext/puma_http11/puma_http11.c +12 -4
  20. data/lib/puma.rb +1 -0
  21. data/lib/puma/app/status.rb +7 -0
  22. data/lib/puma/binder.rb +108 -39
  23. data/lib/puma/capistrano.rb +23 -6
  24. data/lib/puma/cli.rb +141 -446
  25. data/lib/puma/client.rb +48 -1
  26. data/lib/puma/cluster.rb +207 -58
  27. data/lib/puma/commonlogger.rb +107 -0
  28. data/lib/puma/configuration.rb +262 -235
  29. data/lib/puma/const.rb +97 -14
  30. data/lib/puma/control_cli.rb +85 -77
  31. data/lib/puma/convenient.rb +23 -0
  32. data/lib/puma/daemon_ext.rb +11 -4
  33. data/lib/puma/detect.rb +8 -1
  34. data/lib/puma/dsl.rb +456 -0
  35. data/lib/puma/events.rb +35 -18
  36. data/lib/puma/jruby_restart.rb +1 -1
  37. data/lib/puma/launcher.rb +399 -0
  38. data/lib/puma/minissl.rb +49 -20
  39. data/lib/puma/null_io.rb +15 -0
  40. data/lib/puma/plugin.rb +104 -0
  41. data/lib/puma/plugin/tmp_restart.rb +35 -0
  42. data/lib/puma/rack/backports/uri/common_18.rb +56 -0
  43. data/lib/puma/rack/backports/uri/common_192.rb +52 -0
  44. data/lib/puma/rack/backports/uri/common_193.rb +29 -0
  45. data/lib/puma/rack/builder.rb +295 -0
  46. data/lib/puma/rack/urlmap.rb +90 -0
  47. data/lib/puma/reactor.rb +14 -1
  48. data/lib/puma/runner.rb +35 -17
  49. data/lib/puma/server.rb +161 -58
  50. data/lib/puma/single.rb +15 -10
  51. data/lib/puma/state_file.rb +29 -0
  52. data/lib/puma/thread_pool.rb +88 -13
  53. data/lib/puma/util.rb +123 -0
  54. data/lib/rack/handler/puma.rb +35 -29
  55. data/puma.gemspec +2 -4
  56. data/tools/jungle/init.d/README.md +2 -2
  57. data/tools/jungle/init.d/puma +69 -7
  58. data/tools/jungle/upstart/puma.conf +8 -2
  59. metadata +51 -71
  60. data/COPYING +0 -55
  61. data/TODO +0 -5
  62. data/lib/puma/rack_patch.rb +0 -45
  63. data/test/test_app_status.rb +0 -92
  64. data/test/test_cli.rb +0 -173
  65. data/test/test_config.rb +0 -16
  66. data/test/test_http10.rb +0 -27
  67. data/test/test_http11.rb +0 -145
  68. data/test/test_integration.rb +0 -165
  69. data/test/test_iobuffer.rb +0 -38
  70. data/test/test_minissl.rb +0 -25
  71. data/test/test_null_io.rb +0 -31
  72. data/test/test_persistent.rb +0 -238
  73. data/test/test_puma_server.rb +0 -292
  74. data/test/test_rack_handler.rb +0 -10
  75. data/test/test_rack_server.rb +0 -141
  76. data/test/test_tcp_rack.rb +0 -42
  77. data/test/test_thread_pool.rb +0 -156
  78. data/test/test_unix_socket.rb +0 -39
  79. data/test/test_ws.rb +0 -89
data/lib/puma/client.rb CHANGED
@@ -7,6 +7,7 @@ class IO
7
7
  end
8
8
 
9
9
  require 'puma/detect'
10
+ require 'puma/delegation'
10
11
 
11
12
  if Puma::IS_JRUBY
12
13
  # We have to work around some OpenSSL buffer/io-readiness bugs
@@ -21,6 +22,7 @@ module Puma
21
22
 
22
23
  class Client
23
24
  include Puma::Const
25
+ extend Puma::Delegation
24
26
 
25
27
  def initialize(io, env=nil)
26
28
  @io = io
@@ -39,14 +41,25 @@ module Puma
39
41
 
40
42
  @body = nil
41
43
  @buffer = nil
44
+ @tempfile = nil
42
45
 
43
46
  @timeout_at = nil
44
47
 
45
48
  @requests_served = 0
46
49
  @hijacked = false
50
+
51
+ @peerip = nil
52
+ @remote_addr_header = nil
47
53
  end
48
54
 
49
- attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked
55
+ attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked,
56
+ :tempfile
57
+
58
+ attr_writer :peerip
59
+
60
+ attr_accessor :remote_addr_header
61
+
62
+ forward :closed?, :@io
50
63
 
51
64
  def inspect
52
65
  "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
@@ -59,6 +72,10 @@ module Puma
59
72
  env[HIJACK_IO] ||= @io
60
73
  end
61
74
 
75
+ def in_data_phase
76
+ !@read_header
77
+ end
78
+
62
79
  def set_timeout(val)
63
80
  @timeout_at = Time.now + val
64
81
  end
@@ -68,6 +85,7 @@ module Puma
68
85
  @read_header = true
69
86
  @env = @proto_env.dup
70
87
  @body = nil
88
+ @tempfile = nil
71
89
  @parsed_bytes = 0
72
90
  @ready = false
73
91
 
@@ -100,6 +118,7 @@ module Puma
100
118
  EmptyBody = NullIO.new
101
119
 
102
120
  def setup_body
121
+ @in_data_phase = true
103
122
  body = @parser.body
104
123
  cl = @env[CONTENT_LENGTH]
105
124
 
@@ -124,6 +143,7 @@ module Puma
124
143
  if remain > MAX_BODY
125
144
  @body = Tempfile.new(Const::PUMA_TMP_BASE)
126
145
  @body.binmode
146
+ @tempfile = @body
127
147
  else
128
148
  # The body[0,0] trick is to get an empty string in the same
129
149
  # encoding as body.
@@ -217,6 +237,14 @@ module Puma
217
237
  end
218
238
  end # IS_JRUBY
219
239
 
240
+ def finish
241
+ return true if @ready
242
+ until try_to_finish
243
+ IO.select([@to_io], nil, nil)
244
+ end
245
+ true
246
+ end
247
+
220
248
  def read_body
221
249
  # Read an odd sized chunk so we can read even sized ones
222
250
  # after this
@@ -267,11 +295,30 @@ module Puma
267
295
  end
268
296
  end
269
297
 
298
+ def write_408
299
+ begin
300
+ @io << ERROR_408_RESPONSE
301
+ rescue StandardError
302
+ end
303
+ end
304
+
270
305
  def write_500
271
306
  begin
272
307
  @io << ERROR_500_RESPONSE
273
308
  rescue StandardError
274
309
  end
275
310
  end
311
+
312
+ def peerip
313
+ return @peerip if @peerip
314
+
315
+ if @remote_addr_header
316
+ hdr = (@env[@remote_addr_header] || LOCALHOST_ADDR).split(/[\s,]/).first
317
+ @peerip = hdr
318
+ return hdr
319
+ end
320
+
321
+ @peerip ||= @io.peeraddr.last
322
+ end
276
323
  end
277
324
  end
data/lib/puma/cluster.rb CHANGED
@@ -1,12 +1,14 @@
1
1
  require 'puma/runner'
2
+ require 'time'
2
3
 
3
4
  module Puma
4
5
  class Cluster < Runner
5
- def initialize(cli)
6
- super cli
6
+ def initialize(cli, events)
7
+ super cli, events
7
8
 
8
9
  @phase = 0
9
10
  @workers = []
11
+ @next_check = nil
10
12
 
11
13
  @phased_state = :idle
12
14
  @phased_restart = false
@@ -26,51 +28,101 @@ module Puma
26
28
  def start_phased_restart
27
29
  @phase += 1
28
30
  log "- Starting phased worker restart, phase: #{@phase}"
31
+
32
+ # Be sure to change the directory again before loading
33
+ # the app. This way we can pick up new code.
34
+ if dir = @options[:worker_directory]
35
+ log "+ Changing to #{dir}"
36
+ Dir.chdir dir
37
+ end
38
+ end
39
+
40
+ def redirect_io
41
+ super
42
+
43
+ @workers.each { |x| x.hup }
29
44
  end
30
45
 
31
46
  class Worker
32
- def initialize(pid, phase)
47
+ def initialize(idx, pid, phase, options)
48
+ @index = idx
33
49
  @pid = pid
34
50
  @phase = phase
35
51
  @stage = :started
36
52
  @signal = "TERM"
53
+ @options = options
54
+ @first_term_sent = nil
55
+ @last_checkin = Time.now
56
+ @last_status = '{}'
57
+ @dead = false
37
58
  end
38
59
 
39
- attr_reader :pid, :phase, :signal
60
+ attr_reader :index, :pid, :phase, :signal, :last_checkin, :last_status
40
61
 
41
62
  def booted?
42
63
  @stage == :booted
43
64
  end
44
65
 
45
66
  def boot!
67
+ @last_checkin = Time.now
46
68
  @stage = :booted
47
69
  end
48
70
 
71
+ def dead?
72
+ @dead
73
+ end
74
+
75
+ def dead!
76
+ @dead = true
77
+ end
78
+
79
+ def ping!(status)
80
+ @last_checkin = Time.now
81
+ @last_status = status
82
+ end
83
+
84
+ def ping_timeout?(which)
85
+ Time.now - @last_checkin > which
86
+ end
87
+
49
88
  def term
50
89
  begin
51
- if @first_term_sent && (Time.new - @first_term_sent) > 30
90
+ if @first_term_sent && (Time.now - @first_term_sent) > @options[:worker_shutdown_timeout]
52
91
  @signal = "KILL"
53
92
  else
54
- @first_term_sent ||= Time.new
93
+ @first_term_sent ||= Time.now
55
94
  end
56
95
 
57
96
  Process.kill @signal, @pid
58
97
  rescue Errno::ESRCH
59
98
  end
60
99
  end
100
+
101
+ def kill
102
+ Process.kill "KILL", @pid
103
+ rescue Errno::ESRCH
104
+ end
105
+
106
+ def hup
107
+ Process.kill "HUP", @pid
108
+ rescue Errno::ESRCH
109
+ end
61
110
  end
62
111
 
63
112
  def spawn_workers
64
113
  diff = @options[:workers] - @workers.size
65
114
 
66
- upgrade = (@phased_state == :waiting)
67
-
68
115
  master = Process.pid
69
116
 
70
117
  diff.times do
71
- pid = fork { worker(upgrade, master) }
72
- @cli.debug "Spawned worker: #{pid}"
73
- @workers << Worker.new(pid, @phase)
118
+ idx = next_worker_index
119
+ @launcher.config.run_hooks :before_worker_fork, idx
120
+
121
+ pid = fork { worker(idx, master) }
122
+ debug "Spawned worker: #{pid}"
123
+ @workers << Worker.new(idx, pid, @phase, @options)
124
+
125
+ @launcher.config.run_hooks :after_worker_fork, idx
74
126
  end
75
127
 
76
128
  if diff > 0
@@ -78,11 +130,37 @@ module Puma
78
130
  end
79
131
  end
80
132
 
133
+ def next_worker_index
134
+ all_positions = 0...@options[:workers]
135
+ occupied_positions = @workers.map { |w| w.index }
136
+ available_positions = all_positions.to_a - occupied_positions
137
+ available_positions.first
138
+ end
139
+
81
140
  def all_workers_booted?
82
141
  @workers.count { |w| !w.booted? } == 0
83
142
  end
84
143
 
85
- def check_workers
144
+ def check_workers(force=false)
145
+ return if !force && @next_check && @next_check >= Time.now
146
+
147
+ @next_check = Time.now + 5
148
+
149
+ any = false
150
+
151
+ @workers.each do |w|
152
+ next if !w.booted? && !w.ping_timeout?(@options[:worker_boot_timeout])
153
+ if w.ping_timeout?(@options[:worker_timeout])
154
+ log "! Terminating timed out worker: #{w.pid}"
155
+ w.kill
156
+ any = true
157
+ end
158
+ end
159
+
160
+ # If we killed any timed out workers, try to catch them
161
+ # during this loop by giving the kernel time to kill them.
162
+ sleep 1 if any
163
+
86
164
  while @workers.any?
87
165
  pid = Process.waitpid(-1, Process::WNOHANG)
88
166
  break unless pid
@@ -90,6 +168,8 @@ module Puma
90
168
  @workers.delete_if { |w| w.pid == pid }
91
169
  end
92
170
 
171
+ @workers.delete_if(&:dead?)
172
+
93
173
  spawn_workers
94
174
 
95
175
  if all_workers_booted?
@@ -112,16 +192,22 @@ module Puma
112
192
  end
113
193
 
114
194
  def wakeup!
195
+ return unless @wakeup
196
+
115
197
  begin
116
198
  @wakeup.write "!" unless @wakeup.closed?
117
199
  rescue SystemCallError, IOError
118
200
  end
119
201
  end
120
202
 
121
- def worker(upgrade, master)
122
- $0 = "puma: cluster worker: #{master}"
203
+ def worker(index, master)
204
+ title = "puma: cluster worker #{index}: #{master}"
205
+ title << " [#{@options[:tag]}]" if @options[:tag] && !@options[:tag].empty?
206
+ $0 = title
207
+
123
208
  Signal.trap "SIGINT", "IGNORE"
124
209
 
210
+ @workers = []
125
211
  @master_read.close
126
212
  @suicide_pipe.close
127
213
 
@@ -131,19 +217,15 @@ module Puma
131
217
  exit! 1
132
218
  end
133
219
 
134
- # Be sure to change the directory again before loading
135
- # the app. This way we can pick up new code.
136
- if upgrade
137
- if dir = @options[:worker_directory]
138
- log "+ Changing to #{dir}"
139
- Dir.chdir dir
140
- end
220
+ # If we're not running under a Bundler context, then
221
+ # report the info about the context we will be using
222
+ if !ENV['BUNDLE_GEMFILE'] and File.exist?("Gemfile")
223
+ log "+ Gemfile in context: #{File.expand_path("Gemfile")}"
141
224
  end
142
225
 
143
226
  # Invoke any worker boot hooks so they can get
144
227
  # things in shape before booting the app.
145
- hooks = @options[:worker_boot]
146
- hooks.each { |h| h.call }
228
+ @launcher.config.run_hooks :before_worker_boot, index
147
229
 
148
230
  server = start_server
149
231
 
@@ -154,13 +236,33 @@ module Puma
154
236
  begin
155
237
  @worker_write << "b#{Process.pid}\n"
156
238
  rescue SystemCallError, IOError
157
- STDERR.puts "Master seems to have exitted, exitting."
239
+ STDERR.puts "Master seems to have exited, exiting."
158
240
  return
159
241
  end
160
242
 
243
+ Thread.new(@worker_write) do |io|
244
+ base_payload = "p#{Process.pid}"
245
+
246
+ while true
247
+ sleep 5
248
+ begin
249
+ b = server.backlog
250
+ r = server.running
251
+ payload = %Q!#{base_payload}{ "backlog":#{b}, "running":#{r} }\n!
252
+ io << payload
253
+ rescue IOError
254
+ break
255
+ end
256
+ end
257
+ end
258
+
161
259
  server.run.join
162
260
 
261
+ # Invoke any worker shutdown hooks so they can prevent the worker
262
+ # exiting until any background operations are completed
263
+ @launcher.config.run_hooks :before_worker_shutdown, index
163
264
  ensure
265
+ @worker_write << "t#{Process.pid}\n" rescue nil
164
266
  @worker_write.close
165
267
  end
166
268
 
@@ -195,37 +297,27 @@ module Puma
195
297
  wakeup!
196
298
  end
197
299
 
300
+ def reload_worker_directory
301
+ if dir = @options[:worker_directory]
302
+ log "+ Changing to #{dir}"
303
+ Dir.chdir dir
304
+ end
305
+ end
306
+
198
307
  def stats
199
- %Q!{ "workers": #{@workers.size}, "phase": #{@phase} }!
308
+ old_worker_count = @workers.count { |w| w.phase != @phase }
309
+ booted_worker_count = @workers.count { |w| w.booted? }
310
+ 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(",") + ']'
311
+ %Q!{ "workers": #{@workers.size}, "phase": #{@phase}, "booted_workers": #{booted_worker_count}, "old_workers": #{old_worker_count}, "worker_status": #{worker_status} }!
200
312
  end
201
313
 
202
314
  def preload?
203
315
  @options[:preload_app]
204
316
  end
205
317
 
206
- def run
207
- @status = :run
208
-
209
- output_header "cluster"
210
-
211
- log "* Process workers: #{@options[:workers]}"
212
-
213
- if preload?
214
- log "* Preloading application"
215
- load_and_bind
216
- else
217
- log "* Phased restart available"
218
-
219
- unless @cli.config.app_configured?
220
- error "No application configured, nothing to run"
221
- exit 1
222
- end
223
-
224
- @cli.binder.parse @options[:binds], self
225
- end
226
-
227
- read, @wakeup = Puma::Util.pipe
228
-
318
+ # We do this in a separate method to keep the lambad scope
319
+ # of the signals handlers as small as possible.
320
+ def setup_signals
229
321
  Signal.trap "SIGCHLD" do
230
322
  wakeup!
231
323
  end
@@ -254,6 +346,48 @@ module Puma
254
346
  stop
255
347
  end
256
348
  end
349
+ end
350
+
351
+ def run
352
+ @status = :run
353
+
354
+ output_header "cluster"
355
+
356
+ log "* Process workers: #{@options[:workers]}"
357
+
358
+ before = Thread.list
359
+
360
+ if preload?
361
+ log "* Preloading application"
362
+ load_and_bind
363
+
364
+ after = Thread.list
365
+
366
+ if after.size > before.size
367
+ threads = (after - before)
368
+ if threads.first.respond_to? :backtrace
369
+ log "! WARNING: Detected #{after.size-before.size} Thread(s) started in app boot:"
370
+ threads.each do |t|
371
+ log "! #{t.inspect} - #{t.backtrace ? t.backtrace.first : ''}"
372
+ end
373
+ else
374
+ log "! WARNING: Detected #{after.size-before.size} Thread(s) started in app boot"
375
+ end
376
+ end
377
+ else
378
+ log "* Phased restart available"
379
+
380
+ unless @launcher.config.app_configured?
381
+ error "No application configured, nothing to run"
382
+ exit 1
383
+ end
384
+
385
+ @launcher.binder.parse @options[:binds], self
386
+ end
387
+
388
+ read, @wakeup = Puma::Util.pipe
389
+
390
+ setup_signals
257
391
 
258
392
  # Used by the workers to detect if the master process dies.
259
393
  # If select says that @check_pipe is ready, it's because the
@@ -273,34 +407,49 @@ module Puma
273
407
 
274
408
  start_control
275
409
 
276
- @cli.write_state
410
+ @launcher.write_state
277
411
 
278
412
  @master_read, @worker_write = read, @wakeup
413
+
414
+ @launcher.config.run_hooks :before_fork, nil
415
+
279
416
  spawn_workers
280
417
 
281
418
  Signal.trap "SIGINT" do
282
419
  stop
283
420
  end
284
421
 
285
- @cli.events.fire_on_booted!
422
+ @launcher.events.fire_on_booted!
286
423
 
287
424
  begin
288
425
  while @status == :run
289
426
  begin
290
427
  res = IO.select([read], nil, nil, 5)
291
428
 
429
+ force_check = false
430
+
292
431
  if res
293
432
  req = read.read_nonblock(1)
294
433
 
295
- if req == "b"
296
- pid = read.gets.to_i
297
- w = @workers.find { |x| x.pid == pid }
298
- if w
434
+ next if !req || req == "!"
435
+
436
+ result = read.gets
437
+ pid = result.to_i
438
+
439
+ if w = @workers.find { |x| x.pid == pid }
440
+ case req
441
+ when "b"
299
442
  w.boot!
300
- log "- Worker #{pid} booted, phase: #{w.phase}"
301
- else
302
- log "! Out-of-sync worker list, no #{pid} worker"
443
+ log "- Worker #{w.index} (pid: #{pid}) booted, phase: #{w.phase}"
444
+ force_check = true
445
+ when "t"
446
+ w.dead!
447
+ force_check = true
448
+ when "p"
449
+ w.ping!(result.sub(/^\d+/,'').chomp)
303
450
  end
451
+ else
452
+ log "! Out-of-sync worker list, no #{pid} worker"
304
453
  end
305
454
  end
306
455
 
@@ -309,7 +458,7 @@ module Puma
309
458
  @phased_restart = false
310
459
  end
311
460
 
312
- check_workers
461
+ check_workers force_check
313
462
 
314
463
  rescue Interrupt
315
464
  @status = :stop