rack-timeout 0.1.0beta2 → 0.1.0beta3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +6 -14
- data/README.markdown +73 -43
- data/lib/rack-timeout.rb +0 -1
- data/lib/rack/timeout.rb +31 -43
- data/lib/rack/timeout/logger.rb +1 -0
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,15 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
metadata.gz: !binary |-
|
9
|
-
YTVlYTViYjc0ZDdlYzBiYjNhYzJiOTc4ZTQ4OTVlYjNkOWM5MTFmMmRmNzIw
|
10
|
-
MTM0OWI3ZWVmODRiY2M3ZDAwMGY3OGViNmJkMTZiYzlkYzI0ZTJhYThhYTEy
|
11
|
-
MWJlNTUzZTZjYzc2Y2ViMThlMjJlMDdiMTM5Mzg0MzNjMzM0ZTc=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
ZTc3Yzk3MmE4YWE0NjQwNjliNjNmZmY1Zjc0ZTdjODYwYThhYmJjNjI0NTRh
|
14
|
-
ZTk5ZGU0YjE1YmQ3ZWY0YTg4N2I1MWUwOGExYTk4M2Q1ZGI0NWRmM2YxMjAw
|
15
|
-
MjFiMzJlOTQ0NjVkYWZmMWRhYzMzN2UwOGRmYjkwZDc0MTc0MjA=
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d8cadc6366c07e2e809ab55bbcbc66ea82e387cd
|
4
|
+
data.tar.gz: 600c25828d8e91b1556cd4b458833e109c02788d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a4f2117b12ddac87a0398622924219d12eaf84ec11fbcbb55e31f8fa9163eb579a1cb4c7a61d6ec3c0011de3699cf83a1688c80d40de173d1235294fa4b83184
|
7
|
+
data.tar.gz: b54a7b0085fbefa6ecad6e2438b7ec6c99ba3c907a806ef34b8b6e2238b442dd9050e92a47aa9e738138467c081f0965419f085950570d6316bd8fa3d6aa4959
|
data/README.markdown
CHANGED
@@ -1,14 +1,20 @@
|
|
1
|
+
README is not entirely in sync with this release. E.g. the overtime stuff is not present in this
|
2
|
+
release. There may be other discrepancies.
|
3
|
+
|
1
4
|
Rack::Timeout
|
2
5
|
=============
|
3
6
|
|
4
|
-
Abort requests that are taking too long; a `Rack::Timeout::Error`
|
7
|
+
Abort requests that are taking too long; a subclass of `Rack::Timeout::Error` is raised.
|
8
|
+
|
9
|
+
A generous timeout of 15s is the default. It's recommended to set the timeout as low as
|
10
|
+
realistically viable for your application.
|
5
11
|
|
6
12
|
|
7
13
|
Usage
|
8
14
|
-----
|
9
15
|
|
10
|
-
|
11
|
-
end for legacy versions.
|
16
|
+
The following covers currently supported versions of Rails, Rack, Ruby, and Bundler. See the
|
17
|
+
Compatibility section at the end for legacy versions.
|
12
18
|
|
13
19
|
### Rails apps
|
14
20
|
|
@@ -33,27 +39,42 @@ Heroku Niceties
|
|
33
39
|
---------------
|
34
40
|
|
35
41
|
* Normally, Rack::Timeout always times out a request using the `Rack::Timeout.timeout` setting.
|
36
|
-
Heroku
|
37
|
-
indicating the time the request first enters the routing infrastructure.
|
42
|
+
Heroku makes available the [`X-Request-Start`][X-Request-Start] HTTP header, which is a
|
43
|
+
timestamp indicating the time the request first enters the routing infrastructure.
|
38
44
|
|
39
45
|
If the `X-Request-Start` HTTP header is present, Rack::Timeout will take the age of the request
|
40
46
|
into consideration when determining the timeout to use. If a request is older than 30 seconds,
|
41
|
-
it's dropped immediately. Otherwise, the timeout is the number of seconds left
|
42
|
-
or the value of `Rack::Timeout.timeout`, whichever is shorter.
|
47
|
+
it's dropped immediately. Otherwise, the timeout is the number of seconds left for the request
|
48
|
+
to be 30 seconds old, or the value of `Rack::Timeout.timeout`, whichever is shorter.
|
43
49
|
|
44
50
|
So, if a request has been sitting in the queue for 25s, and `Rack::Timeout.timeout` is set to
|
45
51
|
10s, the timeout used will be 5s, because `30 − 25 = 5`, and `5 < 10`.
|
46
52
|
|
47
|
-
The reasoning for this behavior is that the Heroku router drops requests if no
|
48
|
-
|
49
|
-
be able to respond to.
|
53
|
+
The reasoning for this behavior is that the Heroku router drops requests if no data is
|
54
|
+
transferred within 30s, so it makes no sense for the application to process a request it'll
|
55
|
+
never be able to respond to. (This is actually [a bit more involved][heroku-routing].)
|
50
56
|
|
51
|
-
The 30s maximum age is set in
|
57
|
+
The 30s maximum age is set in `Rack::Timeout::MAX_REQUEST_AGE`, and should generally not be
|
52
58
|
altered.
|
53
59
|
|
54
|
-
|
55
|
-
|
56
|
-
|
60
|
+
An exception to this is made for requests that have a non-empty body, e.g. POST, PUT, and PATCH
|
61
|
+
requests. X-Request-Start is set when the Heroku router begins receiving the request, but rack
|
62
|
+
will generally only see the request after it's been fully received by the application server
|
63
|
+
(i.e. thin, unicorn, etc). For short requests such as GET requests, this is irrelevant. But
|
64
|
+
with a slow client (say, a mobile app performing a file upload) the request can take a long
|
65
|
+
time to be fully received. A request that took longer than 30s to transmit would be dropped
|
66
|
+
immediately by Rack::Timeout because it'd be considered too old. The Heroku router, however,
|
67
|
+
would not have dropped this request because it would have been transmitting data all along.
|
68
|
+
|
69
|
+
For requests with a body, Rack::Timeout provides additional overtime before expiring them. The
|
70
|
+
default overtime is 60s, on top of the 30s `MAX_REQUEST_AGE`. This is user-configurable with
|
71
|
+
the `Rack::Timeout.overtime` setting:
|
72
|
+
|
73
|
+
Rack::Timeout.overtime = 10 # seconds over MAX_REQUEST_AGE
|
74
|
+
|
75
|
+
* With every line logged, Rack::Timeout includes a request ID. It'll first look for an ID in the
|
76
|
+
`Heroku-Request-ID` header; if not present, it'll then check `X-Request-ID`; and lastly, it'll
|
77
|
+
generate its own.
|
57
78
|
|
58
79
|
`Heroku-Request-ID` is not present by default on Heroku apps, but can be enabled through the
|
59
80
|
[http-request-id labs feature][http-request-id]. It's recommended to enable http-request-id as
|
@@ -61,6 +82,7 @@ Heroku Niceties
|
|
61
82
|
downsides to enabling http-request-id.
|
62
83
|
|
63
84
|
[X-Request-Start]: https://devcenter.heroku.com/articles/http-routing#heroku-headers
|
85
|
+
[heroku-routing]: https://devcenter.heroku.com/articles/http-routing#timeouts
|
64
86
|
[http-request-id]: https://devcenter.heroku.com/articles/http-request-id
|
65
87
|
|
66
88
|
Both these features are strictly reliant on the presence of the HTTP headers and make no effort to
|
@@ -76,30 +98,42 @@ Throughout a request's lifetime, Rack::Timeout keeps details about the request i
|
|
76
98
|
The value of that entry is an instance of `Rack::Timeout::RequestDetails`, which is a `Struct`
|
77
99
|
containing the following fields:
|
78
100
|
|
79
|
-
* `id`: a unique ID per request. Either `Heroku-Request-ID` or a random ID
|
101
|
+
* `id`: a unique ID per request. Either `Heroku-Request-ID`, `X-Request-ID`, or a random ID
|
102
|
+
generated internally.
|
80
103
|
|
81
104
|
* `age`: time in seconds since `X-Request-Start` when the request is first seen by Rack::Timeout.
|
82
105
|
Only set if `X-Request-Start` is present.
|
83
106
|
|
84
107
|
* `timeout`: timeout to be used, in seconds. Generally `Rack::Timeout.timeout`, unless
|
85
|
-
`X-Request-Start` is present. See discussion above under the Heroku Niceties section.
|
108
|
+
`X-Request-Start` is present. See discussion above, under the Heroku Niceties section.
|
86
109
|
|
87
|
-
* `duration`: set after a request completes (or times out). The time in seconds it took.
|
110
|
+
* `duration`: set after a request completes (or times out). The time in seconds it took. This is
|
111
|
+
also updated while a request is still active, around every second, with the time it's taken so
|
112
|
+
far.
|
88
113
|
|
89
114
|
* `state`: the possible states are:
|
90
115
|
|
91
116
|
* `expired`: the request is considered too old and is skipped entirely. This happens when
|
92
117
|
`X-Request-Start` is present and older than 30s. When this happens, a
|
93
|
-
`Rack::Timeout::RequestExpiryError` exception is raised.
|
118
|
+
`Rack::Timeout::RequestExpiryError` exception is raised. See earlier discussion about the
|
119
|
+
`Rack::Timeout.overtime` setting, too.
|
94
120
|
|
95
121
|
* `ready`: this is the initial state a request is in, before it's passed down the middleware
|
96
|
-
chain.
|
122
|
+
chain. While it's being processed, it'll move on to `active`, and then on to `timed_out`
|
123
|
+
and/or `completed`.
|
124
|
+
|
125
|
+
* `active`: the request is being actively processed in the application thread. This is
|
126
|
+
signaled repeatedly every ~1s until the request completes or times out.
|
97
127
|
|
98
128
|
* `timed_out`: the request had run for longer than the determined timeout and was aborted. A
|
99
129
|
`Rack::Timeout::RequestTimeoutError` error is raised in the application when this occurs.
|
130
|
+
If this error gets caught and handled and not re-raised in the app or framework (which will
|
131
|
+
generally happen with Rails and Sinatra), this state will not be final, `completed` will be
|
132
|
+
set after the framework is done with it.
|
100
133
|
|
101
134
|
* `completed`: the request completed in time and Rack::Timeout is done with it. This does not
|
102
135
|
mean the request completed *successfully*. Rack::Timeout does not concern itself with that.
|
136
|
+
As mentioned just above, a timed out request may still end up with a `completed` state.
|
103
137
|
|
104
138
|
|
105
139
|
Errors
|
@@ -109,20 +143,31 @@ Rack::Timeout can raise two types of exceptions. Both descend from `Rack::Timeou
|
|
109
143
|
itself descends from `RuntimeError`. They are:
|
110
144
|
|
111
145
|
* `Rack::Timeout::RequestTimeoutError`: this is raised when a request has run for longer than the
|
112
|
-
specified timeout. This is raised
|
113
|
-
|
146
|
+
specified timeout. This is raised by the rack-timeout timer thread in the application thread,
|
147
|
+
at the point in the stack the app happens to be in when the timeout is triggered. This
|
148
|
+
exception can generally be caught within the application, but in doing so you're working past
|
149
|
+
the timeout. This is ok for quick cleanups but shouldn't be abused as Rack::Timeout will not
|
150
|
+
kick in twice for the same request.
|
114
151
|
|
115
152
|
* `Rack::Timeout::RequestExpiryError`: this is raised when a request is skipped for being too old
|
116
153
|
(see the X-Request-Start bit under the Heroku Niceties section). This cannot generally be
|
117
|
-
rescued from
|
118
|
-
Rails.
|
154
|
+
rescued from inside a Rails controller action as it happens before the request has a chance to
|
155
|
+
reach Rails.
|
119
156
|
|
120
157
|
This shouldn't be any different for other frameworks, unless you have something above
|
121
158
|
Rack::Timeout in the middleware stack, which you generally shouldn't.
|
122
159
|
|
123
|
-
You shouldn't
|
160
|
+
You shouldn't rescue from these errors for reporting purposes. Instead, you can subscribe for state
|
124
161
|
change notifications with observers.
|
125
162
|
|
163
|
+
If you're trying to test that a `Rack::Timeout::RequestTimeoutError` is raised in an action in your
|
164
|
+
Rails application, you **must do so in integration tests**. Please note that Rack::Timeout will not
|
165
|
+
kick in for functional tests as they bypass the rack middleware stack.
|
166
|
+
|
167
|
+
[More details about testing middleware with Rails here][pablobm].
|
168
|
+
|
169
|
+
[pablobm]: http://stackoverflow.com/a/8681208/13989
|
170
|
+
|
126
171
|
|
127
172
|
Observers
|
128
173
|
---------
|
@@ -158,7 +203,10 @@ Logging
|
|
158
203
|
|
159
204
|
Rack::Timeout logs a line every time there's a change in state in a request's lifetime.
|
160
205
|
|
161
|
-
Changes into `timed_out` and `expired` are logged at the `ERROR` level,
|
206
|
+
Changes into `timed_out` and `expired` are logged at the `ERROR` level, most other things are
|
207
|
+
logged as `INFO`.
|
208
|
+
|
209
|
+
Exceptionally, `active` state is logged as `DEBUG`, every ~1s while the request is still active.
|
162
210
|
|
163
211
|
The default log level for Rack::Timeout is `INFO`, but can be affected via:
|
164
212
|
|
@@ -200,24 +248,6 @@ For applications running Ruby 1.8.x and/or Rails 2.x, use [version 0.0.4][v0.0.4
|
|
200
248
|
[v0.0.4]: https://github.com/kch/rack-timeout/tree/v0.0.4
|
201
249
|
|
202
250
|
|
203
|
-
Here Be Dragons
|
204
|
-
---------------
|
205
|
-
|
206
|
-
* Ruby's Timeout rely on threads. If your app or any of the libraries it depends on is not
|
207
|
-
thread-safe, you may run into issues using Rack::Timeout.
|
208
|
-
|
209
|
-
Concurrent web servers such as [Unicorn][] and [Puma][] should work fine with Rack::Timeout.
|
210
|
-
|
211
|
-
* If you're trying to test that a `Rack::Timeout::Error` is raised in an action in your Rails
|
212
|
-
application, you **must do so in integration tests**. Please note that Rack::Timeout will not
|
213
|
-
kick in for functional tests as they bypass the rack middleware stack.
|
214
|
-
|
215
|
-
[More details about testing middleware with Rails here][pablobm].
|
216
|
-
|
217
|
-
[Unicorn]: http://unicorn.bogomips.org/
|
218
|
-
[Puma]: http://puma.io/
|
219
|
-
[pablobm]: http://stackoverflow.com/a/8681208/13989
|
220
|
-
|
221
251
|
---
|
222
252
|
Copyright © 2010-2013 Caio Chassot, released under the MIT license
|
223
253
|
<http://github.com/kch/rack-timeout>
|
data/lib/rack-timeout.rb
CHANGED
@@ -5,7 +5,6 @@ require 'rack/timeout/logger'
|
|
5
5
|
if defined?(Rails) && [3,4].include?(Rails::VERSION::MAJOR)
|
6
6
|
class Rack::Timeout::Railtie < Rails::Railtie
|
7
7
|
initializer('rack-timeout.prepend') { |app| app.config.middleware.insert 0, Rack::Timeout }
|
8
|
-
initializer('rack-timeout.tracker') { |app| app.config.middleware.use Rack::Timeout::TimeoutTracker }
|
9
8
|
end
|
10
9
|
end
|
11
10
|
|
data/lib/rack/timeout.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
-
require 'timeout'
|
3
2
|
require 'securerandom'
|
4
3
|
|
5
4
|
module Rack
|
@@ -8,14 +7,11 @@ module Rack
|
|
8
7
|
class RequestExpiryError < Error; end
|
9
8
|
class RequestTimeoutError < Error; end
|
10
9
|
|
11
|
-
RequestDetails
|
12
|
-
ENV_INFO_KEY
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
MAX_REQUEST_AGE = 30 # seconds
|
17
|
-
|
18
|
-
@timeout = 15
|
10
|
+
RequestDetails = Struct.new(:id, :age, :timeout, :duration, :state)
|
11
|
+
ENV_INFO_KEY = 'rack-timeout.info'
|
12
|
+
VALID_STATES = [:ready, :active, :expired, :timed_out, :completed]
|
13
|
+
MAX_REQUEST_AGE = 30 # seconds
|
14
|
+
@timeout = 15 # seconds
|
19
15
|
class << self
|
20
16
|
attr_accessor :timeout
|
21
17
|
end
|
@@ -26,61 +22,53 @@ module Rack
|
|
26
22
|
|
27
23
|
def call(env)
|
28
24
|
info = env[ENV_INFO_KEY] ||= RequestDetails.new
|
29
|
-
info.id ||= env['HTTP_HEROKU_REQUEST_ID'] || SecureRandom.hex
|
25
|
+
info.id ||= env['HTTP_HEROKU_REQUEST_ID'] || env['HTTP_X_REQUEST_ID'] || SecureRandom.hex
|
30
26
|
request_start = env['HTTP_X_REQUEST_START'] # unix timestamp in ms
|
31
|
-
request_start = Time.at(request_start.
|
27
|
+
request_start = Time.at(request_start.to_f / 1000) if request_start
|
32
28
|
info.age = Time.now - request_start if request_start
|
33
29
|
time_left = MAX_REQUEST_AGE - info.age if info.age
|
34
30
|
info.timeout = [self.class.timeout, time_left].compact.select { |n| n >= 0 }.min
|
35
31
|
|
36
32
|
if time_left && time_left <= 0
|
37
33
|
Rack::Timeout._set_state! env, :expired
|
38
|
-
raise RequestExpiryError
|
34
|
+
raise RequestExpiryError, "Request older than #{MAX_REQUEST_AGE} seconds."
|
39
35
|
end
|
40
36
|
|
41
37
|
Rack::Timeout._set_state! env, :ready
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
38
|
+
ready_time = Time.now
|
39
|
+
|
40
|
+
begin
|
41
|
+
app_thread = Thread.current
|
42
|
+
timeout_thread = Thread.start do
|
43
|
+
loop do
|
44
|
+
info.duration = Time.now - ready_time
|
45
|
+
sleep_seconds = [1, info.timeout - info.duration].min
|
46
|
+
break if sleep_seconds <= 0
|
47
|
+
Rack::Timeout._set_state! env, :active
|
48
|
+
sleep(sleep_seconds)
|
49
|
+
end
|
50
|
+
Rack::Timeout._set_state! env, :timed_out
|
51
|
+
app_thread.raise(RequestTimeoutError, "Request ran for longer than #{info.timeout} seconds.")
|
52
|
+
end
|
53
|
+
response = @app.call(env)
|
54
|
+
ensure
|
55
|
+
timeout_thread.kill
|
56
|
+
timeout_thread.join
|
48
57
|
end
|
49
|
-
end
|
50
58
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
rescue RequestTimeoutError
|
55
|
-
timed_out = true
|
56
|
-
raise
|
57
|
-
ensure
|
58
|
-
# I do not appreciate having to handle framework business in a rack-level library, but can't see another way around sinatra's error handling.
|
59
|
-
timed_out ||= env.values_at(*FRAMEWORK_ERROR_KEYS).any? { |e| e.is_a? RequestTimeoutError }
|
60
|
-
_set_state! env, :timed_out if timed_out
|
59
|
+
info.duration = Time.now - ready_time
|
60
|
+
Rack::Timeout._set_state! env, :completed
|
61
|
+
response
|
61
62
|
end
|
62
63
|
|
63
64
|
# used internally
|
64
65
|
def self._set_state!(env, state)
|
65
|
-
raise "Invalid state: #{state.inspect}" unless
|
66
|
+
raise "Invalid state: #{state.inspect}" unless VALID_STATES.include? state
|
66
67
|
info = env[ENV_INFO_KEY]
|
67
|
-
return if FINAL_STATES.include? info.state
|
68
68
|
info.state = state
|
69
69
|
notify_state_change_observers(env)
|
70
70
|
end
|
71
71
|
|
72
|
-
# A second middleware to be added last in rails; ensures timed_out states get intercepted properly.
|
73
|
-
# This works as long as it's after ActionDispatch::ShowExceptions and ActionDispatch::DebugExceptions in the middleware list, which happens normally when added via `app.config.middleware.use`.
|
74
|
-
class TimeoutTracker
|
75
|
-
def initialize(app)
|
76
|
-
@app = app
|
77
|
-
end
|
78
|
-
|
79
|
-
def call(env)
|
80
|
-
Rack::Timeout._perform_block_tracking_timeout_to_env(env) { @app.call(env) }
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
72
|
|
85
73
|
### state change notification-related methods
|
86
74
|
|
data/lib/rack/timeout/logger.rb
CHANGED
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.1.
|
4
|
+
version: 0.1.0beta3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Caio Chassot
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-
|
11
|
+
date: 2013-07-01 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Rack middleware which aborts requests that have been running for longer
|
14
14
|
than a specified timeout.
|
@@ -32,12 +32,12 @@ require_paths:
|
|
32
32
|
- lib
|
33
33
|
required_ruby_version: !ruby/object:Gem::Requirement
|
34
34
|
requirements:
|
35
|
-
- -
|
35
|
+
- - '>='
|
36
36
|
- !ruby/object:Gem::Version
|
37
37
|
version: '0'
|
38
38
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
39
39
|
requirements:
|
40
|
-
- -
|
40
|
+
- - '>'
|
41
41
|
- !ruby/object:Gem::Version
|
42
42
|
version: 1.3.1
|
43
43
|
requirements: []
|