rack-timeout 0.5.0.1 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/doc/exceptions.md +24 -0
- data/doc/logging.md +41 -0
- data/doc/observers.md +22 -0
- data/doc/request-lifecycle.md +27 -0
- data/doc/risks.md +36 -0
- data/doc/rollbar.md +30 -0
- data/doc/settings.md +49 -0
- data/lib/rack/timeout/core.rb +3 -2
- data/test/basic_test.rb +11 -20
- data/test/env_settings_test.rb +20 -0
- data/test/test_helper.rb +46 -0
- metadata +13 -4
- data/CHANGELOG +0 -103
- data/README.markdown +0 -317
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 642f765e106069393a85355bb1b205479b125e9e4fb639c4b8b7f8309b5563ea
|
4
|
+
data.tar.gz: 0d8dc45b11b4561760d7163526b9926f8c566ed8c29ff83f7665cc890360da9e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 33ee501f48e9ab8a7399fca2ec9c2ec1c3c26b10e6352b4371e4ca4b01706186457146556dbdcf38e82483e4348eb8815bd77dd15ea7e350e473e56c6f784249
|
7
|
+
data.tar.gz: eaf9a7f04e9b2f1b3404d4fcf2a8f88d368cc0ed4e24b1c9df3cedd032ca41e63879c3064c02652e718fd0a6693756a2b303e832a4f1ffa03b595bfcc168c555
|
data/doc/exceptions.md
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
Exceptions
|
2
|
+
----------
|
3
|
+
|
4
|
+
Rack::Timeout can raise three types of exceptions. They are:
|
5
|
+
|
6
|
+
Two descend from `Rack::Timeout::Error`, which itself descends from `RuntimeError` and is generally caught by an unqualified `rescue`. The third, `RequestTimeoutException`, is more complicated and the most important.
|
7
|
+
|
8
|
+
* `Rack::Timeout::RequestTimeoutException`: this is raised when a request has run for longer than the specified timeout. This descends from `Exception`, not from `Rack::Timeout::Error` (it has to be rescued from explicitly). It's raised by the rack-timeout timer thread in the application thread, at the point in the stack the app happens to be in when the timeout is triggered. This exception could be caught explicitly within the application, but in doing so you're working past the timeout. This is ok for quick cleanup work but shouldn't be abused as Rack::Timeout will not kick in twice for the same request.
|
9
|
+
|
10
|
+
Rails will generally intercept `Exception`s, but in plain Rack apps, this exception will be caught by rack-timeout and re-raised as a `Rack::Timeout::RequestTimeoutError`. This is to prevent an `Exception` from bubbling up beyond rack-timeout and to the server.
|
11
|
+
|
12
|
+
* `Rack::Timeout::RequestTimeoutError` descends from `Rack::Timeout::Error`, but it's only really seen in the case described above. It'll not be seen in a standard Rails app, and will only be seen in Sinatra if rescuing from exceptions is disabled.
|
13
|
+
|
14
|
+
* `Rack::Timeout::RequestExpiryError`: this is raised when a request is skipped for being too old (see Wait Timeout section). This error cannot generally be rescued from inside a Rails controller action as it happens before the request has a chance to enter Rails.
|
15
|
+
|
16
|
+
This shouldn't be different for other frameworks, unless you have something above Rack::Timeout in the middleware stack, which you generally shouldn't.
|
17
|
+
|
18
|
+
You shouldn't rescue from these errors for reporting purposes. Instead, you can subscribe for state change notifications with observers.
|
19
|
+
|
20
|
+
If you're trying to test that a `Rack::Timeout::RequestTimeoutException` is 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.
|
21
|
+
|
22
|
+
[More details about testing middleware with Rails here][pablobm].
|
23
|
+
|
24
|
+
[pablobm]: http://stackoverflow.com/a/8681208/13989
|
data/doc/logging.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
Logging
|
2
|
+
-------
|
3
|
+
|
4
|
+
Rack::Timeout logs a line every time there's a change in state in a request's lifetime.
|
5
|
+
|
6
|
+
Request state changes into `timed_out` and `expired` are logged at the `ERROR` level, most other things are logged as `INFO`. The `active` state is logged as `DEBUG`, every ~1s while the request is still active.
|
7
|
+
|
8
|
+
Rack::Timeout will try to use `Rails.logger` if present, otherwise it'll look for a logger in `env['rack.logger']`, and if neither are present, it'll create its own logger, either writing to `env['rack.errors']`, or to `$stderr` if the former is not set.
|
9
|
+
|
10
|
+
When creating its own logger, rack-timeout will use a log level of `INFO`. Otherwise whatever log level is already set on the logger being used continues in effect.
|
11
|
+
|
12
|
+
A custom logger can be set via `Rack::Timeout::Logger.logger`. This takes priority over the automatic logger detection:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
Rack::Timeout::Logger.logger = Logger.new
|
16
|
+
```
|
17
|
+
|
18
|
+
There are helper setters that replace the logger:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
Rack::Timeout::Logger.device = $stderr
|
22
|
+
Rack::Timeout::Logger.level = Logger::INFO
|
23
|
+
```
|
24
|
+
|
25
|
+
Although each call replaces the logger, these can be use together and the final logger will retain both properties. (If only one is called, the defaults used above apply.)
|
26
|
+
|
27
|
+
Logging is enabled by default, but can be removed with:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
Rack::Timeout::Logger.disable
|
31
|
+
```
|
32
|
+
|
33
|
+
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 `wait`, `timeout`, and `service` are stored internally as seconds, they are logged as milliseconds for readability.
|
34
|
+
|
35
|
+
A sample log excerpt might look like:
|
36
|
+
|
37
|
+
```
|
38
|
+
source=rack-timeout id=13793c wait=369ms timeout=10000ms state=ready at=info
|
39
|
+
source=rack-timeout id=13793c wait=369ms timeout=10000ms service=15ms state=completed at=info
|
40
|
+
source=rack-timeout id=ea7bd3 wait=371ms timeout=10000ms state=timed_out at=error
|
41
|
+
```
|
data/doc/observers.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Observers
|
2
|
+
---------
|
3
|
+
|
4
|
+
Observers are blocks that are notified about state changes during a request's lifetime. Keep in mind that the `active` state is set every ~1s, so you'll be notified every time.
|
5
|
+
|
6
|
+
You can register an observer with:
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
Rack::Timeout.register_state_change_observer(:a_unique_name) { |env| do_things env }
|
10
|
+
```
|
11
|
+
|
12
|
+
There's currently no way to subscribe to changes into or out of a particular state. To check the actual state we're moving into, read `env['rack-timeout.info'].state`. Handling going out of a state would require some additional logic in the observer.
|
13
|
+
|
14
|
+
You can remove an observer with `unregister_state_change_observer`:
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
Rack::Timeout.unregister_state_change_observer(:a_unique_name)
|
18
|
+
```
|
19
|
+
|
20
|
+
rack-timeout's logging is implemented using an observer; see `Rack::Timeout::StateChangeLoggingObserver` in logging-observer.rb for the implementation.
|
21
|
+
|
22
|
+
Custom observers might be used to do cleanup, store statistics on request length, timeouts, etc., and potentially do performance tuning on the fly.
|
@@ -0,0 +1,27 @@
|
|
1
|
+
Request Lifetime
|
2
|
+
----------------
|
3
|
+
|
4
|
+
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"]`.
|
5
|
+
|
6
|
+
The value of that entry is an instance of `Rack::Timeout::RequestDetails`, which is a `Struct` consisting of the following fields:
|
7
|
+
|
8
|
+
* `id`: a unique ID per request. Either the value of the `X-Request-ID` header or a random ID
|
9
|
+
generated internally.
|
10
|
+
|
11
|
+
* `wait`: time in seconds since `X-Request-Start` at the time the request was initially seen by Rack::Timeout. Only set if `X-Request-Start` is present.
|
12
|
+
|
13
|
+
* `timeout`: the final timeout value that was used or to be used, in seconds. For `expired` requests, that would be the `wait_timeout`, possibly with `wait_overtime` applied. In all other cases it's the `service_timeout`, potentially reduced to make up for time lost waiting. (See discussion regarding `service_past_wait` above, under the Wait Timeout section.)
|
14
|
+
|
15
|
+
* `service`: set after a request completes (or times out). The time in seconds it took being processed. This is also updated while a request is still active, around every second, with the time taken so far.
|
16
|
+
|
17
|
+
* `state`: the possible states, and their log level, are:
|
18
|
+
|
19
|
+
* `expired` (`ERROR`): the request is considered too old and is skipped entirely. This happens when `X-Request-Start` is present and older than `wait_timeout`. When in this state, `Rack::Timeout::RequestExpiryError` is raised. See earlier discussion about the `wait_overtime` setting, too.
|
20
|
+
|
21
|
+
* `ready` (`INFO`): this is the state a request is in right before it's passed down the middleware chain. Once it's being processed, it'll move on to `active`, and then on to `timed_out` and/or `completed`.
|
22
|
+
|
23
|
+
* `active` (`DEBUG`): the request is being actively processed in the application thread. This is signaled repeatedly every ~1s until the request completes or times out.
|
24
|
+
|
25
|
+
* `timed_out` (`ERROR`): the request ran for longer than the determined timeout and was aborted. `Rack::Timeout::RequestTimeoutException` is raised in the application when this occurs. This state is not the final one, `completed` will be set after the framework is done with it. (If the exception does bubble up, it's caught by rack-timeout and re-raised as `Rack::Timeout::RequestTimeoutError`, which descends from RuntimeError.)
|
26
|
+
|
27
|
+
* `completed` (`INFO`): the request completed and Rack::Timeout is done with it. This does not mean the request completed *successfully*. Rack::Timeout does not concern itself with that. As mentioned just above, a timed out request will still end up with a `completed` state.
|
data/doc/risks.md
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
Risks and shortcomings of using Rack::Timeout
|
2
|
+
---------------------------------------------
|
3
|
+
|
4
|
+
### Timing Out During IO Blocks
|
5
|
+
|
6
|
+
Sometimes a request is taking too long to complete because it's blocked waiting on synchronous IO. Such IO does not need to be file operations, it could be, say, network or database operations. If said IO is happening in a C library that's unaware of ruby's interrupt system (i.e. anything written without ruby in mind), calling `Thread#raise` (that's what rack-timeout uses) will not have effect until after the IO block is gone.
|
7
|
+
|
8
|
+
At the moment rack-timeout does not try to address this issue. As a fail-safe against these cases, a blunter solution that kills the entire process is recommended, such as unicorn's timeouts.
|
9
|
+
|
10
|
+
More detailed explanations of the issues surrounding timing out in ruby during IO blocks can be found at:
|
11
|
+
|
12
|
+
- http://redgetan.cc/understanding-timeouts-in-cruby/
|
13
|
+
|
14
|
+
### Timing Out is Inherently Unsafe
|
15
|
+
|
16
|
+
Raising mid-flight in stateful applications is inherently unsafe. A request can be aborted at any moment in the code flow, and the application can be left in an inconsistent state. There's little way rack-timeout could be aware of ongoing state changes. Applications that rely on a set of globals (like class variables) or any other state that lives beyond a single request may find those left in an unexpected/inconsistent state after an aborted request. Some cleanup code might not have run, or only half of a set of related changes may have been applied.
|
17
|
+
|
18
|
+
A lot more can go wrong. An intricate explanation of the issue by JRuby's Charles Nutter can be found [here][broken-timeout].
|
19
|
+
|
20
|
+
Ruby 2.1 provides a way to defer the result of raising exceptions through the [Thread.handle_interrupt][handle-interrupt] method. This could be used in critical areas of your application code to prevent Rack::Timeout from accidentally wreaking havoc by raising just in the wrong moment. That said, `handle_interrupt` and threads in general are hard to reason about, and detecting all cases where it would be needed in an application is a tall order, and the added code complexity is probably not worth the trouble.
|
21
|
+
|
22
|
+
Your time is better spent ensuring requests run fast and don't need to timeout.
|
23
|
+
|
24
|
+
That said, it's something to be aware of, and may explain some eerie wonkiness seen in logs.
|
25
|
+
|
26
|
+
[broken-timeout]: http://headius.blogspot.de/2008/02/rubys-threadraise-threadkill-timeoutrb.html
|
27
|
+
[handle-interrupt]: http://www.ruby-doc.org/core-2.1.3/Thread.html#method-c-handle_interrupt
|
28
|
+
|
29
|
+
|
30
|
+
### Time Out Early and Often
|
31
|
+
|
32
|
+
Because of the aforementioned issues, it's recommended you set library-specific timeouts and leave Rack::Timeout as a last resort measure. Library timeouts will generally take care of IO issues and abort the operation safely. See [The Ultimate Guide to Ruby Timeouts][ruby-timeouts].
|
33
|
+
|
34
|
+
You'll want to set all relevant timeouts to something lower than Rack::Timeout's `service_timeout`. Generally you want them to be at least 1s lower, so as to account for time spent elsewhere during the request's lifetime while still giving libraries a chance to time out before Rack::Timeout.
|
35
|
+
|
36
|
+
[ruby-timeouts]: https://github.com/ankane/the-ultimate-guide-to-ruby-timeouts
|
data/doc/rollbar.md
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
### Rollbar
|
2
|
+
|
3
|
+
Because rack-timeout may raise at any point in the codepath of a timed-out request, the stack traces for similar requests may differ, causing rollbar to create separate entries for each timeout.
|
4
|
+
|
5
|
+
The recommended practice is to configure [Custom Fingerprints][rollbar-customfingerprint] on Rollbar.
|
6
|
+
|
7
|
+
[rollbar-customfingerprint]: https://docs.rollbar.com/docs/custom-grouping/
|
8
|
+
|
9
|
+
Example:
|
10
|
+
|
11
|
+
```json
|
12
|
+
[
|
13
|
+
{
|
14
|
+
"condition": {
|
15
|
+
"eq": "Rack::Timeout::RequestTimeoutException",
|
16
|
+
"path": "body.trace.exception.class"
|
17
|
+
},
|
18
|
+
"fingerprint": "Rack::Timeout::RequestTimeoutException {{context}}",
|
19
|
+
"title": "Rack::Timeout::RequestTimeoutException {{context}}"
|
20
|
+
}
|
21
|
+
]
|
22
|
+
|
23
|
+
```
|
24
|
+
|
25
|
+
This configuration will generate exceptions following the pattern: `Rack::Timeout::RequestTimeoutException controller#action
|
26
|
+
`
|
27
|
+
|
28
|
+
On previous versions this configuration was made using `Rack::Timeout::Rollbar` which was removed. [More details on the Issue #122][rollbar-removal-issue].
|
29
|
+
|
30
|
+
[rollbar-removal-issue]: https://github.com/heroku/rack-timeout/issues/122
|
data/doc/settings.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Settings
|
2
|
+
|
3
|
+
Rack::Timeout has 4 settings, each of which impacts when Rack::Timeout
|
4
|
+
will raise an exception, and which type of exception will be raised.
|
5
|
+
|
6
|
+
### Service Timeout
|
7
|
+
|
8
|
+
`service_timeout` is the most important setting.
|
9
|
+
|
10
|
+
*Service time* is the time taken from when a request first enters rack to when its response is sent back. When the application takes longer than `service_timeout` to process a request, the request's status is logged as `timed_out` and `Rack::Timeout::RequestTimeoutException` or `Rack::Timeout::RequestTimeoutError` is raised on the application thread. This may be automatically caught by the framework or plugins, so beware. Also, the exception is not guaranteed to be raised in a timely fashion, see section below about IO blocks.
|
11
|
+
|
12
|
+
Service timeout can be disabled entirely by setting the property to `0` or `false`, at which point the request skips Rack::Timeout's machinery (so no logging will be present).
|
13
|
+
|
14
|
+
### Wait Timeout
|
15
|
+
|
16
|
+
Before a request reaches the rack application, it may have spent some time being received by the web server, or waiting in the application server's queue before being dispatched to rack. The time between when a request is received by the web server and when rack starts handling it is called the *wait time*.
|
17
|
+
|
18
|
+
On Heroku, a request will be dropped when the routing layer sees no data being transferred for over 30 seconds. (You can read more about the specifics of Heroku routing's timeout [here][heroku-routing] and [here][heroku-timeout].) In this case, it makes no sense to process a request that reaches the application after having waited more than 30 seconds. That's where the `wait_timeout` setting comes in. When a request has a wait time greater than `wait_timeout`, it'll be dropped without ever being sent down to the application, and a `Rack::Timeout::RequestExpiryError` is raised. Such requests are logged as `expired`.
|
19
|
+
|
20
|
+
[heroku-routing]: https://devcenter.heroku.com/articles/http-routing#timeouts
|
21
|
+
[heroku-timeout]: https://devcenter.heroku.com/articles/request-timeout
|
22
|
+
|
23
|
+
`wait_timeout` is set at a default of 30 seconds, matching Heroku's router's timeout.
|
24
|
+
|
25
|
+
Wait timeout can be disabled entirely by setting the property to `0` or `false`.
|
26
|
+
|
27
|
+
A request's computed wait time may affect the service timeout used for it. Basically, a request's wait time plus service time may not exceed the wait timeout. The reasoning for that is based on Heroku router's behavior, that the request would be dropped anyway after the wait timeout. So, for example, with the default settings of `service_timeout=15`, `wait_timeout=30`, a request that had 20 seconds of wait time will not have a service timeout of 15, but instead of 10, as there are only 10 seconds left before `wait_timeout` is reached. This behavior can be disabled by setting `service_past_wait` to `true`. When set, the `service_timeout` setting will always be honored. Please note that if you're using the `RACK_TIMEOUT_SERVICE_PAST_WAIT` environment variable, any value different than `"false"` will be considered `true`.
|
28
|
+
|
29
|
+
The way we're able to infer a request's start time, and from that its wait time, is through the availability of the `X-Request-Start` HTTP header, which is expected to contain the time since epoch in milliseconds. (A concession is made for nginx's sec.msec notation.)
|
30
|
+
|
31
|
+
If the `X-Request-Start` header is not present `wait_timeout` handling is skipped entirely.
|
32
|
+
|
33
|
+
### Wait Overtime
|
34
|
+
|
35
|
+
Relying on `X-Request-Start` is less than ideal, as it computes the time since the request *started* being received by the web server, rather than the time the request *finished* being received by the web server. That poses a problem for lengthy requests.
|
36
|
+
|
37
|
+
Lengthy requests are requests with a body, such as POST requests. These take time to complete being received by the application server, especially when the client has a slow upload speed, as is common for example with mobile clients or asymmetric connections.
|
38
|
+
|
39
|
+
While we can infer the time since a request started being received, we can't tell when it completed being received, which would be preferable. We're also unable to tell the time since the last byte was sent in the request, which would be relevant in tracking Heroku's router timeout appropriately.
|
40
|
+
|
41
|
+
A request that took longer than 30s to be fully received, but that had been uploading data all that while, would be dropped immediately by Rack::Timeout because it'd be considered too old. Heroku's router, however, would not have dropped this request because data was being transmitted all along.
|
42
|
+
|
43
|
+
As a concession to these shortcomings, for requests that have a body present, we allow some additional wait time on top of `wait_timeout`. This aims to make up for time lost to long uploads.
|
44
|
+
|
45
|
+
This extra time is called *wait overtime* and can be set via `wait_overtime`. It defaults to 60 seconds. This can be disabled as usual by setting the property to `0` or `false`. When disabled, there's no overtime. If you want lengthy requests to never get expired, set `wait_overtime` to a very high number.
|
46
|
+
|
47
|
+
Keep in mind that Heroku [recommends][uploads] uploading large files directly to S3, so as to prevent the dyno from being blocked for too long and hence unable to handle further incoming requests.
|
48
|
+
|
49
|
+
[uploads]: https://devcenter.heroku.com/articles/s3#file-uploads
|
data/lib/rack/timeout/core.rb
CHANGED
@@ -49,7 +49,7 @@ module Rack
|
|
49
49
|
# helper methods to read timeout properties. Ensure they're always positive numbers or false. When set to false (or 0), their behaviour is disabled.
|
50
50
|
def read_timeout_property value, default
|
51
51
|
case value
|
52
|
-
when nil ; default
|
52
|
+
when nil ; read_timeout_property default, default
|
53
53
|
when false ; false
|
54
54
|
when 0 ; false
|
55
55
|
else
|
@@ -147,8 +147,9 @@ module Rack
|
|
147
147
|
# This is a code extraction for readability, this method is only called from a single point.
|
148
148
|
RX_NGINX_X_REQUEST_START = /^(?:t=)?(\d+)\.(\d{3})$/
|
149
149
|
RX_HEROKU_X_REQUEST_START = /^(\d+)$/
|
150
|
+
HTTP_X_REQUEST_START = "HTTP_X_REQUEST_START".freeze
|
150
151
|
def self._read_x_request_start(env)
|
151
|
-
return unless s = env[
|
152
|
+
return unless s = env[HTTP_X_REQUEST_START]
|
152
153
|
return unless m = s.match(RX_HEROKU_X_REQUEST_START) || s.match(RX_NGINX_X_REQUEST_START)
|
153
154
|
Time.at(m[1,2].join.to_f / 1000)
|
154
155
|
end
|
data/test/basic_test.rb
CHANGED
@@ -1,32 +1,23 @@
|
|
1
|
-
require "
|
2
|
-
require "rack/test"
|
3
|
-
require "rack-timeout"
|
4
|
-
|
5
|
-
class BasicTest < Test::Unit::TestCase
|
6
|
-
include Rack::Test::Methods
|
7
|
-
|
8
|
-
def app
|
9
|
-
Rack::Builder.new do
|
10
|
-
use Rack::Timeout, service_timeout: 1
|
11
|
-
|
12
|
-
map "/" do
|
13
|
-
run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] }
|
14
|
-
end
|
15
|
-
|
16
|
-
map "/sleep" do
|
17
|
-
run lambda { |env| sleep }
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
1
|
+
require "test_helper"
|
21
2
|
|
3
|
+
class BasicTest < RackTimeoutTest
|
22
4
|
def test_ok
|
5
|
+
self.settings = { service_timeout: 1 }
|
23
6
|
get "/"
|
24
7
|
assert last_response.ok?
|
25
8
|
end
|
26
9
|
|
27
10
|
def test_timeout
|
11
|
+
self.settings = { service_timeout: 1 }
|
28
12
|
assert_raises(Rack::Timeout::RequestTimeoutError) do
|
29
13
|
get "/sleep"
|
30
14
|
end
|
31
15
|
end
|
16
|
+
|
17
|
+
def test_wait_timeout
|
18
|
+
self.settings = { service_timeout: 1, wait_timeout: 15 }
|
19
|
+
assert_raises(Rack::Timeout::RequestExpiryError) do
|
20
|
+
get "/", "", 'HTTP_X_REQUEST_START' => time_in_msec(Time.now - 100)
|
21
|
+
end
|
22
|
+
end
|
32
23
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class EnvSettingsTest < RackTimeoutTest
|
4
|
+
|
5
|
+
def test_service_timeout
|
6
|
+
with_env(RACK_TIMEOUT_SERVICE_TIMEOUT: 1) do
|
7
|
+
assert_raises(Rack::Timeout::RequestTimeoutError) do
|
8
|
+
get "/sleep"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_zero_wait_timeout
|
14
|
+
with_env(RACK_TIMEOUT_WAIT_TIMEOUT: 0) do
|
15
|
+
get "/", "", 'HTTP_X_REQUEST_START' => time_in_msec(Time.now - 100)
|
16
|
+
assert last_response.ok?
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require "test/unit"
|
2
|
+
require "rack/test"
|
3
|
+
require "rack-timeout"
|
4
|
+
|
5
|
+
class RackTimeoutTest < Test::Unit::TestCase
|
6
|
+
include Rack::Test::Methods
|
7
|
+
|
8
|
+
attr_accessor :settings
|
9
|
+
|
10
|
+
def initialize(*args)
|
11
|
+
self.settings ||= {}
|
12
|
+
super(*args)
|
13
|
+
end
|
14
|
+
|
15
|
+
def app
|
16
|
+
settings = self.settings
|
17
|
+
Rack::Builder.new do
|
18
|
+
use Rack::Timeout, settings
|
19
|
+
|
20
|
+
map "/" do
|
21
|
+
run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] }
|
22
|
+
end
|
23
|
+
|
24
|
+
map "/sleep" do
|
25
|
+
run lambda { |env| sleep }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# runs the test with the given environment, but doesnt restore the original
|
31
|
+
# environment afterwards. This should be sufficient for rack-timeout testing.
|
32
|
+
def with_env(hash)
|
33
|
+
hash.each_pair do |k, v|
|
34
|
+
ENV[k.to_s] = v.to_s
|
35
|
+
end
|
36
|
+
yield
|
37
|
+
hash.each_key do |k|
|
38
|
+
ENV[k.to_s] = nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def time_in_msec(t = Time.now)
|
43
|
+
"#{t.tv_sec}#{t.tv_usec/1000}"
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-timeout
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.
|
4
|
+
version: 0.5.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Caio Chassot
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-05-
|
11
|
+
date: 2018-05-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -59,11 +59,16 @@ executables: []
|
|
59
59
|
extensions: []
|
60
60
|
extra_rdoc_files: []
|
61
61
|
files:
|
62
|
-
- CHANGELOG
|
63
62
|
- Gemfile
|
64
63
|
- MIT-LICENSE
|
65
|
-
- README.markdown
|
66
64
|
- Rakefile
|
65
|
+
- doc/exceptions.md
|
66
|
+
- doc/logging.md
|
67
|
+
- doc/observers.md
|
68
|
+
- doc/request-lifecycle.md
|
69
|
+
- doc/risks.md
|
70
|
+
- doc/rollbar.md
|
71
|
+
- doc/settings.md
|
67
72
|
- lib/rack-timeout.rb
|
68
73
|
- lib/rack/timeout/base.rb
|
69
74
|
- lib/rack/timeout/core.rb
|
@@ -76,6 +81,8 @@ files:
|
|
76
81
|
- lib/rack/timeout/support/scheduler.rb
|
77
82
|
- lib/rack/timeout/support/timeout.rb
|
78
83
|
- test/basic_test.rb
|
84
|
+
- test/env_settings_test.rb
|
85
|
+
- test/test_helper.rb
|
79
86
|
homepage: http://github.com/heroku/rack-timeout
|
80
87
|
licenses:
|
81
88
|
- MIT
|
@@ -102,5 +109,7 @@ specification_version: 4
|
|
102
109
|
summary: Abort requests that are taking too long
|
103
110
|
test_files:
|
104
111
|
- test/basic_test.rb
|
112
|
+
- test/env_settings_test.rb
|
113
|
+
- test/test_helper.rb
|
105
114
|
- Gemfile
|
106
115
|
- Rakefile
|
data/CHANGELOG
DELETED
@@ -1,103 +0,0 @@
|
|
1
|
-
0.5.0.1
|
2
|
-
=======
|
3
|
-
- Fix 0600 permissions in gem pushed to rubygems
|
4
|
-
|
5
|
-
0.5.0
|
6
|
-
=====
|
7
|
-
|
8
|
-
Breaking Changes
|
9
|
-
|
10
|
-
- Remove Rollbar module (#124)
|
11
|
-
- Remove legacy class setters (#125)
|
12
|
-
|
13
|
-
Other
|
14
|
-
|
15
|
-
- Add support to configure via environment variables (#105)
|
16
|
-
- Adds support for ActionDispatch::RequestId generated request ids (#115)
|
17
|
-
- Changes uuid format to proper uuid (#115)
|
18
|
-
|
19
|
-
0.4.2
|
20
|
-
=====
|
21
|
-
- Ruby 2.0 compatible
|
22
|
-
|
23
|
-
0.4.1
|
24
|
-
=====
|
25
|
-
- Rails 5 support
|
26
|
-
- Remove deprecation warning on timeout setter for Rails apps
|
27
|
-
|
28
|
-
0.4.0
|
29
|
-
=====
|
30
|
-
- Using monotonic time instead of Time.now where available (/ht concurrent-ruby)
|
31
|
-
- Settings are now passable to the middleware initializer instead of class-level
|
32
|
-
- Rollbar module may take a custom fingerprint block
|
33
|
-
- Rollbar module considered final
|
34
|
-
- Fixed an issue where some heartbeats would live on forever (#103, /ht @0x0badc0de)
|
35
|
-
|
36
|
-
0.3.2
|
37
|
-
=====
|
38
|
-
- Fixes calling timeout with a value of 0 (issue #90)
|
39
|
-
|
40
|
-
0.3.1
|
41
|
-
=====
|
42
|
-
- Rollbar module improvements
|
43
|
-
|
44
|
-
0.3.0
|
45
|
-
=====
|
46
|
-
- use a single scheduler thread to manage timeouts, instead of one timeout thread per request
|
47
|
-
- instead of inserting middleware at position 0 for rails, insert before Rack::Runtime (which is right after Rack::Lock and the static file stuff)
|
48
|
-
- reshuffle error types: RequestExpiryError is again a RuntimeError, and timeouts raise a RequestTimeoutException, an Exception, and not descending from Rack::Timeout::Error (see README for more)
|
49
|
-
- don't insert middleware for rails in test environment
|
50
|
-
- add convenience module Rack::Timeout::Logger (see README for more)
|
51
|
-
- StageChangeLoggingObserver renamed to StateChangeLoggingObserver, works slightly differently too
|
52
|
-
- file layout reorganization (see 6e82c276 for details)
|
53
|
-
- CHANGELOG file is now in the gem (@dbackeus)
|
54
|
-
- add optional and experimental support for grouping errors by url under rollbar. see "rack/timeout/rollbar" for usage
|
55
|
-
|
56
|
-
0.2.4
|
57
|
-
=====
|
58
|
-
- Previous fix was borked.
|
59
|
-
|
60
|
-
0.2.3
|
61
|
-
=====
|
62
|
-
- Ignore Rack::NullLogger when picking a logger
|
63
|
-
|
64
|
-
0.2.1
|
65
|
-
=====
|
66
|
-
- Fix raised error messages
|
67
|
-
|
68
|
-
0.2.0
|
69
|
-
=====
|
70
|
-
- Added CHANGELOG
|
71
|
-
- Rack::Timeout::Error now inherits from Exception instead of StandardError, with the hope users won't rescue from it accidentally
|
72
|
-
|
73
|
-
0.1.2
|
74
|
-
=====
|
75
|
-
- improve RequestTimeoutError error string so @watsonian is happy
|
76
|
-
|
77
|
-
0.1.1
|
78
|
-
=====
|
79
|
-
- README updates
|
80
|
-
- fix that setting properties to false resulted in an error
|
81
|
-
|
82
|
-
0.1.0
|
83
|
-
=====
|
84
|
-
- Rewrote README
|
85
|
-
|
86
|
-
0.1.0beta4
|
87
|
-
==========
|
88
|
-
- Renamed `timeout` setting to `service_timeout`; `timeout=` still works for backwards compatibility
|
89
|
-
– `MAX_REQUEST_AGE` is gone, the `wait_timeout` setting more or less replaces it
|
90
|
-
- Renamed `overtime` setting to `wait_overtime`
|
91
|
-
- overtime setting should actually work (It had never made it to beta3)
|
92
|
-
- In the request info struct, renamed `age` to `wait`, `duration` to `service`
|
93
|
-
- Rack::Timeout::StageChangeLogger is gone, replaced by Rack::Timeout::StageChangeLoggingObserver, which is an observer class that composites with a logger, instead of inheriting from Logger. Anything logging related will likely be incompatible with previous beta release.
|
94
|
-
- Log level can no longer be set with env vars, has to be set in the logger being used. (Which can now be custom / user-provided.)
|
95
|
-
|
96
|
-
0.1.0beta1,2,3
|
97
|
-
==============
|
98
|
-
- Dropped ruby 1.8.x support
|
99
|
-
- Dropped rails 2 support
|
100
|
-
- Added rails 4 support
|
101
|
-
- Added much logging
|
102
|
-
– Added support for dropping requests that waited too long in the queue without ever handling them
|
103
|
-
- Other things I can't remember, see git logs :P
|
data/README.markdown
DELETED
@@ -1,317 +0,0 @@
|
|
1
|
-
Rack::Timeout
|
2
|
-
=============
|
3
|
-
|
4
|
-
Abort requests that are taking too long; an exception is raised.
|
5
|
-
|
6
|
-
A generous timeout of 15s is the default. It's recommended to set the timeout as low as realistically viable for your application. Most applications will do fine with a setting between 2 and 5 seconds.
|
7
|
-
|
8
|
-
There's a handful of other settings, read on for details.
|
9
|
-
|
10
|
-
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.
|
11
|
-
|
12
|
-
|
13
|
-
Basic Usage
|
14
|
-
-----------
|
15
|
-
|
16
|
-
The following covers currently supported versions of Rails, Rack, Ruby, and Bundler. See the Compatibility section at the end for legacy versions.
|
17
|
-
|
18
|
-
### Rails apps
|
19
|
-
|
20
|
-
```ruby
|
21
|
-
# Gemfile
|
22
|
-
gem "rack-timeout"
|
23
|
-
```
|
24
|
-
|
25
|
-
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.
|
26
|
-
|
27
|
-
### Rails apps, manually
|
28
|
-
|
29
|
-
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.
|
30
|
-
|
31
|
-
```ruby
|
32
|
-
# Gemfile
|
33
|
-
gem "rack-timeout", require:"rack/timeout/base"
|
34
|
-
```
|
35
|
-
|
36
|
-
```ruby
|
37
|
-
# config/initializers/rack_timeout.rb
|
38
|
-
|
39
|
-
# insert middleware wherever you want in the stack, optionally pass initialization arguments or use environment variables
|
40
|
-
Rails.application.config.middleware.insert_before Rack::Runtime, Rack::Timeout, service_timeout: 5
|
41
|
-
```
|
42
|
-
|
43
|
-
### Sinatra and other Rack apps
|
44
|
-
|
45
|
-
```ruby
|
46
|
-
# config.ru
|
47
|
-
require "rack-timeout"
|
48
|
-
|
49
|
-
# Call as early as possible so rack-timeout runs before all other middleware.
|
50
|
-
# Setting service_timeout or `RACK_TIMEOUT_SERVICE_TIMEOUT` environment variable is recommended. If omitted, defaults to 15 seconds.
|
51
|
-
use Rack::Timeout, service_timeout: 5
|
52
|
-
```
|
53
|
-
|
54
|
-
|
55
|
-
The Rabbit Hole
|
56
|
-
---------------
|
57
|
-
|
58
|
-
Rack::Timeout takes the following settings, shown here with their default values:
|
59
|
-
|
60
|
-
```
|
61
|
-
service_timeout: 15 # RACK_TIMEOUT_SERVICE_TIMEOUT
|
62
|
-
wait_timeout: 30 # RACK_TIMEOUT_WAIT_TIMEOUT
|
63
|
-
wait_overtime: 60 # RACK_TIMEOUT_WAIT_OVERTIME
|
64
|
-
service_past_wait: false # RACK_TIMEOUT_SERVICE_PAST_WAIT
|
65
|
-
```
|
66
|
-
|
67
|
-
As shown earlier, these settings can be overriden during middleware initialization or environment variables `RACK_TIMEOUT_*` mentioned above. Middleware parameters take precedence:
|
68
|
-
|
69
|
-
```ruby
|
70
|
-
use Rack::Timeout, service_timeout: 5, wait_timeout: false
|
71
|
-
```
|
72
|
-
|
73
|
-
### Service Timeout
|
74
|
-
|
75
|
-
`service_timeout` is our most important setting.
|
76
|
-
|
77
|
-
*Service time* is the time taken from when a request first enters rack to when its response is sent back. When the application takes longer than `service_timeout` to process a request, the request's status is logged as `timed_out` and `Rack::Timeout::RequestTimeoutException` or `Rack::Timeout::RequestTimeoutError` is raised on the application thread. This may be automatically caught by the framework or plugins, so beware. Also, the exception is not guaranteed to be raised in a timely fashion, see section below about IO blocks.
|
78
|
-
|
79
|
-
Service timeout can be disabled entirely by setting the property to `0` or `false`, at which point the request skips Rack::Timeout's machinery (so no logging will be present).
|
80
|
-
|
81
|
-
|
82
|
-
### Wait Timeout
|
83
|
-
|
84
|
-
Before a request reaches the rack application, it may have spent some time being received by the web server, or waiting in the application server's queue before being dispatched to rack. The time between when a request is received by the web server and when rack starts handling it is called the *wait time*.
|
85
|
-
|
86
|
-
On Heroku, a request will be dropped when the routing layer sees no data being transferred for over 30 seconds. (You can read more about the specifics of Heroku routing's timeout [here][heroku-routing] and [here][heroku-timeout].) In this case, it makes no sense to process a request that reaches the application after having waited more than 30 seconds. That's where the `wait_timeout` setting comes in. When a request has a wait time greater than `wait_timeout`, it'll be dropped without ever being sent down to the application, and a `Rack::Timeout::RequestExpiryError` is raised. Such requests are logged as `expired`.
|
87
|
-
|
88
|
-
[heroku-routing]: https://devcenter.heroku.com/articles/http-routing#timeouts
|
89
|
-
[heroku-timeout]: https://devcenter.heroku.com/articles/request-timeout
|
90
|
-
|
91
|
-
`wait_timeout` is set at a default of 30 seconds, matching Heroku's router's timeout.
|
92
|
-
|
93
|
-
Wait timeout can be disabled entirely by setting the property to `0` or `false`.
|
94
|
-
|
95
|
-
A request's computed wait time may affect the service timeout used for it. Basically, a request's wait time plus service time may not exceed the wait timeout. The reasoning for that is based on Heroku router's behavior, that the request would be dropped anyway after the wait timeout. So, for example, with the default settings of `service_timeout=15`, `wait_timeout=30`, a request that had 20 seconds of wait time will not have a service timeout of 15, but instead of 10, as there are only 10 seconds left before `wait_timeout` is reached. This behavior can be disabled by setting `service_past_wait` to `true`. When set, the `service_timeout` setting will always be honored. Please note that if you're using the `RACK_TIMEOUT_SERVICE_PAST_WAIT` environment variable, any value different than `"false"` will be considered `true`.
|
96
|
-
|
97
|
-
The way we're able to infer a request's start time, and from that its wait time, is through the availability of the `X-Request-Start` HTTP header, which is expected to contain the time since epoch in milliseconds. (A concession is made for nginx's sec.msec notation.)
|
98
|
-
|
99
|
-
If the `X-Request-Start` header is not present `wait_timeout` handling is skipped entirely.
|
100
|
-
|
101
|
-
|
102
|
-
### Wait Overtime
|
103
|
-
|
104
|
-
Relying on `X-Request-Start` is less than ideal, as it computes the time since the request *started* being received by the web server, rather than the time the request *finished* being received by the web server. That poses a problem for lengthy requests.
|
105
|
-
|
106
|
-
Lengthy requests are requests with a body, such as POST requests. These take time to complete being received by the application server, especially when the client has a slow upload speed, as is common for example with mobile clients or asymmetric connections.
|
107
|
-
|
108
|
-
While we can infer the time since a request started being received, we can't tell when it completed being received, which would be preferable. We're also unable to tell the time since the last byte was sent in the request, which would be relevant in tracking Heroku's router timeout appropriately.
|
109
|
-
|
110
|
-
A request that took longer than 30s to be fully received, but that had been uploading data all that while, would be dropped immediately by Rack::Timeout because it'd be considered too old. Heroku's router, however, would not have dropped this request because data was being transmitted all along.
|
111
|
-
|
112
|
-
As a concession to these shortcomings, for requests that have a body present, we allow some additional wait time on top of `wait_timeout`. This aims to make up for time lost to long uploads.
|
113
|
-
|
114
|
-
This extra time is called *wait overtime* and can be set via `wait_overtime`. It defaults to 60 seconds. This can be disabled as usual by setting the property to `0` or `false`. When disabled, there's no overtime. If you want lengthy requests to never get expired, set `wait_overtime` to a very high number.
|
115
|
-
|
116
|
-
Keep in mind that Heroku [recommends][uploads] uploading large files directly to S3, so as to prevent the dyno from being blocked for too long and hence unable to handle further incoming requests.
|
117
|
-
|
118
|
-
[uploads]: https://devcenter.heroku.com/articles/s3#file-uploads
|
119
|
-
|
120
|
-
|
121
|
-
### Timing Out During IO Blocks
|
122
|
-
|
123
|
-
Sometimes a request is taking too long to complete because it's blocked waiting on synchronous IO. Such IO does not need to be file operations, it could be, say, network or database operations. If said IO is happening in a C library that's unaware of ruby's interrupt system (i.e. anything written without ruby in mind), calling `Thread#raise` (that's what rack-timeout uses) will not have effect until after the IO block is gone.
|
124
|
-
|
125
|
-
At the moment rack-timeout does not try to address this issue. As a fail-safe against these cases, a blunter solution that kills the entire process is recommended, such as unicorn's timeouts.
|
126
|
-
|
127
|
-
More detailed explanations of the issues surrounding timing out in ruby during IO blocks can be found at:
|
128
|
-
|
129
|
-
- http://redgetan.cc/understanding-timeouts-in-cruby/
|
130
|
-
|
131
|
-
### Timing Out Inherently Unsafe
|
132
|
-
|
133
|
-
Raising mid-flight in stateful applications is inherently unsafe. A request can be aborted at any moment in the code flow, and the application can be left in an inconsistent state. There's little way rack-timeout could be aware of ongoing state changes. Applications that rely on a set of globals (like class variables) or any other state that lives beyond a single request may find those left in an unexpected/inconsistent state after an aborted request. Some cleanup code might not have run, or only half of a set of related changes may have been applied.
|
134
|
-
|
135
|
-
A lot more can go wrong. An intricate explanation of the issue by JRuby's Charles Nutter can be found [here][broken-timeout].
|
136
|
-
|
137
|
-
Ruby 2.1 provides a way to defer the result of raising exceptions through the [Thread.handle_interrupt][handle-interrupt] method. This could be used in critical areas of your application code to prevent Rack::Timeout from accidentally wreaking havoc by raising just in the wrong moment. That said, `handle_interrupt` and threads in general are hard to reason about, and detecting all cases where it would be needed in an application is a tall order, and the added code complexity is probably not worth the trouble.
|
138
|
-
|
139
|
-
Your time is better spent ensuring requests run fast and don't need to timeout.
|
140
|
-
|
141
|
-
That said, it's something to be aware of, and may explain some eerie wonkiness seen in logs.
|
142
|
-
|
143
|
-
[broken-timeout]: http://headius.blogspot.de/2008/02/rubys-threadraise-threadkill-timeoutrb.html
|
144
|
-
[handle-interrupt]: http://www.ruby-doc.org/core-2.1.3/Thread.html#method-c-handle_interrupt
|
145
|
-
|
146
|
-
|
147
|
-
### Time Out Early and Often
|
148
|
-
|
149
|
-
Because of the aforementioned issues, it's recommended you set library-specific timeouts and leave Rack::Timeout as a last resort measure. Library timeouts will generally take care of IO issues and abort the operation safely. See [The Ultimate Guide to Ruby Timeouts][ruby-timeouts].
|
150
|
-
|
151
|
-
You'll want to set all relevant timeouts to something lower than Rack::Timeout's `service_timeout`. Generally you want them to be at least 1s lower, so as to account for time spent elsewhere during the request's lifetime while still giving libraries a chance to time out before Rack::Timeout.
|
152
|
-
|
153
|
-
[ruby-timeouts]: https://github.com/ankane/the-ultimate-guide-to-ruby-timeouts
|
154
|
-
|
155
|
-
|
156
|
-
Request Lifetime
|
157
|
-
----------------
|
158
|
-
|
159
|
-
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"]`.
|
160
|
-
|
161
|
-
The value of that entry is an instance of `Rack::Timeout::RequestDetails`, which is a `Struct` consisting of the following fields:
|
162
|
-
|
163
|
-
* `id`: a unique ID per request. Either the value of the `X-Request-ID` header or a random ID
|
164
|
-
generated internally.
|
165
|
-
|
166
|
-
* `wait`: time in seconds since `X-Request-Start` at the time the request was initially seen by Rack::Timeout. Only set if `X-Request-Start` is present.
|
167
|
-
|
168
|
-
* `timeout`: the final timeout value that was used or to be used, in seconds. For `expired` requests, that would be the `wait_timeout`, possibly with `wait_overtime` applied. In all other cases it's the `service_timeout`, potentially reduced to make up for time lost waiting. (See discussion regarding `service_past_wait` above, under the Wait Timeout section.)
|
169
|
-
|
170
|
-
* `service`: set after a request completes (or times out). The time in seconds it took being processed. This is also updated while a request is still active, around every second, with the time taken so far.
|
171
|
-
|
172
|
-
* `state`: the possible states, and their log level, are:
|
173
|
-
|
174
|
-
* `expired` (`ERROR`): the request is considered too old and is skipped entirely. This happens when `X-Request-Start` is present and older than `wait_timeout`. When in this state, `Rack::Timeout::RequestExpiryError` is raised. See earlier discussion about the `wait_overtime` setting, too.
|
175
|
-
|
176
|
-
* `ready` (`INFO`): this is the state a request is in right before it's passed down the middleware chain. Once it's being processed, it'll move on to `active`, and then on to `timed_out` and/or `completed`.
|
177
|
-
|
178
|
-
* `active` (`DEBUG`): the request is being actively processed in the application thread. This is signaled repeatedly every ~1s until the request completes or times out.
|
179
|
-
|
180
|
-
* `timed_out` (`ERROR`): the request ran for longer than the determined timeout and was aborted. `Rack::Timeout::RequestTimeoutException` is raised in the application when this occurs. This state is not the final one, `completed` will be set after the framework is done with it. (If the exception does bubble up, it's caught by rack-timeout and re-raised as `Rack::Timeout::RequestTimeoutError`, which descends from RuntimeError.)
|
181
|
-
|
182
|
-
* `completed` (`INFO`): the request completed and Rack::Timeout is done with it. This does not mean the request completed *successfully*. Rack::Timeout does not concern itself with that. As mentioned just above, a timed out request will still end up with a `completed` state.
|
183
|
-
|
184
|
-
|
185
|
-
Errors
|
186
|
-
------
|
187
|
-
|
188
|
-
Rack::Timeout can raise three types of exceptions. They are:
|
189
|
-
|
190
|
-
Two descend from `Rack::Timeout::Error`, which itself descends from `RuntimeError` and is generally caught by an unqualified `rescue`. The third, `RequestTimeoutException`, is more complicated and the most important.
|
191
|
-
|
192
|
-
* `Rack::Timeout::RequestTimeoutException`: this is raised when a request has run for longer than the specified timeout. This descends from `Exception`, not from `Rack::Timeout::Error` (it has to be rescued from explicitly). It's raised by the rack-timeout timer thread in the application thread, at the point in the stack the app happens to be in when the timeout is triggered. This exception could be caught explicitly within the application, but in doing so you're working past the timeout. This is ok for quick cleanup work but shouldn't be abused as Rack::Timeout will not kick in twice for the same request.
|
193
|
-
|
194
|
-
Rails will generally intercept `Exception`s, but in plain Rack apps, this exception will be caught by rack-timeout and re-raised as a `Rack::Timeout::RequestTimeoutError`. This is to prevent an `Exception` from bubbling up beyond rack-timeout and to the server.
|
195
|
-
|
196
|
-
* `Rack::Timeout::RequestTimeoutError` descends from `Rack::Timeout::Error`, but it's only really seen in the case described above. It'll not be seen in a standard Rails app, and will only be seen in Sinatra if rescuing from exceptions is disabled.
|
197
|
-
|
198
|
-
* `Rack::Timeout::RequestExpiryError`: this is raised when a request is skipped for being too old (see Wait Timeout section). This error cannot generally be rescued from inside a Rails controller action as it happens before the request has a chance to enter Rails.
|
199
|
-
|
200
|
-
This shouldn't be different for other frameworks, unless you have something above Rack::Timeout in the middleware stack, which you generally shouldn't.
|
201
|
-
|
202
|
-
You shouldn't rescue from these errors for reporting purposes. Instead, you can subscribe for state change notifications with observers.
|
203
|
-
|
204
|
-
If you're trying to test that a `Rack::Timeout::RequestTimeoutException` is 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.
|
205
|
-
|
206
|
-
[More details about testing middleware with Rails here][pablobm].
|
207
|
-
|
208
|
-
[pablobm]: http://stackoverflow.com/a/8681208/13989
|
209
|
-
|
210
|
-
|
211
|
-
### Rollbar
|
212
|
-
|
213
|
-
Because rack-timeout may raise at any point in the codepath of a timed-out request, the stack traces for similar requests may differ, causing rollbar to create separate entries for each timeout.
|
214
|
-
|
215
|
-
The recommended practice is to configure [Custom Fingerprints][rollbar-customfingerprint] on Rollbar.
|
216
|
-
|
217
|
-
[rollbar-customfingerprint]: https://docs.rollbar.com/docs/custom-grouping/
|
218
|
-
|
219
|
-
Example:
|
220
|
-
|
221
|
-
```json
|
222
|
-
[
|
223
|
-
{
|
224
|
-
"condition": {
|
225
|
-
"eq": "Rack::Timeout::RequestTimeoutException",
|
226
|
-
"path": "body.trace.exception.class"
|
227
|
-
},
|
228
|
-
"fingerprint": "Rack::Timeout::RequestTimeoutException {{context}}",
|
229
|
-
"title": "Rack::Timeout::RequestTimeoutException {{context}}"
|
230
|
-
}
|
231
|
-
]
|
232
|
-
|
233
|
-
```
|
234
|
-
|
235
|
-
This configuration will generate exceptions following the pattern: `Rack::Timeout::RequestTimeoutException controller#action
|
236
|
-
`
|
237
|
-
|
238
|
-
On previous versions this configuration was made using `Rack::Timeout::Rollbar` which was removed. [More details on the Issue #122][rollbar-removal-issue].
|
239
|
-
|
240
|
-
[rollbar-removal-issue]: https://github.com/heroku/rack-timeout/issues/122
|
241
|
-
|
242
|
-
Observers
|
243
|
-
---------
|
244
|
-
|
245
|
-
Observers are blocks that are notified about state changes during a request's lifetime. Keep in mind that the `active` state is set every ~1s, so you'll be notified every time.
|
246
|
-
|
247
|
-
You can register an observer with:
|
248
|
-
|
249
|
-
```ruby
|
250
|
-
Rack::Timeout.register_state_change_observer(:a_unique_name) { |env| do_things env }
|
251
|
-
```
|
252
|
-
|
253
|
-
There's currently no way to subscribe to changes into or out of a particular state. To check the actual state we're moving into, read `env['rack-timeout.info'].state`. Handling going out of a state would require some additional logic in the observer.
|
254
|
-
|
255
|
-
You can remove an observer with `unregister_state_change_observer`:
|
256
|
-
|
257
|
-
```ruby
|
258
|
-
Rack::Timeout.unregister_state_change_observer(:a_unique_name)
|
259
|
-
```
|
260
|
-
|
261
|
-
rack-timeout's logging is implemented using an observer; see `Rack::Timeout::StateChangeLoggingObserver` in logging-observer.rb for the implementation.
|
262
|
-
|
263
|
-
Custom observers might be used to do cleanup, store statistics on request length, timeouts, etc., and potentially do performance tuning on the fly.
|
264
|
-
|
265
|
-
|
266
|
-
Logging
|
267
|
-
-------
|
268
|
-
|
269
|
-
Rack::Timeout logs a line every time there's a change in state in a request's lifetime.
|
270
|
-
|
271
|
-
Request state changes into `timed_out` and `expired` are logged at the `ERROR` level, most other things are logged as `INFO`. The `active` state is logged as `DEBUG`, every ~1s while the request is still active.
|
272
|
-
|
273
|
-
Rack::Timeout will try to use `Rails.logger` if present, otherwise it'll look for a logger in `env['rack.logger']`, and if neither are present, it'll create its own logger, either writing to `env['rack.errors']`, or to `$stderr` if the former is not set.
|
274
|
-
|
275
|
-
When creating its own logger, rack-timeout will use a log level of `INFO`. Otherwise whatever log level is already set on the logger being used continues in effect.
|
276
|
-
|
277
|
-
A custom logger can be set via `Rack::Timeout::Logger.logger`. This takes priority over the automatic logger detection:
|
278
|
-
|
279
|
-
```ruby
|
280
|
-
Rack::Timeout::Logger.logger = Logger.new
|
281
|
-
```
|
282
|
-
|
283
|
-
There are helper setters that replace the logger:
|
284
|
-
|
285
|
-
```ruby
|
286
|
-
Rack::Timeout::Logger.device = $stderr
|
287
|
-
Rack::Timeout::Logger.level = Logger::INFO
|
288
|
-
```
|
289
|
-
|
290
|
-
Although each call replaces the logger, these can be use together and the final logger will retain both properties. (If only one is called, the defaults used above apply.)
|
291
|
-
|
292
|
-
Logging is enabled by default, but can be removed with:
|
293
|
-
|
294
|
-
```ruby
|
295
|
-
Rack::Timeout::Logger.disable
|
296
|
-
```
|
297
|
-
|
298
|
-
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 `wait`, `timeout`, and `service` are stored internally as seconds, they are logged as milliseconds for readability.
|
299
|
-
|
300
|
-
A sample log excerpt might look like:
|
301
|
-
|
302
|
-
```
|
303
|
-
source=rack-timeout id=13793c wait=369ms timeout=10000ms state=ready at=info
|
304
|
-
source=rack-timeout id=13793c wait=369ms timeout=10000ms service=15ms state=completed at=info
|
305
|
-
source=rack-timeout id=ea7bd3 wait=371ms timeout=10000ms state=timed_out at=error
|
306
|
-
```
|
307
|
-
|
308
|
-
|
309
|
-
Compatibility
|
310
|
-
-------------
|
311
|
-
|
312
|
-
This version of Rack::Timeout is compatible with Ruby 2.1 and up, and, for Rails apps, Rails 3.x and up.
|
313
|
-
|
314
|
-
|
315
|
-
---
|
316
|
-
Copyright © 2010-2016 Caio Chassot, released under the MIT license
|
317
|
-
<http://github.com/heroku/rack-timeout>
|