Browse files

Limit number of keep-alive requests.

Add integration test for keep-alive.
  • Loading branch information...
1 parent c2bbc90 commit 10c8e6bdadc71c2b4b1f53e64297d7e9d3bb8a8c @macournoyer committed Aug 6, 2012
Showing with 97 additions and 17 deletions.
  1. +6 −0 lib/thin/configurator.rb
  2. +22 −10 lib/thin/protocols/http.rb
  3. +27 −4 lib/thin/server.rb
  4. +35 −0 test/integration/keep_alive_test.rb
  5. +7 −3 v2.todo
View
6 lib/thin/configurator.rb
@@ -39,6 +39,12 @@ def timeout(seconds)
set :timeout, seconds, Integer
end
+ # {include:Thin::Server#timeout}
+ # @see Thin::Server#timeout
+ def keep_alive_requests(seconds)
+ set :max_keep_alive_requests, seconds, Integer
+ end
+
# {include:Thin::Server#log_path}
# @see Thin::Server#log_path
def log_path(path)
View
32 lib/thin/protocols/http.rb
@@ -18,10 +18,17 @@ class Http < EM::Connection
attr_accessor :server
attr_accessor :listener
+ attr_accessor :can_keep_alive
+ # For tests
attr_reader :request, :response
+ def on_close(&block)
+ @on_close = block
+ end
+
+
# == EM callback methods
# Get the connection ready to process a request.
@@ -41,14 +48,8 @@ def receive_data(data)
# Called when the connection is unbinded from the socket
# and can no longer be used to process requests.
def unbind
- if @request
- @request.close
- @request = nil
- end
- if @response
- @response.close
- @response = nil
- end
+ close_request_and_response
+ @on_close.call if @on_close
end
@@ -187,7 +188,7 @@ def reset
close_connection_after_writing
end
- unbind
+ close_request_and_response
end
@@ -199,7 +200,7 @@ def send_response(response=@response)
if @request
# Keep connection alive if requested by the client.
- @response.keep_alive! if @request.keep_alive?
+ @response.keep_alive! if @can_keep_alive && @request.keep_alive?
@response.http_version = @request.http_version
end
@@ -256,6 +257,17 @@ def send_chunk(data)
private
# == Support methods
+
+ def close_request_and_response
+ if @request
+ @request.close
+ @request = nil
+ end
+ if @response
+ @response.close
+ @response = nil
+ end
+ end
# Returns IP address of peer as a string.
def socket_address
View
31 lib/thin/server.rb
@@ -85,6 +85,10 @@ class Server
# Workers are killed if they don't check-in under +timeout+ seconds.
# Default: 30
attr_accessor :timeout
+
+ # Maximum number of concurrent requests which can be made over a keep-alive connection.
+ # Default: 100
+ attr_accessor :max_keep_alive_requests
# Path to the file in which the PID is saved.
# Default: ./thin.pid
@@ -129,6 +133,9 @@ def initialize(host=nil, port=nil, app=nil, &app_loader)
@worker_connections = 1024
@threaded = false
@thread_pool_size = 20
+ @max_keep_alive_requests = 100
+ @keep_alive_requests = 0
+ @connections = 0 # Number of active connections
if System.supports_fork?
# One worker per processor
@@ -194,10 +201,26 @@ def start(daemonize=false)
@app = @app_loader.call unless @preload_app
@listeners.each do |listener|
- EM.attach_server(listener.socket, listener.protocol_class) do |c|
- c.comm_inactivity_timeout = @timeout
- c.server = self if c.respond_to?(:server=)
- c.listener = listener if c.respond_to?(:listener=)
+ EM.attach_server(listener.socket, listener.protocol_class) do |connection|
+ connection.comm_inactivity_timeout = @timeout
+ connection.server = self
+ connection.listener = listener
+
+ # We control the number of keep-alive connections to prevent easy DDoS attacks.
+ if @keep_alive_requests < @max_keep_alive_requests
+ connection.can_keep_alive = true
+ @keep_alive_requests += 1
+ else
+ connection.can_keep_alive = false
+ end
+
+ @connections += 1
+
+ # Decrement counters on close
+ connection.on_close do
+ @keep_alive_requests -= 1 if connection.can_keep_alive
+ @connections -= 1
+ end
end
end
end
View
35 test/integration/keep_alive_test.rb
@@ -0,0 +1,35 @@
+require 'test_helper'
+
+class KeepAliveTest < IntegrationTestCase
+ def test_enabled_by_default_on_http_1_1
+ thin
+
+ get '/'
+
+ assert_status 200
+ assert_header "Connection", "keep-alive"
+ end
+
+ def test_disabled_on_http_1_0
+ thin
+
+ socket do |s|
+ s.write("GET / HTTP/1.0\r\n")
+ s.write("\r\n")
+ s.flush
+
+ assert_match "Connection: close", s.readpartial(1024)
+ end
+ end
+
+ def test_limited
+ thin do
+ keep_alive_requests 0
+ end
+
+ get '/'
+
+ assert_status 200
+ assert_header "Connection", "close"
+ end
+end
View
10 v2.todo
@@ -5,13 +5,17 @@ x Async
x Preload app
x Fast file serving with streaming
x Keep-alive
+x Limit # of keep-alive requests
- Pipelining?
x Transfer-Encoding: chunked
- Use Logger
- Rotate logs on USR1 signal
- Zero downtime restart on USR2 signal
x Threading
-- Rails streaming
- Change user:group after bind
-- Wait for app ready before binding: support https://devcenter.heroku.com/articles/labs-preboot/
-- SSL
+- SSL
+- Drop custom protocol?
+- Graceful stop
+
+Optimizations:
+- Stock 200 OK response (store in frozen const)

0 comments on commit 10c8e6b

Please sign in to comment.