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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c9ae29d987db68c611785883420553a2f6f40ee7
4
- data.tar.gz: 1c1785b62a64d3a6cf5b5937baae4dec0a092ddd
3
+ metadata.gz: 3c6085bc1b83e9bb9f2b9e461a397dc4764fffe1
4
+ data.tar.gz: 30290762388e9fc3512a48e962b4101817b7d53f
5
5
  SHA512:
6
- metadata.gz: 22dd8d23074301936134273f9dd3763c1dfb03a42ad49ea0e63f53f0db64095135e00b33fdcc08f9b10fb9a8d43ad94c3dc94bf527e054e6e0376c65a8c6d131
7
- data.tar.gz: 114c9f37b4b435d213eb33fa4596cc18b0949f1c670fd2c9c25b3a4a061666ffb7a1bd52540d46b752d1795b224455ab9399b8d496e8e5f62cfb9812c0a8270a
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 StageChangeLoggingObserver, works slightly differently too
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/timeout.rb
31
- Rack::Timeout.timeout = 5 # seconds
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/timeout.rb
44
+ # config/initializers/rack_timeout.rb
45
45
 
46
- # insert middleware wherever you want in the stack
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
- use Rack::Timeout # Call as early as possible so rack-timeout runs before all other middleware.
59
- Rack::Timeout.timeout = 5 # Recommended. If omitted, defaults to 15 seconds.
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
- `Rack::Timeout.timeout` (or `Rack::Timeout.service_timeout`) is our principal setting.
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 `Rack::Timeout.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`.
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
- `Rack::Timeout.wait_timeout` is set at a default of 30 seconds, matching Heroku's router's timeout.
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 `Rack::Timeout.service_past_wait` to `true`. When set, the `service_timeout` setting will always be honored.
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 `Rack::Timeout.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.
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. If this exception gets caught, handled, and not re-raised in the app or framework (which will generally happen with Rails and Sinatra), this state will not be final, `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.)
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 may still end up with a `completed` state if the framework has dealt with the timeout exception.
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 1.9.1 and up, and, for Rails apps, Rails 3.x and up.
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
  ---
@@ -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 setup getter/setters for timeout properties. Ensure they're always positive numbers or false. When set to false (or 0), their behaviour is disabled.
45
- class << self
46
- def set_timeout_property(property_name, value)
47
- unless value == false || (value.is_a?(Numeric) && value >= 0)
48
- raise ArgumentError, "value for #{property_name} should be false, zero, or a positive number."
49
- end
50
- value = false if value && value.zero? # zero means we're disabling the feature
51
- instance_variable_set("@#{property_name}", value)
52
- end
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
- # all values are in seconds
64
- timeout_property :wait_timeout, 30 # 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.
65
- timeout_property :wait_overtime, 60 # 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.
66
- timeout_property :service_timeout, 15 # How long the application can take to complete handling the request once it's passed down to it.
67
-
68
- class << self
69
- alias_method :timeout=, :service_timeout= # legacy compatibility setter
70
- attr_accessor :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.
71
- @service_past_wait = false # 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.
72
- end
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 = (RT.wait_overtime && RT._request_has_body?(env)) ? RT.wait_overtime : 0 # additional wait timeout (if set and applicable)
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 && RT.wait_timeout
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 = RT.wait_timeout + effective_overtime # how long the request will be allowed to have waited
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 RT.service_timeout
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 = RT.service_timeout # nice and simple, when service_past_wait is true, not so much otherwise:
107
- info.timeout = seconds_service_left if !RT.service_past_wait && seconds_service_left && seconds_service_left > 0 && seconds_service_left < RT.service_timeout
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 = Time.now - time_started_service # update service time
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-reaise RequestTimeoutException as RequestTimeoutError
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
@@ -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
- request = ::Rack::Request.new(exception.env)
25
- payload = payload.dup
26
- data = data.dup
27
- payload["data"] = 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 "assert-types"
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::AssertTypes
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(:time, :proc)
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(time, proc, every)
45
- @start = time
39
+ def initialize(monotime, proc, every)
40
+ @start = monotime
46
41
  @every = every
47
42
  @iter = 0
48
- super(time, proc)
43
+ super(monotime, proc)
49
44
  end
50
45
 
51
46
  def run!
52
47
  super
53
48
  ensure
54
- self.time = @start + @every * (@iter += 1) until time >= Time.now
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
- def runner
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, Time.now # 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
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 Time.now - last_run > MAX_IDLE_SECS # exit the run loop if done nothing for the past MAX_IDLE_SECS seconds
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(&:time).min - Time.now, 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)
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 = Time.now #
93
- run, defer = @events.partition { |ev| ev.time <= now } # separate events to run now and events to run later
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(&:time).each { |ev| ev.run! } # run the events scheduled to run now
100
- last_run = Time.now # 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
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 to run at a different time. 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
122
- def reschedule(event, time)
123
- assert_types! event => RunEvent, time => Time
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.time = time
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
- run_at(Time.now + secs, &block)
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(Time.now, block, seconds)
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.3.2
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: 2015-09-07 00:00:00.000000000 Z
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/assert-types.rb
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.4.8
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