wendell-puma 2.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +55 -0
  3. data/DEPLOYMENT.md +92 -0
  4. data/Gemfile +17 -0
  5. data/History.txt +588 -0
  6. data/LICENSE +26 -0
  7. data/Manifest.txt +68 -0
  8. data/README.md +251 -0
  9. data/Rakefile +158 -0
  10. data/bin/puma +10 -0
  11. data/bin/puma-wild +31 -0
  12. data/bin/pumactl +12 -0
  13. data/docs/config.md +0 -0
  14. data/docs/nginx.md +80 -0
  15. data/docs/signals.md +43 -0
  16. data/ext/puma_http11/PumaHttp11Service.java +17 -0
  17. data/ext/puma_http11/ext_help.h +15 -0
  18. data/ext/puma_http11/extconf.rb +9 -0
  19. data/ext/puma_http11/http11_parser.c +1225 -0
  20. data/ext/puma_http11/http11_parser.h +64 -0
  21. data/ext/puma_http11/http11_parser.java.rl +161 -0
  22. data/ext/puma_http11/http11_parser.rl +146 -0
  23. data/ext/puma_http11/http11_parser_common.rl +54 -0
  24. data/ext/puma_http11/io_buffer.c +155 -0
  25. data/ext/puma_http11/mini_ssl.c +198 -0
  26. data/ext/puma_http11/org/jruby/puma/Http11.java +225 -0
  27. data/ext/puma_http11/org/jruby/puma/Http11Parser.java +488 -0
  28. data/ext/puma_http11/org/jruby/puma/MiniSSL.java +391 -0
  29. data/ext/puma_http11/puma_http11.c +491 -0
  30. data/lib/puma.rb +14 -0
  31. data/lib/puma/accept_nonblock.rb +23 -0
  32. data/lib/puma/app/status.rb +59 -0
  33. data/lib/puma/binder.rb +298 -0
  34. data/lib/puma/capistrano.rb +86 -0
  35. data/lib/puma/cli.rb +606 -0
  36. data/lib/puma/client.rb +289 -0
  37. data/lib/puma/cluster.rb +404 -0
  38. data/lib/puma/compat.rb +18 -0
  39. data/lib/puma/configuration.rb +377 -0
  40. data/lib/puma/const.rb +165 -0
  41. data/lib/puma/control_cli.rb +251 -0
  42. data/lib/puma/daemon_ext.rb +25 -0
  43. data/lib/puma/delegation.rb +11 -0
  44. data/lib/puma/detect.rb +4 -0
  45. data/lib/puma/events.rb +130 -0
  46. data/lib/puma/io_buffer.rb +7 -0
  47. data/lib/puma/java_io_buffer.rb +45 -0
  48. data/lib/puma/jruby_restart.rb +83 -0
  49. data/lib/puma/minissl.rb +187 -0
  50. data/lib/puma/null_io.rb +34 -0
  51. data/lib/puma/rack_default.rb +7 -0
  52. data/lib/puma/rack_patch.rb +45 -0
  53. data/lib/puma/reactor.rb +183 -0
  54. data/lib/puma/runner.rb +146 -0
  55. data/lib/puma/server.rb +801 -0
  56. data/lib/puma/single.rb +102 -0
  57. data/lib/puma/tcp_logger.rb +32 -0
  58. data/lib/puma/thread_pool.rb +185 -0
  59. data/lib/puma/util.rb +9 -0
  60. data/lib/rack/handler/puma.rb +66 -0
  61. data/test/test_app_status.rb +92 -0
  62. data/test/test_cli.rb +173 -0
  63. data/test/test_config.rb +26 -0
  64. data/test/test_http10.rb +27 -0
  65. data/test/test_http11.rb +144 -0
  66. data/test/test_integration.rb +165 -0
  67. data/test/test_iobuffer.rb +38 -0
  68. data/test/test_minissl.rb +29 -0
  69. data/test/test_null_io.rb +31 -0
  70. data/test/test_persistent.rb +238 -0
  71. data/test/test_puma_server.rb +288 -0
  72. data/test/test_puma_server_ssl.rb +137 -0
  73. data/test/test_rack_handler.rb +10 -0
  74. data/test/test_rack_server.rb +141 -0
  75. data/test/test_tcp_rack.rb +42 -0
  76. data/test/test_thread_pool.rb +156 -0
  77. data/test/test_unix_socket.rb +39 -0
  78. data/test/test_ws.rb +89 -0
  79. data/tools/jungle/README.md +9 -0
  80. data/tools/jungle/init.d/README.md +54 -0
  81. data/tools/jungle/init.d/puma +332 -0
  82. data/tools/jungle/init.d/run-puma +3 -0
  83. data/tools/jungle/upstart/README.md +61 -0
  84. data/tools/jungle/upstart/puma-manager.conf +31 -0
  85. data/tools/jungle/upstart/puma.conf +63 -0
  86. data/tools/trickletest.rb +45 -0
  87. data/wendell-puma.gemspec +55 -0
  88. metadata +225 -0
@@ -0,0 +1,289 @@
1
+ class IO
2
+ # We need to use this for a jruby work around on both 1.8 and 1.9.
3
+ # So this either creates the constant (on 1.8), or harmlessly
4
+ # reopens it (on 1.9).
5
+ module WaitReadable
6
+ end
7
+ end
8
+
9
+ require 'puma/detect'
10
+
11
+ if Puma::IS_JRUBY
12
+ # We have to work around some OpenSSL buffer/io-readiness bugs
13
+ # so we pull it in regardless of if the user is binding
14
+ # to an SSL socket
15
+ require 'openssl'
16
+ end
17
+
18
+ module Puma
19
+
20
+ class ConnectionError < RuntimeError; end
21
+
22
+ class Client
23
+ include Puma::Const
24
+
25
+ def initialize(io, env=nil)
26
+ @io = io
27
+ @to_io = io.to_io
28
+ @proto_env = env
29
+ if !env
30
+ @env = nil
31
+ else
32
+ @env = env.dup
33
+ end
34
+
35
+ @parser = HttpParser.new
36
+ @parsed_bytes = 0
37
+ @read_header = true
38
+ @ready = false
39
+
40
+ @body = nil
41
+ @buffer = nil
42
+
43
+ @timeout_at = nil
44
+
45
+ @requests_served = 0
46
+ @hijacked = false
47
+ end
48
+
49
+ attr_reader :env, :to_io, :body, :io, :timeout_at, :ready, :hijacked
50
+
51
+ def inspect
52
+ "#<Puma::Client:0x#{object_id.to_s(16)} @ready=#{@ready.inspect}>"
53
+ end
54
+
55
+ # For the hijack protocol (allows us to just put the Client object
56
+ # into the env)
57
+ def call
58
+ @hijacked = true
59
+ env[HIJACK_IO] ||= @io
60
+ end
61
+
62
+ def in_data_phase
63
+ !@read_header
64
+ end
65
+
66
+ def set_timeout(val)
67
+ @timeout_at = Time.now + val
68
+ end
69
+
70
+ def reset(fast_check=true)
71
+ @parser.reset
72
+ @read_header = true
73
+ @env = @proto_env.dup
74
+ @body = nil
75
+ @parsed_bytes = 0
76
+ @ready = false
77
+
78
+ if @buffer
79
+ @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
80
+
81
+ if @parser.finished?
82
+ return setup_body
83
+ elsif @parsed_bytes >= MAX_HEADER
84
+ raise HttpParserError,
85
+ "HEADER is longer than allowed, aborting client early."
86
+ end
87
+
88
+ return false
89
+ elsif fast_check &&
90
+ IO.select([@to_io], nil, nil, FAST_TRACK_KA_TIMEOUT)
91
+ return try_to_finish
92
+ end
93
+ end
94
+
95
+ def close
96
+ begin
97
+ @io.close
98
+ rescue IOError
99
+ end
100
+ end
101
+
102
+ # The object used for a request with no body. All requests with
103
+ # no body share this one object since it has no state.
104
+ EmptyBody = NullIO.new
105
+
106
+ def setup_body
107
+ @in_data_phase = true
108
+ body = @parser.body
109
+ cl = @env[CONTENT_LENGTH]
110
+
111
+ unless cl
112
+ @buffer = body.empty? ? nil : body
113
+ @body = EmptyBody
114
+ @requests_served += 1
115
+ @ready = true
116
+ return true
117
+ end
118
+
119
+ remain = cl.to_i - body.bytesize
120
+
121
+ if remain <= 0
122
+ @body = StringIO.new(body)
123
+ @buffer = nil
124
+ @requests_served += 1
125
+ @ready = true
126
+ return true
127
+ end
128
+
129
+ if remain > MAX_BODY
130
+ @body = Tempfile.new(Const::PUMA_TMP_BASE)
131
+ @body.binmode
132
+ else
133
+ # The body[0,0] trick is to get an empty string in the same
134
+ # encoding as body.
135
+ @body = StringIO.new body[0,0]
136
+ end
137
+
138
+ @body.write body
139
+
140
+ @body_remain = remain
141
+
142
+ @read_header = false
143
+
144
+ return false
145
+ end
146
+
147
+ def try_to_finish
148
+ return read_body unless @read_header
149
+
150
+ begin
151
+ data = @io.read_nonblock(CHUNK_SIZE)
152
+ rescue Errno::EAGAIN
153
+ return false
154
+ rescue SystemCallError, IOError
155
+ raise ConnectionError, "Connection error detected during read"
156
+ end
157
+
158
+ if @buffer
159
+ @buffer << data
160
+ else
161
+ @buffer = data
162
+ end
163
+
164
+ @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
165
+
166
+ if @parser.finished?
167
+ return setup_body
168
+ elsif @parsed_bytes >= MAX_HEADER
169
+ raise HttpParserError,
170
+ "HEADER is longer than allowed, aborting client early."
171
+ end
172
+
173
+ false
174
+ end
175
+
176
+ if IS_JRUBY
177
+ def jruby_start_try_to_finish
178
+ return read_body unless @read_header
179
+
180
+ begin
181
+ data = @io.sysread_nonblock(CHUNK_SIZE)
182
+ rescue OpenSSL::SSL::SSLError => e
183
+ return false if e.kind_of? IO::WaitReadable
184
+ raise e
185
+ end
186
+
187
+ if @buffer
188
+ @buffer << data
189
+ else
190
+ @buffer = data
191
+ end
192
+
193
+ @parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes)
194
+
195
+ if @parser.finished?
196
+ return setup_body
197
+ elsif @parsed_bytes >= MAX_HEADER
198
+ raise HttpParserError,
199
+ "HEADER is longer than allowed, aborting client early."
200
+ end
201
+
202
+ false
203
+ end
204
+
205
+ def eagerly_finish
206
+ return true if @ready
207
+
208
+ if @io.kind_of? OpenSSL::SSL::SSLSocket
209
+ return true if jruby_start_try_to_finish
210
+ end
211
+
212
+ return false unless IO.select([@to_io], nil, nil, 0)
213
+ try_to_finish
214
+ end
215
+
216
+ else
217
+
218
+ def eagerly_finish
219
+ return true if @ready
220
+ return false unless IO.select([@to_io], nil, nil, 0)
221
+ try_to_finish
222
+ end
223
+ end # IS_JRUBY
224
+
225
+ def read_body
226
+ # Read an odd sized chunk so we can read even sized ones
227
+ # after this
228
+ remain = @body_remain
229
+
230
+ if remain > CHUNK_SIZE
231
+ want = CHUNK_SIZE
232
+ else
233
+ want = remain
234
+ end
235
+
236
+ begin
237
+ chunk = @io.read_nonblock(want)
238
+ rescue Errno::EAGAIN
239
+ return false
240
+ rescue SystemCallError, IOError
241
+ raise ConnectionError, "Connection error detected during read"
242
+ end
243
+
244
+ # No chunk means a closed socket
245
+ unless chunk
246
+ @body.close
247
+ @buffer = nil
248
+ @requests_served += 1
249
+ @ready = true
250
+ raise EOFError
251
+ end
252
+
253
+ remain -= @body.write(chunk)
254
+
255
+ if remain <= 0
256
+ @body.rewind
257
+ @buffer = nil
258
+ @requests_served += 1
259
+ @ready = true
260
+ return true
261
+ end
262
+
263
+ @body_remain = remain
264
+
265
+ false
266
+ end
267
+
268
+ def write_400
269
+ begin
270
+ @io << ERROR_400_RESPONSE
271
+ rescue StandardError
272
+ end
273
+ end
274
+
275
+ def write_408
276
+ begin
277
+ @io << ERROR_408_RESPONSE
278
+ rescue StandardError
279
+ end
280
+ end
281
+
282
+ def write_500
283
+ begin
284
+ @io << ERROR_500_RESPONSE
285
+ rescue StandardError
286
+ end
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,404 @@
1
+ require 'puma/runner'
2
+
3
+ module Puma
4
+ class Cluster < Runner
5
+ def initialize(cli)
6
+ super cli
7
+
8
+ @phase = 0
9
+ @workers = []
10
+ @next_check = nil
11
+
12
+ @phased_state = :idle
13
+ @phased_restart = false
14
+ end
15
+
16
+ def stop_workers
17
+ log "- Gracefully shutting down workers..."
18
+ @workers.each { |x| x.term }
19
+
20
+ begin
21
+ Process.waitall
22
+ rescue Interrupt
23
+ log "! Cancelled waiting for workers"
24
+ end
25
+ end
26
+
27
+ def start_phased_restart
28
+ @phase += 1
29
+ log "- Starting phased worker restart, phase: #{@phase}"
30
+
31
+ # Be sure to change the directory again before loading
32
+ # the app. This way we can pick up new code.
33
+ if dir = @options[:worker_directory]
34
+ log "+ Changing to #{dir}"
35
+ Dir.chdir dir
36
+ end
37
+ end
38
+
39
+ def redirect_io
40
+ super
41
+
42
+ @workers.each { |x| x.hup }
43
+ end
44
+
45
+ class Worker
46
+ def initialize(idx, pid, phase)
47
+ @index = idx
48
+ @pid = pid
49
+ @phase = phase
50
+ @stage = :started
51
+ @signal = "TERM"
52
+ @last_checkin = Time.now
53
+ end
54
+
55
+ attr_reader :index, :pid, :phase, :signal, :last_checkin
56
+
57
+ def booted?
58
+ @stage == :booted
59
+ end
60
+
61
+ def boot!
62
+ @last_checkin = Time.now
63
+ @stage = :booted
64
+ end
65
+
66
+ def ping!
67
+ @last_checkin = Time.now
68
+ end
69
+
70
+ def ping_timeout?(which)
71
+ Time.now - @last_checkin > which
72
+ end
73
+
74
+ def term
75
+ begin
76
+ if @first_term_sent && (Time.new - @first_term_sent) > 30
77
+ @signal = "KILL"
78
+ else
79
+ @first_term_sent ||= Time.new
80
+ end
81
+
82
+ Process.kill @signal, @pid
83
+ rescue Errno::ESRCH
84
+ end
85
+ end
86
+
87
+ def kill
88
+ Process.kill "KILL", @pid
89
+ rescue Errno::ESRCH
90
+ end
91
+
92
+ def hup
93
+ Process.kill "HUP", @pid
94
+ rescue Errno::ESRCH
95
+ end
96
+ end
97
+
98
+ def spawn_workers
99
+ diff = @options[:workers] - @workers.size
100
+
101
+ master = Process.pid
102
+
103
+ diff.times do
104
+ idx = next_worker_index
105
+
106
+ pid = fork { worker(idx, master) }
107
+ @cli.debug "Spawned worker: #{pid}"
108
+ @workers << Worker.new(idx, pid, @phase)
109
+ @options[:after_worker_boot].each { |h| h.call }
110
+ end
111
+
112
+ if diff > 0
113
+ @phased_state = :idle
114
+ end
115
+ end
116
+
117
+ def next_worker_index
118
+ all_positions = 0...@options[:workers]
119
+ occupied_positions = @workers.map { |w| w.index }
120
+ available_positions = all_positions.to_a - occupied_positions
121
+ available_positions.first
122
+ end
123
+
124
+ def all_workers_booted?
125
+ @workers.count { |w| !w.booted? } == 0
126
+ end
127
+
128
+ def check_workers(force=false)
129
+ return if !force && @next_check && @next_check >= Time.now
130
+
131
+ @next_check = Time.now + 5
132
+
133
+ any = false
134
+
135
+ @workers.each do |w|
136
+ if w.ping_timeout?(@options[:worker_timeout])
137
+ log "! Terminating timed out worker: #{w.pid}"
138
+ w.kill
139
+ any = true
140
+ end
141
+ end
142
+
143
+ # If we killed any timed out workers, try to catch them
144
+ # during this loop by giving the kernel time to kill them.
145
+ sleep 1 if any
146
+
147
+ while @workers.any?
148
+ pid = Process.waitpid(-1, Process::WNOHANG)
149
+ break unless pid
150
+
151
+ @workers.delete_if { |w| w.pid == pid }
152
+ end
153
+
154
+ spawn_workers
155
+
156
+ if all_workers_booted?
157
+ # If we're running at proper capacity, check to see if
158
+ # we need to phase any workers out (which will restart
159
+ # in the right phase).
160
+ #
161
+ w = @workers.find { |x| x.phase != @phase }
162
+
163
+ if w
164
+ if @phased_state == :idle
165
+ @phased_state = :waiting
166
+ log "- Stopping #{w.pid} for phased upgrade..."
167
+ end
168
+
169
+ w.term
170
+ log "- #{w.signal} sent to #{w.pid}..."
171
+ end
172
+ end
173
+ end
174
+
175
+ def wakeup!
176
+ begin
177
+ @wakeup.write "!" unless @wakeup.closed?
178
+ rescue SystemCallError, IOError
179
+ end
180
+ end
181
+
182
+ def worker(index, master)
183
+ $0 = "puma: cluster worker #{index}: #{master}"
184
+ Signal.trap "SIGINT", "IGNORE"
185
+
186
+ @workers = []
187
+ @master_read.close
188
+ @suicide_pipe.close
189
+
190
+ Thread.new do
191
+ IO.select [@check_pipe]
192
+ log "! Detected parent died, dying"
193
+ exit! 1
194
+ end
195
+
196
+ # If we're not running under a Bundler context, then
197
+ # report the info about the context we will be using
198
+ if !ENV['BUNDLE_GEMFILE'] and File.exist?("Gemfile")
199
+ log "+ Gemfile in context: #{File.expand_path("Gemfile")}"
200
+ end
201
+
202
+ # Invoke any worker boot hooks so they can get
203
+ # things in shape before booting the app.
204
+ hooks = @options[:before_worker_boot]
205
+ hooks.each { |h| h.call(index) }
206
+
207
+ server = start_server
208
+
209
+ Signal.trap "SIGTERM" do
210
+ server.stop
211
+ end
212
+
213
+ begin
214
+ @worker_write << "b#{Process.pid}\n"
215
+ rescue SystemCallError, IOError
216
+ STDERR.puts "Master seems to have exitted, exitting."
217
+ return
218
+ end
219
+
220
+ Thread.new(@worker_write) do |io|
221
+ payload = "p#{Process.pid}\n"
222
+
223
+ while true
224
+ sleep 5
225
+ io << payload
226
+ end
227
+ end
228
+
229
+ server.run.join
230
+
231
+ ensure
232
+ @worker_write.close
233
+ end
234
+
235
+ def restart
236
+ @restart = true
237
+ stop
238
+ end
239
+
240
+ def phased_restart
241
+ return false if @options[:preload_app]
242
+
243
+ @phased_restart = true
244
+ wakeup!
245
+
246
+ true
247
+ end
248
+
249
+ def stop
250
+ @status = :stop
251
+ wakeup!
252
+ end
253
+
254
+ def stop_blocked
255
+ @status = :stop if @status == :run
256
+ wakeup!
257
+ @control.stop(true) if @control
258
+ Process.waitall
259
+ end
260
+
261
+ def halt
262
+ @status = :halt
263
+ wakeup!
264
+ end
265
+
266
+ def stats
267
+ %Q!{ "workers": #{@workers.size}, "phase": #{@phase}, "booted_workers": #{@workers.count{|w| w.booted?}} }!
268
+ end
269
+
270
+ def preload?
271
+ @options[:preload_app]
272
+ end
273
+
274
+ def run
275
+ @status = :run
276
+
277
+ output_header "cluster"
278
+
279
+ log "* Process workers: #{@options[:workers]}"
280
+
281
+ if preload?
282
+ log "* Preloading application"
283
+ load_and_bind
284
+ else
285
+ log "* Phased restart available"
286
+
287
+ unless @cli.config.app_configured?
288
+ error "No application configured, nothing to run"
289
+ exit 1
290
+ end
291
+
292
+ @cli.binder.parse @options[:binds], self
293
+ end
294
+
295
+ read, @wakeup = Puma::Util.pipe
296
+
297
+ Signal.trap "SIGCHLD" do
298
+ wakeup!
299
+ end
300
+
301
+ Signal.trap "TTIN" do
302
+ @options[:workers] += 1
303
+ wakeup!
304
+ end
305
+
306
+ Signal.trap "TTOU" do
307
+ @options[:workers] -= 1 if @options[:workers] >= 2
308
+ @workers.last.term
309
+ wakeup!
310
+ end
311
+
312
+ master_pid = Process.pid
313
+
314
+ Signal.trap "SIGTERM" do
315
+ # The worker installs their own SIGTERM when booted.
316
+ # Until then, this is run by the worker and the worker
317
+ # should just exit if they get it.
318
+ if Process.pid != master_pid
319
+ log "Early termination of worker"
320
+ exit! 0
321
+ else
322
+ stop
323
+ end
324
+ end
325
+
326
+ # Used by the workers to detect if the master process dies.
327
+ # If select says that @check_pipe is ready, it's because the
328
+ # master has exited and @suicide_pipe has been automatically
329
+ # closed.
330
+ #
331
+ @check_pipe, @suicide_pipe = Puma::Util.pipe
332
+
333
+ if daemon?
334
+ log "* Daemonizing..."
335
+ Process.daemon(true)
336
+ else
337
+ log "Use Ctrl-C to stop"
338
+ end
339
+
340
+ redirect_io
341
+
342
+ start_control
343
+
344
+ @cli.write_state
345
+
346
+ @master_read, @worker_write = read, @wakeup
347
+ spawn_workers
348
+
349
+ Signal.trap "SIGINT" do
350
+ stop
351
+ end
352
+
353
+ @cli.events.fire_on_booted!
354
+
355
+ begin
356
+ while @status == :run
357
+ begin
358
+ res = IO.select([read], nil, nil, 5)
359
+
360
+ force_check = false
361
+
362
+ if res
363
+ req = read.read_nonblock(1)
364
+
365
+ next if !req || req == "!"
366
+
367
+ pid = read.gets.to_i
368
+
369
+ if w = @workers.find { |x| x.pid == pid }
370
+ case req
371
+ when "b"
372
+ w.boot!
373
+ log "- Worker #{w.index} (pid: #{pid}) booted, phase: #{w.phase}"
374
+ force_check = true
375
+ when "p"
376
+ w.ping!
377
+ end
378
+ else
379
+ log "! Out-of-sync worker list, no #{pid} worker"
380
+ end
381
+ end
382
+
383
+ if @phased_restart
384
+ start_phased_restart
385
+ @phased_restart = false
386
+ end
387
+
388
+ check_workers force_check
389
+
390
+ rescue Interrupt
391
+ @status = :stop
392
+ end
393
+ end
394
+
395
+ stop_workers unless @status == :halt
396
+ ensure
397
+ @check_pipe.close
398
+ @suicide_pipe.close
399
+ read.close
400
+ @wakeup.close
401
+ end
402
+ end
403
+ end
404
+ end