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 CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- ZGY0YjFmYWYzMTAzY2E4NWUzMWQyM2ZlN2YwY2E2Yzk0ZGVkZGRlNA==
5
- data.tar.gz: !binary |-
6
- MjE3NDM5ZTdlMWU4NTQ0OGEzNGYyYzU2MTc4MjczNTNhZDhhYWE0Mw==
7
- !binary "U0hBNTEy":
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` will be raised.
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
- Setup for current versions of Rails, Rack, Ruby, and Bundler. See the Compatibility section at the
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 offers the [`X-Request-Start`][X-Request-Start] HTTP header, which is a timestamp
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 to 30 seconds,
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 response is
48
- received within 30s, so it makes no sense for the application to process a request it'll never
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 in `Rack::Timeout::MAX_REQUEST_AGE`, and should generally not be
57
+ The 30s maximum age is set in `Rack::Timeout::MAX_REQUEST_AGE`, and should generally not be
52
58
  altered.
53
59
 
54
- * With every line logged, Rack::Timeout includes a request ID. Generally it'll generate its own
55
- ID, but before that, it'll look for the `Heroku-Request-ID` header. If present, this is the ID
56
- that'll get logged.
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 generated internally.
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. After that, it'll either end up as `timed_out` or `completed`.
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 in the application thread, as per the `::Timeout.timeout`
113
- semantics, and can generally be caught within the application.
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 in a Rails controller action as it happens before the request has a chance to reach
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 generally care about rescuing from these errors. Instead, you can subscribe for state
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, everything else is `INFO`.
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 = Struct.new(:id, :age, :timeout, :duration, :state)
12
- ENV_INFO_KEY = 'rack-timeout.info'
13
- FRAMEWORK_ERROR_KEYS = %w(sinatra.error rack.exception) # No idea what actually sets rack.exception but a lot of other libraries seem to reference it.
14
- FINAL_STATES = [:expired, :timed_out, :completed]
15
- ACCEPTABLE_STATES = [:ready] + FINAL_STATES
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.to_i / 1000) if 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
- ::Timeout.timeout(info.timeout, RequestTimeoutError) do
43
- ready_time = Time.now
44
- response = Rack::Timeout._perform_block_tracking_timeout_to_env(env) { @app.call(env) }
45
- info.duration = Time.now - ready_time
46
- Rack::Timeout._set_state! env, :completed
47
- response
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
- # used in #call and TimeoutTracker
52
- def self._perform_block_tracking_timeout_to_env(env)
53
- yield
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 ACCEPTABLE_STATES.include? state
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
 
@@ -12,6 +12,7 @@ module Rack
12
12
  SIMPLE_FORMATTER = ->(severity, timestamp, progname, msg) { "#{msg} at=#{severity.downcase}\n" }
13
13
  DEFAULT_LEVEL = INFO
14
14
  STATE_LOG_LEVEL = { ready: INFO,
15
+ active: DEBUG,
15
16
  completed: INFO,
16
17
  expired: ERROR,
17
18
  timed_out: ERROR,
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.0beta2
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-05-08 00:00:00.000000000 Z
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: []