Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: conditional per request timeout setting #110

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,20 @@ require "rack-timeout"
use Rack::Timeout, service_timeout: 5
```

### Conditional timeout

You can set a conditional timeout per request.
Although make sure to always meet your requirements (e.g. Heroku 30s timeout) in all cases.
If the block returns `nil` or `false`, timeout will be disabled for this request.

```ruby
# set conditional_timeout to a callable block
# config/initializers/rack_timeout.rb
Rack::Timeout.conditional_timeout = proc { |env| env["PATH_INFO"] =~ /^\/admin/ ? 20 : 5 }

# equivalent for Sinatra/Rack apps
use Rack::Timeout, conditional_timeout: ->(env) { env["PATH_INFO"] =~ /^\/admin/ ? 20 : 5 }
```

The Rabbit Hole
---------------
Expand Down
25 changes: 15 additions & 10 deletions lib/rack/timeout/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,18 @@ def read_timeout_property value, default
end

attr_reader \
:service_timeout, # How long the application can take to complete handling the request once it's passed down to it.
:wait_timeout, # How long the request is allowed to have waited before reaching rack. If exceeded, the request is 'expired', i.e. dropped entirely without being passed down to the application.
:wait_overtime, # Additional time over @wait_timeout for requests with a body, like POST requests. These may take longer to be received by the server before being passed down to the application, but should not be expired.
:service_past_wait # when false, reduces the request's computed timeout from the service_timeout value if the complete request lifetime (wait + service) would have been longer than wait_timeout (+ wait_overtime when applicable). When true, always uses the service_timeout value. we default to false under the assumption that the router would drop a request that's not responded within wait_timeout, thus being there no point in servicing beyond seconds_service_left (see code further down) up until service_timeout.

def initialize(app, service_timeout:nil, wait_timeout:nil, wait_overtime:nil, service_past_wait:false)
@service_timeout = read_timeout_property service_timeout, 15
@wait_timeout = read_timeout_property wait_timeout, 30
@wait_overtime = read_timeout_property wait_overtime, 60
@service_past_wait = service_past_wait
:service_timeout, # How long the application can take to complete handling the request once it's passed down to it.
:wait_timeout, # How long the request is allowed to have waited before reaching rack. If exceeded, the request is 'expired', i.e. dropped entirely without being passed down to the application.
:wait_overtime, # Additional time over @wait_timeout for requests with a body, like POST requests. These may take longer to be received by the server before being passed down to the application, but should not be expired.
:service_past_wait, # when false, reduces the request's computed timeout from the service_timeout value if the complete request lifetime (wait + service) would have been longer than wait_timeout (+ wait_overtime when applicable). When true, always uses the service_timeout value. we default to false under the assumption that the router would drop a request that's not responded within wait_timeout, thus being there no point in servicing beyond seconds_service_left (see code further down) up until service_timeout.
:conditional_timeout # service_timeout is determined on the fly, evaluating the given block in the middleware context

def initialize(app, service_timeout:nil, wait_timeout:nil, wait_overtime:nil, service_past_wait:false, conditional_timeout:nil)
@service_timeout = read_timeout_property service_timeout, 15
@wait_timeout = read_timeout_property wait_timeout, 30
@wait_overtime = read_timeout_property wait_overtime, 60
@service_past_wait = service_past_wait
@conditional_timeout = conditional_timeout
@app = app
end

Expand Down Expand Up @@ -96,6 +98,9 @@ def call(env)
end
end

# set service timeout from configuration or by calling conditional_timeout block with env
service_timeout = conditional_timeout.respond_to?(:call) ? conditional_timeout.call(env) : self.service_timeout

# pass request through if service_timeout is false (i.e., don't time it out at all.)
return @app.call(env) unless service_timeout

Expand Down
7 changes: 6 additions & 1 deletion lib/rack/timeout/legacy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@
module Rack::Timeout::ClassLevelProperties

module ClassMethods
attr_accessor :service_timeout, :wait_timeout, :wait_overtime, :service_past_wait
attr_accessor :service_timeout, :wait_timeout, :wait_overtime, :service_past_wait, :conditional_timeout
alias_method :timeout=, :service_timeout=

[ :service_timeout=,
:timeout=,
:wait_timeout=,
:wait_overtime=,
:service_past_wait=,
:conditional_timeout=
].each do |isetter|
setter = instance_method(isetter)
define_method(isetter) do |x|
Expand All @@ -31,6 +32,10 @@ module InstanceMethods
define_method(m) { read_timeout_property self.class.send(m), super() }
end

def conditional_timeout
self.class.conditional_timeout || super
end

def service_past_wait
self.class.service_past_wait || super
end
Expand Down