Abort requests that are taking too long; a Rack::Timeout::Error will be raised.
Usage
Setup for current versions of Rails, Rack, Ruby, and Bundler. See the Compatibility section at the end for legacy versions.
Rails apps
# Gemfile
gem "rack-timeout"
That's all that's required if you want to use the default timeout of 15s. To use a custom timeout, create an initializer file:
# config/initializers/timeout.rb
Rack::Timeout.timeout = 10 # seconds
Sinatra and other Rack apps
# config.ru
require "rack-timeout"
use Rack::Timeout # Call as early as possible so rack-timeout runs before other middleware.
Rack::Timeout.timeout = 10 # This line is optional. If omitted, timeout defaults to 15 seconds.
Heroku Niceties
-
Normally, Rack::Timeout always times out a request using the
Rack::Timeout.timeoutsetting. Heroku offers theX-Request-StartHTTP header, which is a timestamp indicating the time the request first enters the routing infrastructure.If the
X-Request-StartHTTP header is present, Rack::Timeout will take the age of the request into consideration when determining the timeout to use. If a request is older than 30 seconds, it's dropped immediately. Otherwise, the timeout is the number of seconds left to 30 seconds, or the value ofRack::Timeout.timeout, whichever is shorter.So, if a request has been sitting in the queue for 25s, and
Rack::Timeout.timeoutis set to 10s, the timeout used will be 5s, because30 − 25 = 5, and5 < 10.The reasoning for this behavior is that the Heroku router drops requests if no response is received within 30s, so it makes no sense for the application to process a request it'll never be able to respond to.
The 30s maximum age is set in
Rack::Timeout::MAX_REQUEST_AGE, and should generally not be altered. -
With every line logged, Rack::Timeout includes a request ID. Generally it'll generate its own ID, but before that, it'll look for the
Heroku-Request-IDheader. If present, this is the ID that'll get logged.Heroku-Request-IDis not present by default on Heroku apps, but can be enabled through the http-request-id labs feature. It's recommended to enable http-request-id as it allows one to correlate Rack::Timeout events with the Heroku router's events. There are no downsides to enabling http-request-id.
Both these features are strictly reliant on the presence of the HTTP headers and make no effort to determine if the app is actually running on Heroku.
Request Lifetime
Throughout a request's lifetime, Rack::Timeout keeps details about the request in
env[Rack::Timeout::ENV_INFO_KEY], or, more explicitly, env["rack-timeout.info"].
The value of that entry is an instance of Rack::Timeout::RequestDetails, which is a Struct
containing the following fields:
-
id: a unique ID per request. EitherHeroku-Request-IDor a random ID generated internally. -
age: time in seconds sinceX-Request-Startwhen the request is first seen by Rack::Timeout. Only set ifX-Request-Startis present. -
timeout: timeout to be used, in seconds. GenerallyRack::Timeout.timeout, unlessX-Request-Startis present. See discussion above under the Heroku Niceties section. -
duration: set after a request completes (or times out). The time in seconds it took. -
state: the possible states are:-
expired: the request is considered too old and is skipped entirely. This happens whenX-Request-Startis present and older than 30s. When this happens, aRack::Timeout::RequestExpiryErrorexception is raised. -
ready: this is the initial state a request is in, before it's passed down the middleware chain. After that, it'll either end up astimed_outorcompleted. -
timed_out: the request had run for longer than the determined timeout and was aborted. ARack::Timeout::RequestTimeoutErrorerror is raised in the application when this occurs. -
completed: the request completed in time and Rack::Timeout is done with it. This does not mean the request completed successfully. Rack::Timeout does not concern itself with that.
-
Errors
Rack::Timeout can raise two types of exceptions. Both descend from Rack::Timeout::Error, which
itself descends from RuntimeError. They are:
-
Rack::Timeout::RequestTimeoutError: this is raised when a request has run for longer than the specified timeout. This is raised in the application thread, as per the::Timeout.timeoutsemantics, and can generally be caught within the application. -
Rack::Timeout::RequestExpiryError: this is raised when a request is skipped for being too old (see the X-Request-Start bit under the Heroku Niceties section). This cannot generally be rescued from inside a Rails controller action as it happens before the request has a chance to reach Rails.This shouldn't be any different for other frameworks, unless you have something above Rack::Timeout in the middleware stack, which you generally shouldn't.
You shouldn't generally care about rescuing from these errors. Instead, you can subscribe for state change notifications with observers.
Observers
Observers are objects or blocks that are notified about state changes during a request lifetime.
You can register an observer easily with a block:
Rack::Timeout.register_state_change_observer(:a_unique_name) { |env| do_things env }
or by passing an object that responds to rack_timeout_request_did_change_state_in(env):
class MyObserver
def rack_timeout_request_did_change_state_in(env)
# ... do stuff ...
end
end
Rack::Timeout.register_state_change_observer(:another_name, MyObserver.new)
This is how logging is implemented, too. See Rack::Timeout::StateChangeLogger.
You can remove an observer with unregister_state_change_observer:
Rack::Timeout.unregister_state_change_observer(:a_unique_name)
Custom observers might be used to store statistics on request length, timeouts, etc., and potentially do performance tuning on the fly.
Logging
Rack::Timeout logs a line every time there's a change in state in a request's lifetime.
Changes into timed_out and expired are logged at the ERROR level, everything else is INFO.
The default log level for Rack::Timeout is INFO, but can be affected via:
-
Unix environment variables. First
RACK_TIMEOUT_LOG_LEVELis checked, thenLOG_LEVEL. Their value must be name of a predefined constant in ruby'sLoggerclass, e.g.INFOorDEBUG. Case is not significant. -
By setting
Rack::Timeout.logger.leveldirectly, e.g.:Rack::Timeout.logger.level = ::Logger::DEBUG
Logging is enabled by default if Rack::Timeout is loaded via the rack-timeout file (recommended),
but can be removed by unregistering its observer:
Rack::Timeout.unregister_state_change_observer(:logger)
Each log line is a set of key=value pairs, containing the entries from the
env["rack-timeout.info"] struct that are not nil. See the Request Lifetime section above for a
description of each field. Note that while the values for age, timeout, and duration are
stored internally as seconds, they are logged as milliseconds for readability.
A sample log excerpt might look like:
source=rack-timeout id=13793c age=369ms timeout=10000ms state=ready at=info
source=rack-timeout id=13793c age=369ms timeout=10000ms duration=15ms state=completed at=info
source=rack-timeout id=ea7bd3 age=371ms timeout=10000ms state=timed_out at=error
(IDs shortened for readability.)
Compatibility
This version of Rack::Timeout is compatible with Ruby 1.9.1 and up, and, for Rails apps, Rails 3.x and up.
For applications running Ruby 1.8.x and/or Rails 2.x, use version 0.0.4.
Here Be Dragons
-
Ruby's Timeout rely on threads. If your app or any of the libraries it depends on is not thread-safe, you may run into issues using Rack::Timeout.
Concurrent web servers such as Unicorn and Puma should work fine with Rack::Timeout.
-
If you're trying to test that a
Rack::Timeout::Erroris raised in an action in your Rails application, you must do so in integration tests. Please note that Rack::Timeout will not kick in for functional tests as they bypass the rack middleware stack.
Copyright © 2010-2013 Caio Chassot, released under the MIT license
http://github.com/kch/rack-timeout