-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add separate IO reactor to defeat slow clients
Previously, the app thread would be in charge of reading the request directly from the client. This resulted in a set of slow clients being able to completely starve the app thread pool and prevent any further connections from being handled. This new organization uses a seperate reactor thread that is in charge of responding when a client has more data, buffering the data and attempting to parse the data. When the data represents a fully realized request, only then is it handed to the app thread pool. This means we trust apps to not starve the pool, but don't trust clients.
- Loading branch information
Showing
4 changed files
with
259 additions
and
64 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
module Puma | ||
class Client | ||
include Puma::Const | ||
|
||
def initialize(io, env) | ||
@io = io | ||
@to_io = io.to_io | ||
@proto_env = env | ||
@env = env.dup | ||
|
||
@parser = HttpParser.new | ||
@parsed_bytes = 0 | ||
@read_header = true | ||
@body = nil | ||
@buffer = nil | ||
|
||
@timeout_at = nil | ||
end | ||
|
||
attr_reader :env, :to_io, :body, :io, :timeout_at | ||
|
||
def set_timeout(val) | ||
@timeout_at = Time.now + val | ||
end | ||
|
||
def reset | ||
@parser.reset | ||
@read_header = true | ||
@env = @proto_env.dup | ||
@body = nil | ||
@parsed_bytes = 0 | ||
|
||
if @buffer | ||
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes) | ||
|
||
if @parser.finished? | ||
return setup_body | ||
elsif @parsed_bytes >= MAX_HEADER | ||
raise HttpParserError, | ||
"HEADER is longer than allowed, aborting client early." | ||
end | ||
|
||
return false | ||
end | ||
end | ||
|
||
def close | ||
@io.close | ||
end | ||
|
||
EmptyBody = NullIO.new | ||
|
||
def setup_body | ||
body = @parser.body | ||
cl = @env[CONTENT_LENGTH] | ||
|
||
unless cl | ||
@buffer = body.empty? ? nil : body | ||
@body = EmptyBody | ||
return true | ||
end | ||
|
||
remain = cl.to_i - body.bytesize | ||
|
||
if remain <= 0 | ||
@body = StringIO.new(body) | ||
return true | ||
end | ||
|
||
if remain > MAX_BODY | ||
@body = Tempfile.new(Const::PUMA_TMP_BASE) | ||
@body.binmode | ||
else | ||
# The body[0,0] trick is to get an empty string in the same | ||
# encoding as body. | ||
@body = StringIO.new body[0,0] | ||
end | ||
|
||
@body.write body | ||
|
||
@body_remain = remain | ||
|
||
@read_header = false | ||
|
||
return false | ||
end | ||
|
||
def try_to_finish | ||
return read_body unless @read_header | ||
|
||
data = @io.readpartial(CHUNK_SIZE) | ||
|
||
if @buffer | ||
@buffer << data | ||
else | ||
@buffer = data | ||
end | ||
|
||
@parsed_bytes = @parser.execute(@env, @buffer, @parsed_bytes) | ||
|
||
if @parser.finished? | ||
return setup_body | ||
elsif @parsed_bytes >= MAX_HEADER | ||
raise HttpParserError, | ||
"HEADER is longer than allowed, aborting client early." | ||
end | ||
|
||
false | ||
end | ||
|
||
def read_body | ||
# Read an odd sized chunk so we can read even sized ones | ||
# after this | ||
remain = @body_remain | ||
|
||
if remain > CHUNK_SIZE | ||
want = CHUNK_SIZE | ||
else | ||
want = remain | ||
end | ||
|
||
chunk = @io.readpartial(want) | ||
|
||
# No chunk means a closed socket | ||
unless chunk | ||
@body.close | ||
raise EOFError | ||
end | ||
|
||
remain -= @body.write(chunk) | ||
|
||
if remain <= 0 | ||
@body.rewind | ||
return true | ||
end | ||
|
||
@body_remain = remain | ||
|
||
false | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
module Puma | ||
class Reactor | ||
DefaultSleepFor = 5 | ||
|
||
def initialize(events, app_pool) | ||
@events = events | ||
@app_pool = app_pool | ||
|
||
@mutex = Mutex.new | ||
@ready, @trigger = IO.pipe | ||
@input = [] | ||
@sleep_for = DefaultSleepFor | ||
@timeouts = [] | ||
end | ||
|
||
def run | ||
sockets = [@ready] | ||
|
||
while true | ||
ready = IO.select sockets, nil, nil, @sleep_for | ||
|
||
if ready and reads = ready[0] | ||
reads.each do |c| | ||
if c == @ready | ||
@mutex.synchronize do | ||
@ready.read(1) # drain | ||
sockets += @input | ||
@input.clear | ||
end | ||
else | ||
begin | ||
if c.try_to_finish | ||
@app_pool << c | ||
sockets.delete c | ||
end | ||
# The client doesn't know HTTP well | ||
rescue HttpParserError => e | ||
@events.parse_error self, c.env, e | ||
|
||
rescue EOFError | ||
c.close | ||
sockets.delete c | ||
end | ||
end | ||
end | ||
end | ||
|
||
unless @timeouts.empty? | ||
now = Time.now | ||
|
||
while @timeouts.first.timeout_at < now | ||
c = @timeouts.shift | ||
sockets.delete c | ||
c.close | ||
|
||
if @timeouts.empty? | ||
@sleep_for = DefaultSleepFor | ||
break | ||
end | ||
end | ||
end | ||
end | ||
end | ||
|
||
def run_in_thread | ||
@thread = Thread.new { run } | ||
end | ||
|
||
def add(c) | ||
@mutex.synchronize do | ||
@input << c | ||
@trigger << "!" | ||
|
||
if c.timeout_at | ||
@timeouts << c | ||
@timeouts.sort! { |a,b| a.timeout_at <=> b.timeout_at } | ||
@sleep_for = @timeouts.first.timeout_at.to_f - Time.now.to_f | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
run lambda { |env| | ||
p :body => env['rack.input'].read | ||
[200, {"Content-Type" => "text/plain"}, ["Hello World"]] | ||
} |