wendell-puma 2.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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