rack-timeout 0.3.2 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +8 -1
- data/README.markdown +65 -22
- data/lib/rack/timeout/core.rb +37 -39
- data/lib/rack/timeout/legacy.rb +46 -0
- data/lib/rack/timeout/rollbar.rb +26 -12
- data/lib/rack/timeout/support/monotonic_time.rb +29 -0
- data/lib/rack/timeout/support/scheduler.rb +21 -37
- metadata +5 -4
- data/lib/rack/timeout/support/assert-types.rb +0 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3c6085bc1b83e9bb9f2b9e461a397dc4764fffe1
|
4
|
+
data.tar.gz: 30290762388e9fc3512a48e962b4101817b7d53f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 58a88b26d6aad37c19e15ddac0db4b8b77684542c02ab00670a76bb36db01a98b2dbf9dadf0e2b34402a4e971eaf7f4d653311708c1e36ed52f64d09073d0c7a
|
7
|
+
data.tar.gz: c542d17400ea593c1b85192a3e68917880a83dc89d274598566111c5f20d3c611e2fee7228bcaabe8fae18782bb65014a7ffff315157d04894287651874e53bb
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
0.4.0
|
2
|
+
=====
|
3
|
+
- Using monotonic time instead of Time.now where available (/ht concurrent-ruby)
|
4
|
+
- Settings are now passable to the middleware initializer instead of class-level
|
5
|
+
- Rollbar module may take a custom fingerprint block
|
6
|
+
- Rollbar module considered final
|
7
|
+
|
1
8
|
0.3.2
|
2
9
|
=====
|
3
10
|
- Fixes calling timeout with a value of 0 (issue #90)
|
@@ -13,7 +20,7 @@
|
|
13
20
|
- 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)
|
14
21
|
- don't insert middleware for rails in test environment
|
15
22
|
- add convenience module Rack::Timeout::Logger (see README for more)
|
16
|
-
- StageChangeLoggingObserver renamed to
|
23
|
+
- StageChangeLoggingObserver renamed to StateChangeLoggingObserver, works slightly differently too
|
17
24
|
- file layout reorganization (see 6e82c276 for details)
|
18
25
|
- CHANGELOG file is now in the gem (@dbackeus)
|
19
26
|
- add optional and experimental support for grouping errors by url under rollbar. see "rack/timeout/rollbar" for usage
|
data/README.markdown
CHANGED
@@ -27,8 +27,8 @@ That'll load rack-timeout and set it up as a Rails middleware using the default
|
|
27
27
|
To use a custom timeout, create an initializer file:
|
28
28
|
|
29
29
|
```ruby
|
30
|
-
# config/initializers/
|
31
|
-
Rack::Timeout.
|
30
|
+
# config/initializers/rack_timeout.rb
|
31
|
+
Rack::Timeout.service_timeout = 5 # seconds
|
32
32
|
```
|
33
33
|
|
34
34
|
### Rails apps, manually
|
@@ -41,13 +41,10 @@ gem "rack-timeout", require:"rack/timeout/base"
|
|
41
41
|
```
|
42
42
|
|
43
43
|
```ruby
|
44
|
-
# config/initializers/
|
44
|
+
# config/initializers/rack_timeout.rb
|
45
45
|
|
46
|
-
#
|
47
|
-
Rails.application.config.middleware.insert_before Rack::Runtime, Rack::Timeout
|
48
|
-
|
49
|
-
# customize seconds before timeout
|
50
|
-
Rack::Timeout.timeout = 5
|
46
|
+
# insert middleware wherever you want in the stack, optionally pass initialization arguments
|
47
|
+
Rails.application.config.middleware.insert_before Rack::Runtime, Rack::Timeout, service_timeout: 5
|
51
48
|
```
|
52
49
|
|
53
50
|
### Sinatra and other Rack apps
|
@@ -55,17 +52,37 @@ Rack::Timeout.timeout = 5
|
|
55
52
|
```ruby
|
56
53
|
# config.ru
|
57
54
|
require "rack-timeout"
|
58
|
-
|
59
|
-
|
55
|
+
|
56
|
+
# Call as early as possible so rack-timeout runs before all other middleware.
|
57
|
+
# Setting service_timeout is recommended. If omitted, defaults to 15 seconds.
|
58
|
+
use Rack::Timeout, service_timeout: 5
|
60
59
|
```
|
61
60
|
|
62
61
|
|
63
62
|
The Rabbit Hole
|
64
63
|
---------------
|
65
64
|
|
65
|
+
Rack::Timeout takes the following settings, shown here with their default values:
|
66
|
+
|
67
|
+
```
|
68
|
+
service_timeout: 15
|
69
|
+
wait_timeout: 30
|
70
|
+
wait_overtime: 60
|
71
|
+
service_past_wait: false
|
72
|
+
```
|
73
|
+
|
74
|
+
As shown earlier, these settings can be overriden during middleware initialization:
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
use Rack::Timeout, service_timeout: 5, wait_timeout: false
|
78
|
+
```
|
79
|
+
|
80
|
+
For legacy reasons, it's also possible to set these at the class level (e.g. `Rack::Timeout.service_timeout = 3`). This is, however, not recommended. Also beware this style takes precedence over all instances' initialization settings.
|
81
|
+
|
82
|
+
|
66
83
|
### Service Timeout
|
67
84
|
|
68
|
-
`
|
85
|
+
`service_timeout` is our most important setting.
|
69
86
|
|
70
87
|
*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.
|
71
88
|
|
@@ -76,16 +93,16 @@ Service timeout can be disabled entirely by setting the property to `0` or `fals
|
|
76
93
|
|
77
94
|
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*.
|
78
95
|
|
79
|
-
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 `
|
96
|
+
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`.
|
80
97
|
|
81
98
|
[heroku-routing]: https://devcenter.heroku.com/articles/http-routing#timeouts
|
82
99
|
[heroku-timeout]: https://devcenter.heroku.com/articles/request-timeout
|
83
100
|
|
84
|
-
`
|
101
|
+
`wait_timeout` is set at a default of 30 seconds, matching Heroku's router's timeout.
|
85
102
|
|
86
103
|
Wait timeout can be disabled entirely by setting the property to `0` or `false`.
|
87
104
|
|
88
|
-
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 `
|
105
|
+
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.
|
89
106
|
|
90
107
|
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.)
|
91
108
|
|
@@ -104,7 +121,7 @@ A request that took longer than 30s to be fully received, but that had been uplo
|
|
104
121
|
|
105
122
|
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.
|
106
123
|
|
107
|
-
This extra time is called *wait overtime* and can be set via `
|
124
|
+
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.
|
108
125
|
|
109
126
|
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.
|
110
127
|
|
@@ -139,6 +156,15 @@ That said, it's something to be aware of, and may explain some eerie wonkiness s
|
|
139
156
|
[handle-interrupt]: http://www.ruby-doc.org/core-2.1.3/Thread.html#method-c-handle_interrupt
|
140
157
|
|
141
158
|
|
159
|
+
### Time Out Early and Often
|
160
|
+
|
161
|
+
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].
|
162
|
+
|
163
|
+
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.
|
164
|
+
|
165
|
+
[ruby-timeouts]: https://github.com/ankane/the-ultimate-guide-to-ruby-timeouts
|
166
|
+
|
167
|
+
|
142
168
|
Request Lifetime
|
143
169
|
----------------
|
144
170
|
|
@@ -163,9 +189,9 @@ The value of that entry is an instance of `Rack::Timeout::RequestDetails`, which
|
|
163
189
|
|
164
190
|
* `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.
|
165
191
|
|
166
|
-
* `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.
|
192
|
+
* `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.)
|
167
193
|
|
168
|
-
* `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
|
194
|
+
* `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.
|
169
195
|
|
170
196
|
|
171
197
|
Errors
|
@@ -194,6 +220,27 @@ If you're trying to test that a `Rack::Timeout::RequestTimeoutException` is rais
|
|
194
220
|
[pablobm]: http://stackoverflow.com/a/8681208/13989
|
195
221
|
|
196
222
|
|
223
|
+
### Rollbar
|
224
|
+
|
225
|
+
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.
|
226
|
+
|
227
|
+
A Rollbar module is provided which causes rack-timeout errors to be grouped by exception type, request HTTP method, and URL, so all timeouts for a particular endpoint are reported under the same entry.
|
228
|
+
|
229
|
+
To enable it, simply require it after having required rollbar. For example, in your rollbar initializer file, do:
|
230
|
+
|
231
|
+
require "rollbar"
|
232
|
+
require "rack/timeout/rollbar"
|
233
|
+
|
234
|
+
It'll set itself up.
|
235
|
+
|
236
|
+
If you wish to use a custom fingerprint for grouping:
|
237
|
+
|
238
|
+
Rack::Timeout::Rollbar.fingerprint do |exception, env|
|
239
|
+
# return a string derived from exception and env
|
240
|
+
end
|
241
|
+
|
242
|
+
|
243
|
+
|
197
244
|
Observers
|
198
245
|
---------
|
199
246
|
|
@@ -264,11 +311,7 @@ source=rack-timeout id=ea7bd3 wait=371ms timeout=10000ms state=timed_out at=erro
|
|
264
311
|
Compatibility
|
265
312
|
-------------
|
266
313
|
|
267
|
-
This version of Rack::Timeout is compatible with Ruby
|
268
|
-
|
269
|
-
For applications running Ruby 1.8.x and/or Rails 2.x, use [version 0.0.4][v0.0.4].
|
270
|
-
|
271
|
-
[v0.0.4]: https://github.com/heroku/rack-timeout/tree/v0.0.4
|
314
|
+
This version of Rack::Timeout is compatible with Ruby 2.1 and up, and, for Rails apps, Rails 3.x and up.
|
272
315
|
|
273
316
|
|
274
317
|
---
|
data/lib/rack/timeout/core.rb
CHANGED
@@ -1,10 +1,14 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require "securerandom"
|
3
|
+
require_relative "support/monotonic_time"
|
3
4
|
require_relative "support/scheduler"
|
4
5
|
require_relative "support/timeout"
|
6
|
+
require_relative "legacy"
|
5
7
|
|
6
8
|
module Rack
|
7
9
|
class Timeout
|
10
|
+
include Rack::Timeout::MonotonicTime # gets us the #fsecs method
|
11
|
+
|
8
12
|
module ExceptionWithEnv # shared by the following exceptions, allows them to receive the current env
|
9
13
|
attr :env
|
10
14
|
def initialize(env)
|
@@ -26,7 +30,7 @@ module Rack
|
|
26
30
|
:wait, # seconds the request spent in the web server before being serviced by rack
|
27
31
|
:service, # time rack spent processing the request (updated ~ every second)
|
28
32
|
:timeout, # the actual computed timeout to be used for this request
|
29
|
-
:state, # the request's current state, see below
|
33
|
+
:state, # the request's current state, see VALID_STATES below
|
30
34
|
) {
|
31
35
|
def ms(k) # helper method used for formatting values in milliseconds
|
32
36
|
"%.fms" % (self[k] * 1000) if self[k]
|
@@ -41,37 +45,29 @@ module Rack
|
|
41
45
|
]
|
42
46
|
ENV_INFO_KEY = "rack-timeout.info" # key under which each request's RequestDetails instance is stored in its env.
|
43
47
|
|
44
|
-
# helper methods to
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
def timeout_property(property_name, start_value)
|
55
|
-
singleton_class.instance_eval do
|
56
|
-
attr_reader property_name
|
57
|
-
define_method("#{property_name}=") { |v| set_timeout_property(property_name, v) }
|
58
|
-
end
|
59
|
-
set_timeout_property(property_name, start_value)
|
48
|
+
# helper methods to read timeout properties. Ensure they're always positive numbers or false. When set to false (or 0), their behaviour is disabled.
|
49
|
+
def read_timeout_property value, default
|
50
|
+
case value
|
51
|
+
when nil ; default
|
52
|
+
when false ; false
|
53
|
+
when 0 ; false
|
54
|
+
else
|
55
|
+
value.is_a?(Numeric) && value > 0 or raise ArgumentError, "value #{value.inspect} should be false, zero, or a positive number."
|
56
|
+
value
|
60
57
|
end
|
61
58
|
end
|
62
59
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
@
|
72
|
-
|
73
|
-
|
74
|
-
def initialize(app)
|
60
|
+
attr_reader \
|
61
|
+
:service_timeout, # How long the application can take to complete handling the request once it's passed down to it.
|
62
|
+
: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.
|
63
|
+
: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.
|
64
|
+
: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.
|
65
|
+
|
66
|
+
def initialize(app, service_timeout:nil, wait_timeout:nil, wait_overtime:nil, service_past_wait:false)
|
67
|
+
@service_timeout = read_timeout_property service_timeout, 15
|
68
|
+
@wait_timeout = read_timeout_property wait_timeout, 30
|
69
|
+
@wait_overtime = read_timeout_property wait_overtime, 60
|
70
|
+
@service_past_wait = service_past_wait
|
75
71
|
@app = app
|
76
72
|
end
|
77
73
|
|
@@ -81,16 +77,17 @@ module Rack
|
|
81
77
|
info = (env[ENV_INFO_KEY] ||= RequestDetails.new)
|
82
78
|
info.id ||= env["HTTP_X_REQUEST_ID"] || SecureRandom.hex
|
83
79
|
|
84
|
-
time_started_service = Time.now # The time the request started being processed by rack
|
80
|
+
time_started_service = Time.now # The wall time the request started being processed by rack
|
81
|
+
ts_started_service = fsecs # The monotonic time the request started being processed by rack
|
85
82
|
time_started_wait = RT._read_x_request_start(env) # The time the request was initially received by the web server (if available)
|
86
|
-
effective_overtime = (
|
83
|
+
effective_overtime = (wait_overtime && RT._request_has_body?(env)) ? wait_overtime : 0 # additional wait timeout (if set and applicable)
|
87
84
|
seconds_service_left = nil
|
88
85
|
|
89
86
|
# if X-Request-Start is present and wait_timeout is set, expire requests older than wait_timeout (+wait_overtime when applicable)
|
90
|
-
if time_started_wait &&
|
87
|
+
if time_started_wait && wait_timeout
|
91
88
|
seconds_waited = time_started_service - time_started_wait # how long it took between the web server first receiving the request and rack being able to handle it
|
92
89
|
seconds_waited = 0 if seconds_waited < 0 # make up for potential time drift between the routing server and the application server
|
93
|
-
final_wait_timeout =
|
90
|
+
final_wait_timeout = wait_timeout + effective_overtime # how long the request will be allowed to have waited
|
94
91
|
seconds_service_left = final_wait_timeout - seconds_waited # first calculation of service timeout (relevant if request doesn't get expired, may be overriden later)
|
95
92
|
info.wait, info.timeout = seconds_waited, final_wait_timeout # updating the info properties; info.timeout will be the wait timeout at this point
|
96
93
|
if seconds_service_left <= 0 # expire requests that have waited for too long in the queue (as they are assumed to have been dropped by the web server / routing layer at this point)
|
@@ -100,18 +97,18 @@ module Rack
|
|
100
97
|
end
|
101
98
|
|
102
99
|
# pass request through if service_timeout is false (i.e., don't time it out at all.)
|
103
|
-
return @app.call(env) unless
|
100
|
+
return @app.call(env) unless service_timeout
|
104
101
|
|
105
102
|
# compute actual timeout to be used for this request; if service_past_wait is true, this is just service_timeout. If false (the default), and wait time was determined, we'll use the shortest value between seconds_service_left and service_timeout. See comment above at service_past_wait for justification.
|
106
|
-
info.timeout =
|
107
|
-
info.timeout = seconds_service_left if !
|
103
|
+
info.timeout = service_timeout # nice and simple, when service_past_wait is true, not so much otherwise:
|
104
|
+
info.timeout = seconds_service_left if !service_past_wait && seconds_service_left && seconds_service_left > 0 && seconds_service_left < service_timeout
|
108
105
|
|
109
106
|
RT._set_state! env, :ready # we're good to go, but have done nothing yet
|
110
107
|
|
111
108
|
heartbeat_event = nil # init var so it's in scope for following proc
|
112
109
|
register_state_change = ->(status = :active) { # updates service time and state; will run every second
|
113
110
|
heartbeat_event.cancel! if status != :active # if the request is no longer active we should stop updating every second
|
114
|
-
info.service =
|
111
|
+
info.service = fsecs - ts_started_service # update service time
|
115
112
|
RT._set_state! env, status # update status
|
116
113
|
}
|
117
114
|
heartbeat_event = RT::Scheduler.run_every(1) { register_state_change.call :active } # start updating every second while active; if log level is debug, this will log every sec
|
@@ -124,11 +121,12 @@ module Rack
|
|
124
121
|
response = timeout.timeout(info.timeout) do # perform request with timeout
|
125
122
|
begin @app.call(env) # boom, send request down the middleware chain
|
126
123
|
rescue RequestTimeoutException => e # will actually hardly ever get to this point because frameworks tend to catch this. see README for more
|
127
|
-
raise RequestTimeoutError.new(env), e.message, e.backtrace # but in case it does get here, re-
|
124
|
+
raise RequestTimeoutError.new(env), e.message, e.backtrace # but in case it does get here, re-raise RequestTimeoutException as RequestTimeoutError
|
125
|
+
ensure
|
126
|
+
register_state_change.call :completed
|
128
127
|
end
|
129
128
|
end
|
130
129
|
|
131
|
-
register_state_change.call :completed
|
132
130
|
response
|
133
131
|
end
|
134
132
|
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require_relative "support/namespace"
|
2
|
+
|
3
|
+
|
4
|
+
# This provides compatibility with versions <= 0.3.x where timeout settings were class-level.
|
5
|
+
# Beware that, unintuitively, a class-level setting overrides local settings for all instances.
|
6
|
+
# Generally speaking, everyone should migrate to instance-level settings.
|
7
|
+
|
8
|
+
module Rack::Timeout::ClassLevelProperties
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
attr_accessor :service_timeout, :wait_timeout, :wait_overtime, :service_past_wait
|
12
|
+
alias_method :timeout=, :service_timeout=
|
13
|
+
|
14
|
+
[ :service_timeout=,
|
15
|
+
:timeout=,
|
16
|
+
:wait_timeout=,
|
17
|
+
:wait_overtime=,
|
18
|
+
:service_past_wait=,
|
19
|
+
].each do |isetter|
|
20
|
+
setter = instance_method(isetter)
|
21
|
+
define_method(isetter) do |x|
|
22
|
+
warn "`Rack::Timeout.#{isetter}`: class-level settings are deprecated. See README for examples on using the middleware initializer instead."
|
23
|
+
setter.bind(self).call(x)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
module InstanceMethods
|
29
|
+
def read_timeout_property_with_class_override property_name
|
30
|
+
read_timeout_property self.class.send(property_name), method(property_name).super_method.call
|
31
|
+
end
|
32
|
+
|
33
|
+
[:service_timeout, :wait_timeout, :wait_overtime].each do |m|
|
34
|
+
define_method(m) { read_timeout_property_with_class_override m }
|
35
|
+
end
|
36
|
+
|
37
|
+
def service_past_wait
|
38
|
+
self.class.service_past_wait || super
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
Rack::Timeout.extend Rack::Timeout::ClassLevelProperties::ClassMethods
|
46
|
+
Rack::Timeout.prepend Rack::Timeout::ClassLevelProperties::InstanceMethods
|
data/lib/rack/timeout/rollbar.rb
CHANGED
@@ -6,11 +6,31 @@ require_relative "core"
|
|
6
6
|
#
|
7
7
|
# require "rack/timeout/rollbar"
|
8
8
|
#
|
9
|
-
# This is somewhat experimental and very lightly tested.
|
10
|
-
#
|
11
9
|
# Ruby 2.0 is required as we use `Module.prepend`.
|
10
|
+
#
|
11
|
+
# To use a custom fingerprint for grouping:
|
12
|
+
#
|
13
|
+
# Rack::Timeout::Rollbar.fingerprint do |exception, env|
|
14
|
+
# # … return some kind of string derived from exception and env
|
15
|
+
# end
|
12
16
|
|
13
17
|
module Rack::Timeout::Rollbar
|
18
|
+
|
19
|
+
def self.fingerprint(&block)
|
20
|
+
define_method(:rack_timeout_fingerprint) { |exception, env| block[exception, env] }
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.default_rack_timeout_fingerprint(exception, env)
|
24
|
+
request = ::Rack::Request.new(env)
|
25
|
+
[ exception.class.name,
|
26
|
+
request.request_method,
|
27
|
+
request.path
|
28
|
+
].join(" ")
|
29
|
+
end
|
30
|
+
|
31
|
+
fingerprint &method(:default_rack_timeout_fingerprint)
|
32
|
+
|
33
|
+
|
14
34
|
def build_payload(level, message, exception, extra)
|
15
35
|
payload = super(level, message, exception, extra)
|
16
36
|
|
@@ -21,16 +41,10 @@ module Rack::Timeout::Rollbar
|
|
21
41
|
data = payload["data"]
|
22
42
|
return payload unless data.respond_to?(:[]=)
|
23
43
|
|
24
|
-
|
25
|
-
|
26
|
-
data
|
27
|
-
payload["data"]
|
28
|
-
|
29
|
-
data["fingerprint"] = [
|
30
|
-
exception.class.name,
|
31
|
-
request.request_method,
|
32
|
-
request.fullpath
|
33
|
-
].join(" ")
|
44
|
+
payload = payload.dup
|
45
|
+
data = data.dup
|
46
|
+
data["fingerprint"] = rack_timeout_fingerprint(exception, exception.env)
|
47
|
+
payload["data"] = data
|
34
48
|
|
35
49
|
return payload
|
36
50
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative "namespace"
|
2
|
+
|
3
|
+
# lifted from https://github.com/ruby-concurrency/concurrent-ruby/blob/master/lib/concurrent/utility/monotonic_time.rb
|
4
|
+
|
5
|
+
module Rack::Timeout::MonotonicTime
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def fsecs_mono
|
9
|
+
Process.clock_gettime Process::CLOCK_MONOTONIC
|
10
|
+
end
|
11
|
+
|
12
|
+
def fsecs_java
|
13
|
+
java.lang.System.nanoTime() / 1_000_000_000.0
|
14
|
+
end
|
15
|
+
|
16
|
+
mutex = Mutex.new
|
17
|
+
last_time = Time.now.to_f
|
18
|
+
define_method(:fsecs_ruby) do
|
19
|
+
now = Time.now.to_f
|
20
|
+
mutex.synchronize { last_time = last_time < now ? now : last_time + 1e-6 }
|
21
|
+
end
|
22
|
+
|
23
|
+
case
|
24
|
+
when defined? Process::CLOCK_MONOTONIC ; alias fsecs fsecs_mono
|
25
|
+
when RUBY_PLATFORM == "java" ; alias fsecs fsecs_java
|
26
|
+
else ; alias fsecs fsecs_ruby
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
require_relative "namespace"
|
3
|
-
require_relative "
|
3
|
+
require_relative "monotonic_time"
|
4
4
|
|
5
5
|
# Runs code at a later time
|
6
6
|
#
|
@@ -17,15 +17,10 @@ require_relative "assert-types"
|
|
17
17
|
# One could also instantiate separate instances which would get you separate run threads, but generally there's no point in it.
|
18
18
|
class Rack::Timeout::Scheduler
|
19
19
|
MAX_IDLE_SECS = 30 # how long the runner thread is allowed to live doing nothing
|
20
|
-
include Rack::Timeout::
|
20
|
+
include Rack::Timeout::MonotonicTime # gets us the #fsecs method
|
21
21
|
|
22
22
|
# stores a proc to run later, and the time it should run at
|
23
|
-
class RunEvent < Struct.new(:
|
24
|
-
def initialize(time, proc)
|
25
|
-
Rack::Timeout::AssertTypes.assert_types! time => Time, proc => Proc
|
26
|
-
super
|
27
|
-
end
|
28
|
-
|
23
|
+
class RunEvent < Struct.new(:monotime, :proc)
|
29
24
|
def cancel!
|
30
25
|
@cancelled = true
|
31
26
|
end
|
@@ -41,17 +36,17 @@ class Rack::Timeout::Scheduler
|
|
41
36
|
end
|
42
37
|
|
43
38
|
class RepeatEvent < RunEvent
|
44
|
-
def initialize(
|
45
|
-
@start =
|
39
|
+
def initialize(monotime, proc, every)
|
40
|
+
@start = monotime
|
46
41
|
@every = every
|
47
42
|
@iter = 0
|
48
|
-
super(
|
43
|
+
super(monotime, proc)
|
49
44
|
end
|
50
45
|
|
51
46
|
def run!
|
52
47
|
super
|
53
48
|
ensure
|
54
|
-
self.
|
49
|
+
self.monotime = @start + @every * (@iter += 1) until monotime >= Rack::Timeout::MonotonicTime.fsecs
|
55
50
|
end
|
56
51
|
end
|
57
52
|
|
@@ -65,7 +60,7 @@ class Rack::Timeout::Scheduler
|
|
65
60
|
private
|
66
61
|
|
67
62
|
# returns the runner thread, creating it if needed
|
68
|
-
|
63
|
+
def runner
|
69
64
|
@mx_runner.synchronize {
|
70
65
|
return @runner unless @runner.nil? || !@runner.alive?
|
71
66
|
@joined = false
|
@@ -76,28 +71,28 @@ class Rack::Timeout::Scheduler
|
|
76
71
|
# the actual runner thread loop
|
77
72
|
def run_loop!
|
78
73
|
Thread.current.abort_on_exception = true # always be aborting
|
79
|
-
sleep_for, run, last_run = nil, nil,
|
74
|
+
sleep_for, run, last_run = nil, nil, fsecs # sleep_for: how long to sleep before next run; last_run: time of last run; run: just initializing it outside of the synchronize scope, will contain events to run now
|
80
75
|
loop do # begin event reader loop
|
81
76
|
@mx_events.synchronize { #
|
82
77
|
@events.reject!(&:cancelled?) # get rid of cancelled events
|
83
78
|
if @events.empty? # if there are no further events …
|
84
79
|
return if @joined # exit the run loop if this runner thread has been joined (the thread will die and the join will return)
|
85
|
-
return if
|
80
|
+
return if fsecs - last_run > MAX_IDLE_SECS # exit the run loop if done nothing for the past MAX_IDLE_SECS seconds
|
86
81
|
sleep_for = MAX_IDLE_SECS # sleep for MAX_IDLE_SECS (mind it that we get awaken when new events are scheduled)
|
87
82
|
else #
|
88
|
-
sleep_for = [@events.map(&:
|
83
|
+
sleep_for = [@events.map(&:monotime).min - fsecs, 0].max # if we have events, set to sleep until it's time for the next one to run. (the max bit ensure we don't have negative sleep times)
|
89
84
|
end #
|
90
85
|
@mx_events.sleep sleep_for # do sleep
|
91
86
|
#
|
92
|
-
now =
|
93
|
-
run, defer = @events.partition { |ev| ev.
|
87
|
+
now = fsecs #
|
88
|
+
run, defer = @events.partition { |ev| ev.monotime <= now } # separate events to run now and events to run later
|
94
89
|
defer += run.select { |ev| ev.is_a? RepeatEvent } # repeat events both run and are deferred
|
95
90
|
@events.replace(defer) # keep only events to run later
|
96
91
|
} #
|
97
92
|
#
|
98
93
|
next if run.empty? # done here if there's nothing to run now
|
99
|
-
run.sort_by(&:
|
100
|
-
last_run =
|
94
|
+
run.sort_by(&:monotime).each { |ev| ev.run! } # run the events scheduled to run now
|
95
|
+
last_run = fsecs # store that we did run things at this time, go immediately on to the next loop iteration as it may be time to run more things
|
101
96
|
end
|
102
97
|
end
|
103
98
|
|
@@ -112,41 +107,30 @@ class Rack::Timeout::Scheduler
|
|
112
107
|
|
113
108
|
# adds a RunEvent struct to the run schedule
|
114
109
|
def schedule(event)
|
115
|
-
assert_types! event => RunEvent
|
116
110
|
@mx_events.synchronize { @events << event }
|
117
111
|
runner.run # wakes up the runner thread so it can recalculate sleep length taking this new event into consideration
|
118
112
|
return event
|
119
113
|
end
|
120
114
|
|
121
|
-
# reschedules an event
|
122
|
-
|
123
|
-
|
115
|
+
# reschedules an event by the given number of seconds. can be negative to run sooner.
|
116
|
+
# returns nil and does nothing if the event is not already in the queue (might've run already), otherwise updates the event time in-place; returns the updated event.
|
117
|
+
def delay(event, secs)
|
124
118
|
@mx_events.synchronize {
|
125
119
|
return unless @events.include? event
|
126
|
-
event.
|
120
|
+
event.monotime += secs
|
127
121
|
runner.run
|
128
122
|
return event
|
129
123
|
}
|
130
124
|
end
|
131
125
|
|
132
|
-
# reschedules an event by the given number of seconds. can be negative to run sooner.
|
133
|
-
def delay(event, secs)
|
134
|
-
reschedule(event, event.time + secs)
|
135
|
-
end
|
136
|
-
|
137
|
-
# schedules a block to run at a given time; returns the created event object
|
138
|
-
def run_at(time, &block)
|
139
|
-
schedule RunEvent.new(time, block)
|
140
|
-
end
|
141
|
-
|
142
126
|
# schedules a block to run in the given number of seconds; returns the created event object
|
143
127
|
def run_in(secs, &block)
|
144
|
-
|
128
|
+
schedule RunEvent.new(fsecs + secs, block)
|
145
129
|
end
|
146
130
|
|
147
131
|
# schedules a block to run every x seconds; returns the created event object
|
148
132
|
def run_every(seconds, &block)
|
149
|
-
schedule RepeatEvent.new(
|
133
|
+
schedule RepeatEvent.new(fsecs, block, seconds)
|
150
134
|
end
|
151
135
|
|
152
136
|
|
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.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Caio Chassot
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-04-05 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.
|
@@ -23,11 +23,12 @@ files:
|
|
23
23
|
- lib/rack-timeout.rb
|
24
24
|
- lib/rack/timeout/base.rb
|
25
25
|
- lib/rack/timeout/core.rb
|
26
|
+
- lib/rack/timeout/legacy.rb
|
26
27
|
- lib/rack/timeout/logger.rb
|
27
28
|
- lib/rack/timeout/logging-observer.rb
|
28
29
|
- lib/rack/timeout/rails.rb
|
29
30
|
- lib/rack/timeout/rollbar.rb
|
30
|
-
- lib/rack/timeout/support/
|
31
|
+
- lib/rack/timeout/support/monotonic_time.rb
|
31
32
|
- lib/rack/timeout/support/namespace.rb
|
32
33
|
- lib/rack/timeout/support/scheduler.rb
|
33
34
|
- lib/rack/timeout/support/timeout.rb
|
@@ -51,7 +52,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
51
52
|
version: '0'
|
52
53
|
requirements: []
|
53
54
|
rubyforge_project:
|
54
|
-
rubygems_version: 2.
|
55
|
+
rubygems_version: 2.5.1
|
55
56
|
signing_key:
|
56
57
|
specification_version: 4
|
57
58
|
summary: Abort requests that are taking too long
|
@@ -1,14 +0,0 @@
|
|
1
|
-
require_relative "namespace"
|
2
|
-
|
3
|
-
module Rack::Timeout::AssertTypes
|
4
|
-
extend self
|
5
|
-
|
6
|
-
def assert_types! value_type_map
|
7
|
-
value_type_map.each do |val, types|
|
8
|
-
types = [types] unless types.is_a? Array
|
9
|
-
next if types.any? { |type| val.is_a? type }
|
10
|
-
raise TypeError, "#{val.inspect} is not a #{types.join(" | ")}"
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
end
|