puma 0.8.2 → 0.9.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.

@@ -71,7 +71,7 @@ module Puma
71
71
 
72
72
  PATH_INFO = 'PATH_INFO'.freeze
73
73
 
74
- PUMA_VERSION = VERSION = "0.8.2".freeze
74
+ PUMA_VERSION = VERSION = "0.9.0".freeze
75
75
 
76
76
  PUMA_TMP_BASE = "puma".freeze
77
77
 
@@ -121,12 +121,45 @@ module Puma
121
121
 
122
122
  SERVER_PROTOCOL = "SERVER_PROTOCOL".freeze
123
123
  HTTP_11 = "HTTP/1.1".freeze
124
+ HTTP_10 = "HTTP/1.0".freeze
124
125
 
125
126
  SERVER_SOFTWARE = "SERVER_SOFTWARE".freeze
126
127
  GATEWAY_INTERFACE = "GATEWAY_INTERFACE".freeze
127
128
  CGI_VER = "CGI/1.2".freeze
128
129
 
129
- STOP_COMMAND = "!".freeze
130
+ STOP_COMMAND = "?".freeze
131
+ HALT_COMMAND = "!".freeze
130
132
 
133
+ RACK_INPUT = "rack.input".freeze
134
+ RACK_URL_SCHEME = "rack.url_scheme".freeze
135
+ RACK_AFTER_REPLY = "rack.after_reply".freeze
136
+
137
+ HTTP = "http".freeze
138
+ HTTPS = "https".freeze
139
+
140
+ HTTPS_KEY = "HTTPS".freeze
141
+
142
+ HTTP_VERSION = "HTTP_VERSION".freeze
143
+ HTTP_CONNECTION = "HTTP_CONNECTION".freeze
144
+
145
+ HTTP_11_200 = "HTTP/1.1 200 OK\r\n".freeze
146
+ HTTP_10_200 = "HTTP/1.0 200 OK\r\n".freeze
147
+
148
+ CLOSE = "close".freeze
149
+ KEEP_ALIVE = "Keep-Alive".freeze
150
+
151
+ CONTENT_LENGTH2 = "Content-Length".freeze
152
+ CONTENT_LENGTH_S = "Content-Length: ".freeze
153
+ TRANSFER_ENCODING = "Transfer-Encoding".freeze
154
+
155
+ CONNECTION_CLOSE = "Connection: close\r\n".freeze
156
+ CONNECTION_KEEP_ALIVE = "Connection: Keep-Alive\r\n".freeze
157
+
158
+ TRANSFER_ENCODING_CHUNKED = "Transfer-Encoding: chunked\r\n".freeze
159
+ CLOSE_CHUNKED = "0\r\n\r\n".freeze
160
+
161
+ COLON = ": ".freeze
162
+
163
+ NEWLINE = "\n".freeze
131
164
  end
132
165
  end
@@ -0,0 +1,112 @@
1
+ require 'optparse'
2
+
3
+ require 'puma/const'
4
+ require 'yaml'
5
+ require 'uri'
6
+
7
+ require 'socket'
8
+
9
+ module Puma
10
+ class ControlCLI
11
+
12
+ def initialize(argv)
13
+ @argv = argv
14
+ end
15
+
16
+ def setup_options
17
+ @parser = OptionParser.new do |o|
18
+ o.on "-S", "--state PATH", "Where the state file to use is" do |arg|
19
+ @path = arg
20
+ end
21
+ end
22
+ end
23
+
24
+ def connect
25
+ if str = @state['status_address']
26
+ uri = URI.parse str
27
+ case uri.scheme
28
+ when "tcp"
29
+ return TCPSocket.new uri.host, uri.port
30
+ when "unix"
31
+ path = "#{uri.host}#{uri.path}"
32
+ return UNIXSocket.new path
33
+ else
34
+ raise "Invalid URI: #{str}"
35
+ end
36
+ end
37
+
38
+ raise "No status address configured"
39
+ end
40
+
41
+ def run
42
+ setup_options
43
+
44
+ @parser.parse! @argv
45
+
46
+ @state = YAML.load_file(@path)
47
+
48
+ cmd = @argv.shift
49
+
50
+ meth = "command_#{cmd}"
51
+
52
+ if respond_to?(meth)
53
+ __send__(meth)
54
+ else
55
+ raise "Unknown command: #{cmd}"
56
+ end
57
+ end
58
+
59
+ def command_pid
60
+ puts "#{@state['pid']}"
61
+ end
62
+
63
+ def command_stop
64
+ sock = connect
65
+ sock << "GET /stop HTTP/1.0\r\n\r\n"
66
+ rep = sock.read
67
+
68
+ body = rep.split("\r\n").last
69
+ if body != '{ "status": "ok" }'
70
+ raise "Invalid response: '#{body}'"
71
+ else
72
+ puts "Requested stop from server"
73
+ end
74
+ end
75
+
76
+ def command_halt
77
+ sock = connect
78
+ s << "GET /halt HTTP/1.0\r\n\r\n"
79
+ rep = s.read
80
+
81
+ body = rep.split("\r\n").last
82
+ if body != '{ "status": "ok" }'
83
+ raise "Invalid response: '#{body}'"
84
+ else
85
+ puts "Requested halt from server"
86
+ end
87
+ end
88
+
89
+ def command_restart
90
+ sock = connect
91
+ sock << "GET /restart HTTP/1.0\r\n\r\n"
92
+ rep = sock.read
93
+
94
+ body = rep.split("\r\n").last
95
+ if body != '{ "status": "ok" }'
96
+ raise "Invalid response: '#{body}'"
97
+ else
98
+ puts "Requested restart from server"
99
+ end
100
+ end
101
+
102
+ def command_stats
103
+ sock = connect
104
+ s << "GET /stats HTTP/1.0\r\n\r\n"
105
+ rep = s.read
106
+
107
+ body = rep.split("\r\n").last
108
+
109
+ puts body
110
+ end
111
+ end
112
+ end
@@ -2,10 +2,17 @@ require 'puma/const'
2
2
  require 'stringio'
3
3
 
4
4
  module Puma
5
+ # The default implement of an event sink object used by Server
6
+ # for when certain kinds of events occur in the life of the server.
7
+ #
8
+ # The methods available are the events that the Server fires.
9
+ #
5
10
  class Events
6
11
 
7
12
  include Const
8
13
 
14
+ # Create an Events object that prints to +stdout+ and +stderr+.
15
+ #
9
16
  def initialize(stdout, stderr)
10
17
  @stdout = stdout
11
18
  @stderr = stderr
@@ -13,11 +20,19 @@ module Puma
13
20
 
14
21
  attr_reader :stdout, :stderr
15
22
 
23
+ # An HTTP parse error has occured.
24
+ # +server+ is the Server object, +env+ the request, and +error+ a
25
+ # parsing exception.
26
+ #
16
27
  def parse_error(server, env, error)
17
28
  @stderr.puts "#{Time.now}: HTTP parse error, malformed request (#{env[HTTP_X_FORWARDED_FOR] || env[REMOTE_ADDR]}): #{error.inspect}"
18
29
  @stderr.puts "#{Time.now}: ENV: #{env.inspect}\n---\n"
19
30
  end
20
31
 
32
+ # An unknown error has occured.
33
+ # +server+ is the Server object, +env+ the request, +error+ an exception
34
+ # object, and +kind+ some additional info.
35
+ #
21
36
  def unknown_error(server, env, error, kind="Unknown")
22
37
  if error.respond_to? :render
23
38
  error.render "#{Time.now}: #{kind} error", @stderr
@@ -29,6 +44,9 @@ module Puma
29
44
 
30
45
  DEFAULT = new(STDOUT, STDERR)
31
46
 
47
+ # Returns an Events object which writes it's status to 2 StringIO
48
+ # objects.
49
+ #
32
50
  def self.strings
33
51
  Events.new StringIO.new, StringIO.new
34
52
  end
@@ -0,0 +1,35 @@
1
+ module Puma
2
+
3
+ # Provides an IO-like object that always appears to contain no data.
4
+ # Used as the value for rack.input when the request has no body.
5
+ #
6
+ class NullIO
7
+
8
+ # Always returns nil
9
+ #
10
+ def gets
11
+ nil
12
+ end
13
+
14
+ # Never yields
15
+ #
16
+ def each
17
+ end
18
+
19
+ # Always returns nil
20
+ #
21
+ def read(count)
22
+ nil
23
+ end
24
+
25
+ # Does nothing
26
+ #
27
+ def rewind
28
+ end
29
+
30
+ # Does nothing
31
+ #
32
+ def close
33
+ end
34
+ end
35
+ end
@@ -1,7 +1,10 @@
1
1
  require 'rack/commonlogger'
2
2
 
3
3
  module Rack
4
- # Patch CommonLogger to use after_reply
4
+ # Patch CommonLogger to use after_reply.
5
+ #
6
+ # Simply request this file and CommonLogger will be a bit more
7
+ # efficient.
5
8
  class CommonLogger
6
9
  remove_method :call
7
10
 
@@ -5,12 +5,15 @@ require 'stringio'
5
5
  require 'puma/thread_pool'
6
6
  require 'puma/const'
7
7
  require 'puma/events'
8
+ require 'puma/null_io'
8
9
 
9
10
  require 'puma_http11'
10
11
 
11
12
  require 'socket'
12
13
 
13
14
  module Puma
15
+
16
+ # The HTTP Server itself. Serves out a single Rack app.
14
17
  class Server
15
18
 
16
19
  include Puma::Const
@@ -22,12 +25,15 @@ module Puma
22
25
  attr_accessor :min_threads
23
26
  attr_accessor :max_threads
24
27
  attr_accessor :persistent_timeout
28
+ attr_accessor :auto_trim_time
25
29
 
26
- # Creates a working server on host:port (strange things happen if port
27
- # isn't a Number).
30
+ # Create a server for the rack app +app+.
31
+ #
32
+ # +events+ is an object which will be called when certain error events occur
33
+ # to be handled. See Puma::Events for the list of current methods to implement.
28
34
  #
29
- # Use HttpServer#run to start the server and HttpServer#acceptor.join to
30
- # join the thread that's processing incoming requests on the socket.
35
+ # Server#run returns a thread that you can join on to wait for the server
36
+ # to do it's work.
31
37
  #
32
38
  def initialize(app, events=Events::DEFAULT)
33
39
  @app = app
@@ -36,10 +42,11 @@ module Puma
36
42
  @check, @notify = IO.pipe
37
43
  @ios = [@check]
38
44
 
39
- @running = false
45
+ @status = :stop
40
46
 
41
47
  @min_threads = 0
42
48
  @max_threads = 16
49
+ @auto_trim_time = 1
43
50
 
44
51
  @thread = nil
45
52
  @thread_pool = nil
@@ -61,33 +68,77 @@ module Puma
61
68
  }
62
69
  end
63
70
 
64
- def add_tcp_listener(host, port)
65
- @ios << TCPServer.new(host, port)
71
+ # On Linux, use TCP_CORK to better control how the TCP stack
72
+ # packetizes our stream. This improves both latency and throughput.
73
+ #
74
+ if RUBY_PLATFORM =~ /linux/
75
+ # 6 == Socket::IPPROTO_TCP
76
+ # 3 == TCP_CORK
77
+ # 1/0 == turn on/off
78
+ def cork_socket(socket)
79
+ socket.setsockopt(6, 3, 1) if socket.kind_of? TCPSocket
80
+ end
81
+
82
+ def uncork_socket(socket)
83
+ socket.setsockopt(6, 3, 0) if socket.kind_of? TCPSocket
84
+ end
85
+ else
86
+ def cork_socket(socket)
87
+ end
88
+
89
+ def uncork_socket(socket)
90
+ end
66
91
  end
67
92
 
93
+ # Tell the server to listen on host +host+, port +port+.
94
+ # If optimize_for_latency is true (the default) then clients connecting
95
+ # will be optimized for latency over throughput.
96
+ #
97
+ def add_tcp_listener(host, port, optimize_for_latency=true)
98
+ s = TCPServer.new(host, port)
99
+ if optimize_for_latency
100
+ s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
101
+ end
102
+ @ios << s
103
+ end
104
+
105
+ # Tell the server to listen on +path+ as a UNIX domain socket.
106
+ #
68
107
  def add_unix_listener(path)
69
108
  @ios << UNIXServer.new(path)
70
109
  end
71
110
 
72
- # Runs the server. It returns the thread used so you can "join" it.
73
- # You can also access the HttpServer#acceptor attribute to get the
74
- # thread later.
111
+ def backlog
112
+ @thread_pool and @thread_pool.backlog
113
+ end
114
+
115
+ def running
116
+ @thread_pool and @thread_pool.spawned
117
+ end
118
+
119
+ # Runs the server. It returns the thread used so you can join it.
120
+ # The thread is always available via #thread to be join'd
121
+ #
75
122
  def run
76
123
  BasicSocket.do_not_reverse_lookup = true
77
124
 
78
- @running = true
125
+ @status = :run
79
126
 
80
127
  @thread_pool = ThreadPool.new(@min_threads, @max_threads) do |client|
81
128
  process_client(client)
82
129
  end
83
130
 
131
+ if @auto_trim_time
132
+ @thread_pool.auto_trim!(@auto_trim_time)
133
+ end
134
+
84
135
  @thread = Thread.new do
85
136
  begin
86
137
  check = @check
87
138
  sockets = @ios
88
139
  pool = @thread_pool
89
140
 
90
- while @running
141
+ while @status == :run
91
142
  begin
92
143
  ios = IO.select sockets
93
144
  ios.first.each do |sock|
@@ -104,7 +155,8 @@ module Puma
104
155
  @events.unknown_error self, env, e, "Listen loop"
105
156
  end
106
157
  end
107
- graceful_shutdown
158
+
159
+ graceful_shutdown if @status == :stop
108
160
  ensure
109
161
  @ios.each { |i| i.close }
110
162
  end
@@ -113,22 +165,35 @@ module Puma
113
165
  return @thread
114
166
  end
115
167
 
168
+ # :nodoc:
116
169
  def handle_check
117
170
  cmd = @check.read(1)
118
171
 
119
172
  case cmd
120
173
  when STOP_COMMAND
121
- @running = false
174
+ @status = :stop
175
+ return true
176
+ when HALT_COMMAND
177
+ @status = :halt
122
178
  return true
123
179
  end
124
180
 
125
181
  return false
126
182
  end
127
183
 
184
+ # Given a connection on +client+, handle the incoming requests.
185
+ #
186
+ # This method support HTTP Keep-Alive so it may, depending on if the client
187
+ # indicates that it supports keep alive, wait for another request before
188
+ # returning.
189
+ #
128
190
  def process_client(client)
191
+ parser = HttpParser.new
192
+
129
193
  begin
130
194
  while true
131
- parser = HttpParser.new
195
+ parser.reset
196
+
132
197
  env = @proto_env.dup
133
198
  data = client.readpartial(CHUNK_SIZE)
134
199
  nparsed = 0
@@ -150,7 +215,7 @@ module Puma
150
215
 
151
216
  if data.size > nparsed
152
217
  data.slice!(0, nparsed)
153
- parser = HttpParser.new
218
+ parser.reset
154
219
  env = @proto_env.dup
155
220
  nparsed = 0
156
221
  else
@@ -171,12 +236,16 @@ module Puma
171
236
  end
172
237
  end
173
238
  end
239
+
240
+ # The client disconnected while we were reading data
174
241
  rescue EOFError, SystemCallError
175
- client.close rescue nil
242
+ # Swallow them. The ensure tries to close +client+ down
176
243
 
244
+ # The client doesn't know HTTP well
177
245
  rescue HttpParserError => e
178
246
  @events.parse_error self, env, e
179
247
 
248
+ # Server error
180
249
  rescue StandardError => e
181
250
  @events.unknown_error self, env, e, "Read"
182
251
 
@@ -191,6 +260,9 @@ module Puma
191
260
  end
192
261
  end
193
262
 
263
+ # Given a Hash +env+ for the request read from +client+, add
264
+ # and fixup keys to comply with Rack's env guidelines.
265
+ #
194
266
  def normalize_env(env, client)
195
267
  if host = env[HTTP_HOST]
196
268
  if colon = host.index(":")
@@ -223,6 +295,19 @@ module Puma
223
295
  env[REMOTE_ADDR] = client.peeraddr.last
224
296
  end
225
297
 
298
+ # The object used for a request with no body. All requests with
299
+ # no body share this one object since it has no state.
300
+ EmptyBody = NullIO.new
301
+
302
+ # Given the request +env+ from +client+ and a partial request body
303
+ # in +body+, finish reading the body if there is one and invoke
304
+ # the rack app. Then construct the response and write it back to
305
+ # +client+
306
+ #
307
+ # +cl+ is the previously fetched Content-Length header if there
308
+ # was one. This is an optimization to keep from having to look
309
+ # it up again.
310
+ #
226
311
  def handle_request(env, client, body, cl)
227
312
  normalize_env env, client
228
313
 
@@ -230,26 +315,16 @@ module Puma
230
315
  body = read_body env, client, body, cl
231
316
  return false unless body
232
317
  else
233
- body = StringIO.new("")
234
- end
235
-
236
- env["rack.input"] = body
237
- env["rack.url_scheme"] = env["HTTPS"] ? "https" : "http"
238
-
239
- allow_chunked = false
240
-
241
- if env['HTTP_VERSION'] == 'HTTP/1.1'
242
- allow_chunked = true
243
- http_version = "HTTP/1.1 "
244
- keep_alive = env["HTTP_CONNECTION"] != "close"
245
- else
246
- http_version = "HTTP/1.0 "
247
- keep_alive = env["HTTP_CONNECTION"] == "Keep-Alive"
318
+ body = EmptyBody
248
319
  end
249
320
 
250
- chunked = false
321
+ env[RACK_INPUT] = body
322
+ env[RACK_URL_SCHEME] = env[HTTPS_KEY] ? HTTPS : HTTP
251
323
 
252
- after_reply = env['rack.after_reply'] = []
324
+ # A rack extension. If the app writes #call'ables to this
325
+ # array, we will invoke them when the request is done.
326
+ #
327
+ after_reply = env[RACK_AFTER_REPLY] = []
253
328
 
254
329
  begin
255
330
  begin
@@ -264,26 +339,58 @@ module Puma
264
339
  content_length = res_body[0].size
265
340
  end
266
341
 
267
- client.write http_version
268
- client.write status.to_s
269
- client.write " "
270
- client.write HTTP_STATUS_CODES[status]
271
- client.write "\r\n"
342
+ cork_socket client
272
343
 
273
- colon = ": "
274
- line_ending = "\r\n"
344
+ if env[HTTP_VERSION] == HTTP_11
345
+ allow_chunked = true
346
+ keep_alive = env[HTTP_CONNECTION] != CLOSE
347
+ include_keepalive_header = false
348
+
349
+ # An optimization. The most common response is 200, so we can
350
+ # reply with the proper 200 status without having to compute
351
+ # the response header.
352
+ #
353
+ if status == 200
354
+ client.write HTTP_11_200
355
+ else
356
+ client.write "HTTP/1.1 "
357
+ client.write status.to_s
358
+ client.write " "
359
+ client.write HTTP_STATUS_CODES[status]
360
+ client.write "\r\n"
361
+ end
362
+ else
363
+ allow_chunked = false
364
+ keep_alive = env[HTTP_CONNECTION] == KEEP_ALIVE
365
+ include_keepalive_header = keep_alive
366
+
367
+ # Same optimization as above for HTTP/1.1
368
+ #
369
+ if status == 200
370
+ client.write HTTP_10_200
371
+ else
372
+ client.write "HTTP/1.1 "
373
+ client.write status.to_s
374
+ client.write " "
375
+ client.write HTTP_STATUS_CODES[status]
376
+ client.write "\r\n"
377
+ end
378
+ end
379
+
380
+ colon = COLON
381
+ line_ending = LINE_END
275
382
 
276
383
  headers.each do |k, vs|
277
384
  case k
278
- when "Content-Length"
385
+ when CONTENT_LENGTH2
279
386
  content_length = vs
280
387
  next
281
- when "Transfer-Encoding"
388
+ when TRANSFER_ENCODING
282
389
  allow_chunked = false
283
390
  content_length = nil
284
391
  end
285
392
 
286
- vs.split("\n").each do |v|
393
+ vs.split(NEWLINE).each do |v|
287
394
  client.write k
288
395
  client.write colon
289
396
  client.write v
@@ -291,12 +398,19 @@ module Puma
291
398
  end
292
399
  end
293
400
 
294
- client.write "Connection: close\r\n" unless keep_alive
401
+ if include_keepalive_header
402
+ client.write CONNECTION_KEEP_ALIVE
403
+ elsif !keep_alive
404
+ client.write CONNECTION_CLOSE
405
+ end
295
406
 
296
407
  if content_length
297
- client.write "Content-Length: #{content_length}\r\n"
408
+ client.write CONTENT_LENGTH_S
409
+ client.write content_length.to_s
410
+ client.write line_ending
411
+ chunked = false
298
412
  elsif allow_chunked
299
- client.write "Transfer-Encoding: chunked\r\n"
413
+ client.write TRANSFER_ENCODING_CHUNKED
300
414
  chunked = true
301
415
  end
302
416
 
@@ -316,13 +430,13 @@ module Puma
316
430
  end
317
431
 
318
432
  if chunked
319
- client.write "0"
320
- client.write line_ending
321
- client.write line_ending
433
+ client.write CLOSE_CHUNKED
322
434
  client.flush
323
435
  end
324
436
 
325
437
  ensure
438
+ uncork_socket client
439
+
326
440
  body.close
327
441
  res_body.close if res_body.respond_to? :close
328
442
 
@@ -332,6 +446,13 @@ module Puma
332
446
  return keep_alive
333
447
  end
334
448
 
449
+ # Given the requset +env+ from +client+ and the partial body +body+
450
+ # plus a potential Content-Length value +cl+, finish reading
451
+ # the body and return it.
452
+ #
453
+ # If the body is larger than MAX_BODY, a Tempfile object is used
454
+ # for the body, otherwise a StringIO is used.
455
+ #
335
456
  def read_body(env, client, body, cl)
336
457
  content_length = cl.to_i
337
458
 
@@ -377,50 +498,31 @@ module Puma
377
498
  return stream
378
499
  end
379
500
 
501
+ # A fallback rack response if +@app+ raises as exception.
502
+ #
380
503
  def lowlevel_error(e)
381
504
  [500, {}, ["No application configured"]]
382
505
  end
383
506
 
384
507
  # Wait for all outstanding requests to finish.
508
+ #
385
509
  def graceful_shutdown
386
510
  @thread_pool.shutdown if @thread_pool
387
511
  end
388
512
 
389
513
  # Stops the acceptor thread and then causes the worker threads to finish
390
514
  # off the request queue before finally exiting.
515
+ #
391
516
  def stop(sync=false)
392
517
  @notify << STOP_COMMAND
393
518
 
394
519
  @thread.join if @thread && sync
395
520
  end
396
521
 
397
- def attempt_bonjour(name)
398
- begin
399
- require 'dnssd'
400
- rescue LoadError
401
- return false
402
- end
403
-
404
- @bonjour_registered = false
405
- announced = false
406
-
407
- @ios.each do |io|
408
- if io.kind_of? TCPServer
409
- fixed_name = name.gsub(/\./, "-")
522
+ def halt(sync=false)
523
+ @notify << HALT_COMMAND
410
524
 
411
- DNSSD.announce io, "puma - #{fixed_name}", "http" do |r|
412
- @bonjour_registered = true
413
- end
414
-
415
- announced = true
416
- end
417
- end
418
-
419
- return announced
420
- end
421
-
422
- def bonjour_registered?
423
- @bonjour_registered ||= false
525
+ @thread.join if @thread && sync
424
526
  end
425
527
  end
426
528
  end