rack-timeout 0.1.0beta2 → 0.1.0beta3

Sign up to get free protection for your applications and to get access to all the features.
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: []