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

Add filtering by request paths #1

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
86 changes: 15 additions & 71 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,79 +1,37 @@
Rack::Timeout
Rack::Timeout::Select
=============

Abort requests that are taking too long; an exception is raised.
A fork of [Rack::Timeout](https://github.com/heroku/rack-timeout) with extended functionality that allows filtering timeout by Rails request routes.
Default behaviour is still preserved, for documentation on original middleware see the link above.

A timeout of 15s is the default. It's recommended to set the timeout as
low as realistically viable for your application. You can modify this by
setting the `RACK_TIMEOUT_SERVICE_TIMEOUT` environment variable.

There's a handful of other settings, read on for details.

Rack::Timeout is not a solution to the problem of long-running requests,
it's a debug and remediation tool. App developers should track
rack-timeout's data and address recurring instances of particular
timeouts, for example by refactoring code so it runs faster or
offsetting lengthy work to happen asynchronously.

Upgrading
---------

For fixing issues when upgrading, please see [UPGRADING](UPGRADING.md).

Basic Usage
-----------

The following covers currently supported versions of Rails, Rack, Ruby,
and Bundler. See the Compatibility section at the end for legacy
versions.

### Rails apps

```ruby
# Gemfile
gem "rack-timeout"
```

This will load rack-timeout and set it up as a Rails middleware using
the default timeout of 15s. The middleware is not inserted for the test
environment. You can modify the timeout by setting a
`RACK_TIMEOUT_SERVICE_TIMEOUT` environment variable.
Pass an an array of paths strings you want to exclude from or only run timeout for.

### Rails apps, manually

You'll need to do this if you removed `Rack::Runtime` from the
middleware stack, or if you want to determine yourself where in the
stack `Rack::Timeout` gets inserted.

```ruby
# Gemfile
gem "rack-timeout", require: "rack/timeout/base"
gem "rack-timeout", require:"rack/timeout/base", :git => 'git://github.com/hyfn/rack-timeout.git'
```

```ruby
# config/initializers/rack_timeout.rb

# insert middleware wherever you want in the stack, optionally pass
# initialization arguments, or use environment variables
Rails.application.config.middleware.insert_before Rack::Runtime, Rack::Timeout, service_timeout: 15
Rails.application.config.middleware.insert_before Rack::Runtime, Rack::Timeout::Select, service_timeout: 5, exclude: ["statistics"]
```

### Sinatra and other Rack apps

```ruby
# config.ru
require "rack-timeout"

# Call as early as possible so rack-timeout runs before all other middleware.
# Setting service_timeout or `RACK_TIMEOUT_SERVICE_TIMEOUT` environment
# variable is recommended. If omitted, defaults to 15 seconds.
use Rack::Timeout, service_timeout: 15
```
Or include the original `Rack::Timeout` instead, if you want to temporary disable path filtering.

Configuring
-----------

Rack::Timeout takes the following settings, shown here with their
Same as the original Rack::Timeout, it takes the following settings, shown here with their
default values and associated environment variables.

```
Expand All @@ -82,36 +40,22 @@ wait_timeout: 30 # RACK_TIMEOUT_WAIT_TIMEOUT
wait_overtime: 60 # RACK_TIMEOUT_WAIT_OVERTIME
service_past_wait: false # RACK_TIMEOUT_SERVICE_PAST_WAIT
term_on_timeout: false # RACK_TIMEOUT_TERM_ON_TIMEOUT
exclude: [] # RACK_TIMEOUT_EXCLUDE
only: [] # RACK_TIMEOUT_ONLY
```

Both `exclude` and `only` can be used at the same time, in this case excluded paths will be substracted from the `only` array.

These settings can be overriden during middleware initialization or
environment variables `RACK_TIMEOUT_*` mentioned above. Middleware
parameters take precedence:

```ruby
use Rack::Timeout, service_timeout: 15, wait_timeout: 30
use Rack::Timeout::Select, service_timeout: 5, exclude: ["api"]
```
[Demo application](https://github.com/mkrl/rack-timeout-test)

For more on these settings, please see [doc/settings](doc/settings.md).

Further Documentation
---------------------

Please see the [doc](doc) folder for further documentation on:

* [Risks and shortcomings of using Rack::Timeout](doc/risks.md)
* [Understanding the request lifecycle](doc/request-lifecycle.md)
* [Exceptions raised by Rack::Timeout](doc/exceptions.md)
* [Rollbar fingerprinting](doc/rollbar.md)
* [Observers](doc/observers.md)
* [Logging](doc/logging.md)

Compatibility
-------------

This version of Rack::Timeout is compatible with Ruby 2.1 and up, and,
for Rails apps, Rails 3.x and up.

Please note that you may have controller actions with names similar to your excludes/targets, use with wise.

---
Copyright © 2010-2020 Caio Chassot, released under the MIT license
Expand Down
31 changes: 29 additions & 2 deletions lib/rack/timeout/core.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,29 @@ def ms(k) # helper method used for formatting values in milliseconds
HTTP_X_REQUEST_ID = "HTTP_X_REQUEST_ID".freeze # key where request id is stored if generated by upstream client/proxy
ACTION_DISPATCH_REQUEST_ID = "action_dispatch.request_id".freeze # key where request id is stored if generated by action dispatch

class Select < Timeout
def call(env)
if exclude_or_any?(env)
super(env)
else
@app.call(env)
end
end

def exclude_or_any?(env)
if @exclude.empty? & @only.empty?
true
elsif @only.empty?
!(@exclude.any? { |path| env['PATH_INFO'].include?(path) })
elsif @exclude.empty?
@only.any? { |path| env['PATH_INFO'].include?(path) }
else
both = @only - @exclude
both.any? { |path| env['PATH_INFO'].include?(path) }
end
end
end

# helper methods to read timeout properties. Ensure they're always positive numbers or false. When set to false (or 0), their behaviour is disabled.
def read_timeout_property value, default
case value
Expand All @@ -64,14 +87,18 @@ def read_timeout_property value, default
: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.
:term_on_timeout
:term_on_timeout,
:exclude, # exclude routes with those paths in them from being processed
:only # only process requests coming from those paths

def initialize(app, service_timeout:nil, wait_timeout:nil, wait_overtime:nil, service_past_wait:"not_specified", term_on_timeout: nil)
def initialize(app, service_timeout:nil, wait_timeout:nil, wait_overtime:nil, service_past_wait:"not_specified", term_on_timeout: nil, exclude:[], only:[])
@term_on_timeout = read_timeout_property term_on_timeout, ENV.fetch("RACK_TIMEOUT_TERM_ON_TIMEOUT", 0).to_i
@service_timeout = read_timeout_property service_timeout, ENV.fetch("RACK_TIMEOUT_SERVICE_TIMEOUT", 15).to_i
@wait_timeout = read_timeout_property wait_timeout, ENV.fetch("RACK_TIMEOUT_WAIT_TIMEOUT", 30).to_i
@wait_overtime = read_timeout_property wait_overtime, ENV.fetch("RACK_TIMEOUT_WAIT_OVERTIME", 60).to_i
@service_past_wait = service_past_wait == "not_specified" ? ENV.fetch("RACK_TIMEOUT_SERVICE_PAST_WAIT", false).to_s != "false" : service_past_wait
@exclude = exclude == [] ? ENV.fetch("RACK_TIMEOUT_EXCLUDE", []) : exclude
@only = only == [] ? ENV.fetch("RACK_TIMEOUT_ONLY", []) : only

Thread.main['RACK_TIMEOUT_COUNT'] ||= 0
if @term_on_timeout
Expand Down
10 changes: 5 additions & 5 deletions rack-timeout.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@ RACK_TIMEOUT_VERSION = "0.6.0"
Gem::Specification.new do |spec|
spec.name = "rack-timeout"
spec.summary = "Abort requests that are taking too long"
spec.description = "Rack middleware which aborts requests that have been running for longer than a specified timeout."
spec.description = "Rack middleware which aborts requests that have been running for longer than a specified timeout. This fork allows filtering by request paths."
spec.version = RACK_TIMEOUT_VERSION
spec.homepage = "https://github.com/sharpstone/rack-timeout"
spec.homepage = "https://github.com/hyfn/rack-timeout"
spec.author = "Caio Chassot"
spec.email = "[email protected]"
spec.files = Dir[*%w( MIT-LICENSE CHANGELOG.md UPGRADING.md README.md lib/**/* doc/**/* )]
spec.license = "MIT"
spec.metadata = {
"bug_tracker_uri" => "https://github.com/sharpstone/rack-timeout/issues",
"changelog_uri" => "https://github.com/sharpstone/rack-timeout/blob/v#{RACK_TIMEOUT_VERSION}/CHANGELOG.md",
"bug_tracker_uri" => "https://github.com/hyfn/rack-timeout/issues",
"changelog_uri" => "https://github.com/hyfn/rack-timeout/blob/v#{RACK_TIMEOUT_VERSION}/CHANGELOG.md",
"documentation_uri" => "https://rubydoc.info/gems/rack-timeout/#{RACK_TIMEOUT_VERSION}/",
"source_code_uri" => "https://github.com/sharpstone/rack-timeout"
"source_code_uri" => "https://github.com/hyfn/rack-timeout"
}

spec.test_files = Dir.glob("test/**/*").concat([
Expand Down