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/single.rb CHANGED
@@ -27,7 +27,12 @@ module Puma
27
27
  end
28
28
 
29
29
  def jruby_daemon?
30
- daemon? and @cli.jruby?
30
+ daemon? and Puma.jruby?
31
+ end
32
+
33
+ def jruby_daemon_start
34
+ require 'puma/jruby_restart'
35
+ JRubyRestart.daemon_start(@restart_dir, @launcher.restart_args)
31
36
  end
32
37
 
33
38
  def run
@@ -62,35 +67,35 @@ module Puma
62
67
  end
63
68
 
64
69
  Signal.trap "SIGCHLD" do
65
- log "! Error starting new process as daemon, exitting"
70
+ log "! Error starting new process as daemon, exiting"
66
71
  exit 1
67
72
  end
68
73
 
69
- pid = @cli.jruby_daemon_start
74
+ pid = jruby_daemon_start
70
75
  sleep
71
76
  end
72
77
  else
73
- load_and_bind
74
-
75
78
  if daemon?
76
79
  log "* Daemonizing..."
77
80
  Process.daemon(true)
81
+ redirect_io
78
82
  end
83
+
84
+ load_and_bind
79
85
  end
80
86
 
81
- @cli.write_state
87
+ @launcher.write_state
82
88
 
83
89
  start_control
84
90
 
85
91
  @server = server = start_server
86
92
 
87
- unless @options[:daemon]
93
+ unless daemon?
88
94
  log "Use Ctrl-C to stop"
95
+ redirect_io
89
96
  end
90
97
 
91
- redirect_io
92
-
93
- @cli.events.fire_on_booted!
98
+ @launcher.events.fire_on_booted!
94
99
 
95
100
  begin
96
101
  server.run.join
@@ -0,0 +1,29 @@
1
+ require 'yaml'
2
+
3
+ module Puma
4
+ class StateFile
5
+ def initialize
6
+ @options = {}
7
+ end
8
+
9
+ def save(path)
10
+ File.write path, YAML.dump(@options)
11
+ end
12
+
13
+ def load(path)
14
+ @options = YAML.load File.read(path)
15
+ end
16
+
17
+ FIELDS = %w!control_url control_auth_token pid!
18
+
19
+ FIELDS.each do |f|
20
+ define_method f do
21
+ @options[f]
22
+ end
23
+
24
+ define_method "#{f}=" do |v|
25
+ @options[f] = v
26
+ end
27
+ end
28
+ end
29
+ end
@@ -12,7 +12,8 @@ module Puma
12
12
  # thread.
13
13
  #
14
14
  def initialize(min, max, *extra, &block)
15
- @cond = ConditionVariable.new
15
+ @not_empty = ConditionVariable.new
16
+ @not_full = ConditionVariable.new
16
17
  @mutex = Mutex.new
17
18
 
18
19
  @todo = []
@@ -32,13 +33,23 @@ module Puma
32
33
  @workers = []
33
34
 
34
35
  @auto_trim = nil
36
+ @reaper = nil
35
37
 
36
38
  @mutex.synchronize do
37
39
  @min.times { spawn_thread }
38
40
  end
41
+
42
+ @clean_thread_locals = false
39
43
  end
40
44
 
41
45
  attr_reader :spawned, :trim_requested
46
+ attr_accessor :clean_thread_locals
47
+
48
+ def self.clean_thread_locals
49
+ Thread.current.keys.each do |key|
50
+ Thread.current[key] = nil unless key == :__recursive_key__
51
+ end
52
+ end
42
53
 
43
54
  # How many objects have yet to be processed by the pool?
44
55
  #
@@ -57,7 +68,8 @@ module Puma
57
68
  todo = @todo
58
69
  block = @block
59
70
  mutex = @mutex
60
- cond = @cond
71
+ not_empty = @not_empty
72
+ not_full = @not_full
61
73
 
62
74
  extra = @extra.map { |i| i.new }
63
75
 
@@ -71,6 +83,7 @@ module Puma
71
83
  if @trim_requested > 0
72
84
  @trim_requested -= 1
73
85
  continue = false
86
+ not_full.signal
74
87
  break
75
88
  end
76
89
 
@@ -80,16 +93,24 @@ module Puma
80
93
  end
81
94
 
82
95
  @waiting += 1
83
- cond.wait mutex
96
+ not_full.signal
97
+ not_empty.wait mutex
84
98
  @waiting -= 1
85
99
  end
86
100
 
87
- work = todo.pop if continue
101
+ work = todo.shift if continue
88
102
  end
89
103
 
90
104
  break unless continue
91
105
 
92
- block.call(work, *extra)
106
+ if @clean_thread_locals
107
+ ThreadPool.clean_thread_locals
108
+ end
109
+
110
+ begin
111
+ block.call(work, *extra)
112
+ rescue Exception
113
+ end
93
114
  end
94
115
 
95
116
  mutex.synchronize do
@@ -114,11 +135,19 @@ module Puma
114
135
 
115
136
  @todo << work
116
137
 
117
- if @waiting == 0 and @spawned < @max
138
+ if @waiting < @todo.size and @spawned < @max
118
139
  spawn_thread
119
140
  end
120
141
 
121
- @cond.signal
142
+ @not_empty.signal
143
+ end
144
+ end
145
+
146
+ def wait_until_not_full
147
+ @mutex.synchronize do
148
+ until @todo.size - @waiting < @max - @spawned or @shutdown
149
+ @not_full.wait @mutex
150
+ end
122
151
  end
123
152
  end
124
153
 
@@ -130,11 +159,26 @@ module Puma
130
159
  @mutex.synchronize do
131
160
  if (force or @waiting > 0) and @spawned - @trim_requested > @min
132
161
  @trim_requested += 1
133
- @cond.signal
162
+ @not_empty.signal
134
163
  end
135
164
  end
136
165
  end
137
166
 
167
+ # If there are dead threads in the pool make them go away while decreasing
168
+ # spawned counter so that new healthy threads could be created again.
169
+ def reap
170
+ @mutex.synchronize do
171
+ dead_workers = @workers.reject(&:alive?)
172
+
173
+ dead_workers.each do |worker|
174
+ worker.kill
175
+ @spawned -= 1
176
+ end
177
+
178
+ @workers -= dead_workers
179
+ end
180
+ end
181
+
138
182
  class AutoTrim
139
183
  def initialize(pool, timeout)
140
184
  @pool = pool
@@ -164,19 +208,50 @@ module Puma
164
208
  @auto_trim.start!
165
209
  end
166
210
 
211
+ class Reaper
212
+ def initialize(pool, timeout)
213
+ @pool = pool
214
+ @timeout = timeout
215
+ @running = false
216
+ end
217
+
218
+ def start!
219
+ @running = true
220
+
221
+ @thread = Thread.new do
222
+ while @running
223
+ @pool.reap
224
+ sleep @timeout
225
+ end
226
+ end
227
+ end
228
+
229
+ def stop
230
+ @running = false
231
+ @thread.wakeup
232
+ end
233
+ end
234
+
235
+ def auto_reap!(timeout=5)
236
+ @reaper = Reaper.new(self, timeout)
237
+ @reaper.start!
238
+ end
239
+
167
240
  # Tell all threads in the pool to exit and wait for them to finish.
168
241
  #
169
242
  def shutdown
170
- @mutex.synchronize do
243
+ threads = @mutex.synchronize do
171
244
  @shutdown = true
172
- @cond.broadcast
245
+ @not_empty.broadcast
246
+ @not_full.broadcast
173
247
 
174
248
  @auto_trim.stop if @auto_trim
249
+ @reaper.stop if @reaper
250
+ # dup workers so that we join them all safely
251
+ @workers.dup
175
252
  end
176
253
 
177
- # Use this instead of #each so that we don't stop in the middle
178
- # of each and see a mutated object mid #each
179
- @workers.first.join until @workers.empty?
254
+ threads.each(&:join)
180
255
 
181
256
  @spawned = 0
182
257
  @workers = []
data/lib/puma/util.rb CHANGED
@@ -1,3 +1,15 @@
1
+ major, minor, patch = RUBY_VERSION.split('.').map { |v| v.to_i }
2
+
3
+ if major == 1 && minor < 9
4
+ require 'puma/rack/backports/uri/common_18'
5
+ elsif major == 1 && minor == 9 && patch == 2 && RUBY_PATCHLEVEL <= 328 && RUBY_ENGINE != 'jruby'
6
+ require 'puma/rack/backports/uri/common_192'
7
+ elsif major == 1 && minor == 9 && patch == 3 && RUBY_PATCHLEVEL < 125
8
+ require 'puma/rack/backports/uri/common_193'
9
+ else
10
+ require 'uri/common'
11
+ end
12
+
1
13
  module Puma
2
14
  module Util
3
15
  module_function
@@ -5,5 +17,116 @@ module Puma
5
17
  def pipe
6
18
  IO.pipe
7
19
  end
20
+
21
+ # Unescapes a URI escaped string with +encoding+. +encoding+ will be the
22
+ # target encoding of the string returned, and it defaults to UTF-8
23
+ if defined?(::Encoding)
24
+ def unescape(s, encoding = Encoding::UTF_8)
25
+ URI.decode_www_form_component(s, encoding)
26
+ end
27
+ else
28
+ def unescape(s, encoding = nil)
29
+ URI.decode_www_form_component(s, encoding)
30
+ end
31
+ end
32
+ module_function :unescape
33
+
34
+ DEFAULT_SEP = /[&;] */n
35
+
36
+ # Stolen from Mongrel, with some small modifications:
37
+ # Parses a query string by breaking it up at the '&'
38
+ # and ';' characters. You can also use this to parse
39
+ # cookies by changing the characters used in the second
40
+ # parameter (which defaults to '&;').
41
+ def parse_query(qs, d = nil, &unescaper)
42
+ unescaper ||= method(:unescape)
43
+
44
+ params = {}
45
+
46
+ (qs || '').split(d ? /[#{d}] */n : DEFAULT_SEP).each do |p|
47
+ next if p.empty?
48
+ k, v = p.split('=', 2).map(&unescaper)
49
+
50
+ if cur = params[k]
51
+ if cur.class == Array
52
+ params[k] << v
53
+ else
54
+ params[k] = [cur, v]
55
+ end
56
+ else
57
+ params[k] = v
58
+ end
59
+ end
60
+
61
+ return params
62
+ end
63
+
64
+ # A case-insensitive Hash that preserves the original case of a
65
+ # header when set.
66
+ class HeaderHash < Hash
67
+ def self.new(hash={})
68
+ HeaderHash === hash ? hash : super(hash)
69
+ end
70
+
71
+ def initialize(hash={})
72
+ super()
73
+ @names = {}
74
+ hash.each { |k, v| self[k] = v }
75
+ end
76
+
77
+ def each
78
+ super do |k, v|
79
+ yield(k, v.respond_to?(:to_ary) ? v.to_ary.join("\n") : v)
80
+ end
81
+ end
82
+
83
+ def to_hash
84
+ hash = {}
85
+ each { |k,v| hash[k] = v }
86
+ hash
87
+ end
88
+
89
+ def [](k)
90
+ super(k) || super(@names[k.downcase])
91
+ end
92
+
93
+ def []=(k, v)
94
+ canonical = k.downcase
95
+ delete k if @names[canonical] && @names[canonical] != k # .delete is expensive, don't invoke it unless necessary
96
+ @names[k] = @names[canonical] = k
97
+ super k, v
98
+ end
99
+
100
+ def delete(k)
101
+ canonical = k.downcase
102
+ result = super @names.delete(canonical)
103
+ @names.delete_if { |name,| name.downcase == canonical }
104
+ result
105
+ end
106
+
107
+ def include?(k)
108
+ @names.include?(k) || @names.include?(k.downcase)
109
+ end
110
+
111
+ alias_method :has_key?, :include?
112
+ alias_method :member?, :include?
113
+ alias_method :key?, :include?
114
+
115
+ def merge!(other)
116
+ other.each { |k, v| self[k] = v }
117
+ self
118
+ end
119
+
120
+ def merge(other)
121
+ hash = dup
122
+ hash.merge! other
123
+ end
124
+
125
+ def replace(other)
126
+ clear
127
+ other.each { |k, v| self[k] = v }
128
+ self
129
+ end
130
+ end
8
131
  end
9
132
  end
@@ -5,44 +5,55 @@ module Rack
5
5
  module Handler
6
6
  module Puma
7
7
  DEFAULT_OPTIONS = {
8
- :Host => '0.0.0.0',
9
- :Port => 8080,
10
- :Threads => '0:16',
11
- :Verbose => false
8
+ :Verbose => false,
9
+ :Silent => false
12
10
  }
13
11
 
14
12
  def self.run(app, options = {})
15
13
  options = DEFAULT_OPTIONS.merge(options)
16
14
 
17
- if options[:Verbose]
18
- app = Rack::CommonLogger.new(app, STDOUT)
19
- end
15
+ conf = ::Puma::Configuration.new do |c|
16
+ c.quiet
20
17
 
21
- if options[:environment]
22
- ENV['RACK_ENV'] = options[:environment].to_s
23
- end
18
+ if options.delete(:Verbose)
19
+ app = Rack::CommonLogger.new(app, STDOUT)
20
+ end
21
+
22
+ if options[:environment]
23
+ c.environment options[:environment]
24
+ end
24
25
 
25
- server = ::Puma::Server.new(app)
26
- min, max = options[:Threads].split(':', 2)
26
+ if options[:Threads]
27
+ min, max = options.delete(:Threads).split(':', 2)
28
+ c.threads min, max
29
+ end
27
30
 
28
- puts "Puma #{::Puma::Const::PUMA_VERSION} starting..."
29
- puts "* Min threads: #{min}, max threads: #{max}"
30
- puts "* Environment: #{ENV['RACK_ENV']}"
31
- puts "* Listening on tcp://#{options[:Host]}:#{options[:Port]}"
31
+ host = options[:Host]
32
32
 
33
- server.add_tcp_listener options[:Host], options[:Port]
34
- server.min_threads = min
35
- server.max_threads = max
36
- yield server if block_given?
33
+ if host && (host[0,1] == '.' || host[0,1] == '/')
34
+ c.bind "unix://#{host}"
35
+ else
36
+ host ||= ::Puma::Configuration::DefaultTCPHost
37
+ port = options[:Port] || ::Puma::Configuration::DefaultTCPPort
38
+
39
+ c.port port, host
40
+ end
41
+
42
+ c.app app
43
+ end
37
44
 
45
+ events = options.delete(:Silent) ? ::Puma::Events.strings : ::Puma::Events.stdio
46
+
47
+ launcher = ::Puma::Launcher.new(conf, :events => events)
48
+
49
+ yield launcher if block_given?
38
50
  begin
39
- server.run.join
51
+ launcher.run
40
52
  rescue Interrupt
41
53
  puts "* Gracefully stopping, waiting for requests to finish"
42
- server.stop(true)
54
+ launcher.stop
43
55
  puts "* Goodbye!"
44
56
  end
45
-
46
57
  end
47
58
 
48
59
  def self.valid_options
@@ -50,7 +61,7 @@ module Rack
50
61
  "Host=HOST" => "Hostname to listen on (default: localhost)",
51
62
  "Port=PORT" => "Port to listen on (default: 8080)",
52
63
  "Threads=MIN:MAX" => "min:max threads to use (default 0:16)",
53
- "Quiet" => "Don't report each request"
64
+ "Verbose" => "Don't report each request (default: false)"
54
65
  }
55
66
  end
56
67
  end
@@ -59,8 +70,3 @@ module Rack
59
70
  end
60
71
  end
61
72
 
62
- # This is to trick newrelic into enabling the agent automatically.
63
- module Mongrel
64
- class HttpServer
65
- end
66
- end