rack-timeout 0.1.0beta → 0.1.0beta2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- ZmIyM2ZmYTI0MGY2MWM2YjViZDYwNDZjZjEyNGEwODM5OGY5MGUzYw==
4
+ ZGY0YjFmYWYzMTAzY2E4NWUzMWQyM2ZlN2YwY2E2Yzk0ZGVkZGRlNA==
5
5
  data.tar.gz: !binary |-
6
- YmE0ZjYyZGNjOWE1NTljN2VhYjBjM2E5ZmI3NWIzMjIyY2YzZGJiYQ==
6
+ MjE3NDM5ZTdlMWU4NTQ0OGEzNGYyYzU2MTc4MjczNTNhZDhhYWE0Mw==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- NjQxODhjN2JlNjNjM2E2NzI1NDA4NzRhNDFjMmUxYmVmMTFlOGI1NWU3ZWNk
10
- NDNkMDYwZjc5YjRiZTE3NGViYTY1YjQyYWJiMjcyZWJhNGMzMjQ0ZTk3M2E1
11
- NGI2YTk3YzZjZmRlMmUyNGFkYmQ3OWM2MjdkYTZhZGY1ZWMwZDA=
9
+ YTVlYTViYjc0ZDdlYzBiYjNhYzJiOTc4ZTQ4OTVlYjNkOWM5MTFmMmRmNzIw
10
+ MTM0OWI3ZWVmODRiY2M3ZDAwMGY3OGViNmJkMTZiYzlkYzI0ZTJhYThhYTEy
11
+ MWJlNTUzZTZjYzc2Y2ViMThlMjJlMDdiMTM5Mzg0MzNjMzM0ZTc=
12
12
  data.tar.gz: !binary |-
13
- ODg1ODEzZjRhY2FiYmY1NjBmYzkzZDZlZTAxZjRmZWU5MDk3NGY5OWMyN2Q4
14
- ZDcyN2QwMzM2NjM5N2RjYzIwYWU3MDE3OWI0NGMyNGRhOTNhYTQxNzMxNTcz
15
- YzAwMzE1OTZmMTA5ZmU2OTdmMWNjN2JlYTI3MmVjMjIzMTVjNmM=
13
+ ZTc3Yzk3MmE4YWE0NjQwNjliNjNmZmY1Zjc0ZTdjODYwYThhYmJjNjI0NTRh
14
+ ZTk5ZGU0YjE1YmQ3ZWY0YTg4N2I1MWUwOGExYTk4M2Q1ZGI0NWRmM2YxMjAw
15
+ MjFiMzJlOTQ0NjVkYWZmMWRhYzMzN2UwOGRmYjkwZDc0MTc0MjA=
data/README.markdown CHANGED
@@ -7,7 +7,8 @@ Abort requests that are taking too long; a `Rack::Timeout::Error` will be raised
7
7
  Usage
8
8
  -----
9
9
 
10
- Setup for current versions of Rails, Rack, Ruby, and Bundler. See next section for legacy versions.
10
+ Setup for current versions of Rails, Rack, Ruby, and Bundler. See the Compatibility section at the
11
+ end for legacy versions.
11
12
 
12
13
  ### Rails apps
13
14
 
@@ -23,11 +24,171 @@ create an initializer file:
23
24
  ### Sinatra and other Rack apps
24
25
 
25
26
  # config.ru
26
- require "rack/timeout"
27
+ require "rack-timeout"
27
28
  use Rack::Timeout # Call as early as possible so rack-timeout runs before other middleware.
28
29
  Rack::Timeout.timeout = 10 # This line is optional. If omitted, timeout defaults to 15 seconds.
29
30
 
30
31
 
32
+ Heroku Niceties
33
+ ---------------
34
+
35
+ * 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.
38
+
39
+ If the `X-Request-Start` HTTP header is present, Rack::Timeout will take the age of the request
40
+ 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.
43
+
44
+ So, if a request has been sitting in the queue for 25s, and `Rack::Timeout.timeout` is set to
45
+ 10s, the timeout used will be 5s, because `30 − 25 = 5`, and `5 < 10`.
46
+
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.
50
+
51
+ The 30s maximum age is set in in `Rack::Timeout::MAX_REQUEST_AGE`, and should generally not be
52
+ altered.
53
+
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.
57
+
58
+ `Heroku-Request-ID` is not present by default on Heroku apps, but can be enabled through the
59
+ [http-request-id labs feature][http-request-id]. It's recommended to enable http-request-id as
60
+ it allows one to correlate Rack::Timeout events with the Heroku router's events. There are no
61
+ downsides to enabling http-request-id.
62
+
63
+ [X-Request-Start]: https://devcenter.heroku.com/articles/http-routing#heroku-headers
64
+ [http-request-id]: https://devcenter.heroku.com/articles/http-request-id
65
+
66
+ Both these features are strictly reliant on the presence of the HTTP headers and make no effort to
67
+ determine if the app is actually running on Heroku.
68
+
69
+
70
+ Request Lifetime
71
+ ----------------
72
+
73
+ Throughout a request's lifetime, Rack::Timeout keeps details about the request in
74
+ `env[Rack::Timeout::ENV_INFO_KEY]`, or, more explicitly, `env["rack-timeout.info"]`.
75
+
76
+ The value of that entry is an instance of `Rack::Timeout::RequestDetails`, which is a `Struct`
77
+ containing the following fields:
78
+
79
+ * `id`: a unique ID per request. Either `Heroku-Request-ID` or a random ID generated internally.
80
+
81
+ * `age`: time in seconds since `X-Request-Start` when the request is first seen by Rack::Timeout.
82
+ Only set if `X-Request-Start` is present.
83
+
84
+ * `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.
86
+
87
+ * `duration`: set after a request completes (or times out). The time in seconds it took.
88
+
89
+ * `state`: the possible states are:
90
+
91
+ * `expired`: the request is considered too old and is skipped entirely. This happens when
92
+ `X-Request-Start` is present and older than 30s. When this happens, a
93
+ `Rack::Timeout::RequestExpiryError` exception is raised.
94
+
95
+ * `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`.
97
+
98
+ * `timed_out`: the request had run for longer than the determined timeout and was aborted. A
99
+ `Rack::Timeout::RequestTimeoutError` error is raised in the application when this occurs.
100
+
101
+ * `completed`: the request completed in time and Rack::Timeout is done with it. This does not
102
+ mean the request completed *successfully*. Rack::Timeout does not concern itself with that.
103
+
104
+
105
+ Errors
106
+ ------
107
+
108
+ Rack::Timeout can raise two types of exceptions. Both descend from `Rack::Timeout::Error`, which
109
+ itself descends from `RuntimeError`. They are:
110
+
111
+ * `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.
114
+
115
+ * `Rack::Timeout::RequestExpiryError`: this is raised when a request is skipped for being too old
116
+ (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.
119
+
120
+ This shouldn't be any different for other frameworks, unless you have something above
121
+ Rack::Timeout in the middleware stack, which you generally shouldn't.
122
+
123
+ You shouldn't generally care about rescuing from these errors. Instead, you can subscribe for state
124
+ change notifications with observers.
125
+
126
+
127
+ Observers
128
+ ---------
129
+
130
+ Observers are objects or blocks that are notified about state changes during a request lifetime.
131
+
132
+ You can register an observer easily with a block:
133
+
134
+ Rack::Timeout.register_state_change_observer(:a_unique_name) { |env| do_things env }
135
+
136
+ or by passing an object that responds to `rack_timeout_request_did_change_state_in(env)`:
137
+
138
+ class MyObserver
139
+ def rack_timeout_request_did_change_state_in(env)
140
+ # ... do stuff ...
141
+ end
142
+ end
143
+
144
+ Rack::Timeout.register_state_change_observer(:another_name, MyObserver.new)
145
+
146
+ This is how logging is implemented, too. See `Rack::Timeout::StateChangeLogger`.
147
+
148
+ You can remove an observer with `unregister_state_change_observer`:
149
+
150
+ Rack::Timeout.unregister_state_change_observer(:a_unique_name)
151
+
152
+ Custom observers might be used to store statistics on request length, timeouts, etc., and
153
+ potentially do performance tuning on the fly.
154
+
155
+
156
+ Logging
157
+ -------
158
+
159
+ Rack::Timeout logs a line every time there's a change in state in a request's lifetime.
160
+
161
+ Changes into `timed_out` and `expired` are logged at the `ERROR` level, everything else is `INFO`.
162
+
163
+ The default log level for Rack::Timeout is `INFO`, but can be affected via:
164
+
165
+ * Unix environment variables. First `RACK_TIMEOUT_LOG_LEVEL` is checked, then `LOG_LEVEL`. Their
166
+ value must be name of a predefined constant in ruby's `Logger` class, e.g. `INFO` or `DEBUG`.
167
+ Case is not significant.
168
+
169
+ * By setting `Rack::Timeout.logger.level` directly, e.g.:
170
+
171
+ Rack::Timeout.logger.level = ::Logger::DEBUG
172
+
173
+ Logging is enabled by default if Rack::Timeout is loaded via the `rack-timeout` file (recommended),
174
+ but can be removed by unregistering its observer:
175
+
176
+ Rack::Timeout.unregister_state_change_observer(:logger)
177
+
178
+ Each log line is a set of `key=value` pairs, containing the entries from the
179
+ `env["rack-timeout.info"]` struct that are not `nil`. See the Request Lifetime section above for a
180
+ description of each field. Note that while the values for `age`, `timeout`, and `duration` are
181
+ stored internally as seconds, they are logged as milliseconds for readability.
182
+
183
+ A sample log excerpt might look like:
184
+
185
+ source=rack-timeout id=13793c age=369ms timeout=10000ms state=ready at=info
186
+ source=rack-timeout id=13793c age=369ms timeout=10000ms duration=15ms state=completed at=info
187
+ source=rack-timeout id=ea7bd3 age=371ms timeout=10000ms state=timed_out at=error
188
+
189
+ (IDs shortened for readability.)
190
+
191
+
31
192
  Compatibility
32
193
  -------------
33
194
 
@@ -39,7 +200,7 @@ For applications running Ruby 1.8.x and/or Rails 2.x, use [version 0.0.4][v0.0.4
39
200
  [v0.0.4]: https://github.com/kch/rack-timeout/tree/v0.0.4
40
201
 
41
202
 
42
- Here be dragons
203
+ Here Be Dragons
43
204
  ---------------
44
205
 
45
206
  * Ruby's Timeout rely on threads. If your app or any of the libraries it depends on is not
data/lib/rack-timeout.rb CHANGED
@@ -1,9 +1,12 @@
1
1
  # encoding: utf-8
2
- require_relative 'rack/timeout'
2
+ require 'rack/timeout'
3
+ require 'rack/timeout/logger'
3
4
 
4
5
  if defined?(Rails) && [3,4].include?(Rails::VERSION::MAJOR)
5
6
  class Rack::Timeout::Railtie < Rails::Railtie
6
- initializer('rack-timeout.prepend') { |app| app.config.middleware.insert 0, Rack::Timeout }
7
- initializer('rack-timeout.timeout-trap') { |app| app.config.middleware.use Rack::Timeout::AbortionReporter }
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 }
8
9
  end
9
10
  end
11
+
12
+ Rack::Timeout::StateChangeLogger.register!
data/lib/rack/timeout.rb CHANGED
@@ -5,13 +5,14 @@ require 'securerandom'
5
5
  module Rack
6
6
  class Timeout
7
7
  class Error < RuntimeError; end
8
- class RequestTooOldError < Error; end
9
- class RequestAbortedError < Error; end
8
+ class RequestExpiryError < Error; end
9
+ class RequestTimeoutError < Error; end
10
10
 
11
- RequestData = Struct.new(:id, :age, :timeout, :duration, :state)
11
+ RequestDetails = Struct.new(:id, :age, :timeout, :duration, :state)
12
12
  ENV_INFO_KEY = 'rack-timeout.info'
13
- FRAMEWORK_ERROR_KEYS = %w(sinatra.error rack.exception)
14
- FINAL_STATES = [:dropped, :aborted, :completed]
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
15
16
  MAX_REQUEST_AGE = 30 # seconds
16
17
 
17
18
  @timeout = 15
@@ -24,7 +25,7 @@ module Rack
24
25
  end
25
26
 
26
27
  def call(env)
27
- info = env[ENV_INFO_KEY] ||= RequestData.new
28
+ info = env[ENV_INFO_KEY] ||= RequestDetails.new
28
29
  info.id ||= env['HTTP_HEROKU_REQUEST_ID'] || SecureRandom.hex
29
30
  request_start = env['HTTP_X_REQUEST_START'] # unix timestamp in ms
30
31
  request_start = Time.at(request_start.to_i / 1000) if request_start
@@ -33,57 +34,88 @@ module Rack
33
34
  info.timeout = [self.class.timeout, time_left].compact.select { |n| n >= 0 }.min
34
35
 
35
36
  if time_left && time_left <= 0
36
- Rack::Timeout.set_state_and_log! info, :dropped
37
- raise RequestTooOldError
37
+ Rack::Timeout._set_state! env, :expired
38
+ raise RequestExpiryError
38
39
  end
39
40
 
40
- Rack::Timeout.set_state_and_log! info, :ready
41
- ::Timeout.timeout(info.timeout, RequestAbortedError) do
41
+ Rack::Timeout._set_state! env, :ready
42
+ ::Timeout.timeout(info.timeout, RequestTimeoutError) do
42
43
  ready_time = Time.now
43
- response = Rack::Timeout.perform_reporting_abortion_state_in_env(env) { @app.call(env) }
44
+ response = Rack::Timeout._perform_block_tracking_timeout_to_env(env) { @app.call(env) }
44
45
  info.duration = Time.now - ready_time
45
- Rack::Timeout.set_state_and_log! info, :completed
46
+ Rack::Timeout._set_state! env, :completed
46
47
  response
47
48
  end
48
49
  end
49
50
 
50
- def self.perform_reporting_abortion_state_in_env(env)
51
+ # used in #call and TimeoutTracker
52
+ def self._perform_block_tracking_timeout_to_env(env)
51
53
  yield
52
- rescue RequestAbortedError
53
- set_aborted! env
54
+ rescue RequestTimeoutError
55
+ timed_out = true
54
56
  raise
55
57
  ensure
56
- set_aborted! env if env.values_at(*FRAMEWORK_ERROR_KEYS).any? { |e| e.is_a? RequestAbortedError }
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
57
61
  end
58
62
 
59
- def self.set_aborted!(env)
60
- set_state_and_log!(env[ENV_INFO_KEY], :aborted)
61
- end
62
-
63
- def self.set_state_and_log!(info, state)
63
+ # used internally
64
+ def self._set_state!(env, state)
65
+ raise "Invalid state: #{state.inspect}" unless ACCEPTABLE_STATES.include? state
66
+ info = env[ENV_INFO_KEY]
64
67
  return if FINAL_STATES.include? info.state
65
68
  info.state = state
66
- ms = ->(s) { '%.fms' % (s * 1000) }
67
- s = 'source=rack-timeout'
68
- s << ' id=' << info.id if info.id
69
- s << ' age=' << ms[info.age] if info.age
70
- s << ' timeout=' << ms[info.timeout] if info.timeout
71
- s << ' duration=' << ms[info.duration] if info.duration
72
- s << ' state=' << info.state.to_s if info.state
73
- s << "\n"
74
-
75
- $stderr << s
69
+ notify_state_change_observers(env)
76
70
  end
77
71
 
78
- class AbortionReporter
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
79
75
  def initialize(app)
80
76
  @app = app
81
77
  end
82
78
 
83
79
  def call(env)
84
- Rack::Timeout.perform_reporting_abortion_state_in_env(env) { @app.call(env) }
80
+ Rack::Timeout._perform_block_tracking_timeout_to_env(env) { @app.call(env) }
85
81
  end
86
82
  end
87
83
 
84
+
85
+ ### state change notification-related methods
86
+
87
+ OBSERVER_CALLBACK_METHOD_NAME = :rack_timeout_request_did_change_state_in
88
+ @state_change_observers = {}
89
+
90
+ # Registers an object or a block to be called back when a request changes state in rack-timeout.
91
+ #
92
+ # `id` is anything that uniquely identifies this particular callback, mostly so it may be removed via `unregister_state_change_observer`.
93
+ #
94
+ # The second parameter can be either an object that responds to `rack_timeout_request_did_change_state_in(env)` or a block. The object and the block cannot be both specified at the same time.
95
+ #
96
+ # Example calls:
97
+ # Rack::Timeout.register_state_change_observer(:foo_reporter, FooStateReporter.new)
98
+ # Rack::Timeout.register_state_change_observer(:bar) { |env| do_bar_things(env) }
99
+ def self.register_state_change_observer(id, object = nil, &callback)
100
+ raise RuntimeError, "An observer with the id #{id.inspect} is already set." if @state_change_observers.key? id
101
+ raise ArgumentError, "Pass either a callback object or a block; never both." unless [object, callback].compact.length == 1
102
+ raise RuntimeError, "Object must respond to rack_timeout_request_did_change_state_in" if object && !object.respond_to?(OBSERVER_CALLBACK_METHOD_NAME)
103
+ callback.singleton_class.send :alias_method, OBSERVER_CALLBACK_METHOD_NAME, :call if callback
104
+ @state_change_observers[id] = object || callback
105
+ end
106
+
107
+ # Removes the observer with the given id
108
+ def self.unregister_state_change_observer(id)
109
+ @state_change_observers.delete id
110
+ end
111
+
112
+
113
+ private
114
+
115
+ # Sends out the notifications. Called internally at the end of `set_state!`
116
+ def self.notify_state_change_observers(env)
117
+ @state_change_observers.values.each { |observer| observer.send(OBSERVER_CALLBACK_METHOD_NAME, env) }
118
+ end
119
+
88
120
  end
89
121
  end
@@ -0,0 +1,72 @@
1
+ require 'logger'
2
+
3
+ module Rack
4
+ class Timeout
5
+
6
+ # convenience method so the current logger can be accessed via Rack::Timeout.logger
7
+ def self.logger
8
+ @state_change_observers[:logger]
9
+ end
10
+
11
+ class StateChangeLogger < ::Logger
12
+ SIMPLE_FORMATTER = ->(severity, timestamp, progname, msg) { "#{msg} at=#{severity.downcase}\n" }
13
+ DEFAULT_LEVEL = INFO
14
+ STATE_LOG_LEVEL = { ready: INFO,
15
+ completed: INFO,
16
+ expired: ERROR,
17
+ timed_out: ERROR,
18
+ }
19
+
20
+
21
+ # creates a logger and registers for state change notifications in Rack::Timeout
22
+ def self.register!(*a)
23
+ new(*a).register!
24
+ end
25
+
26
+ # registers for state change notifications in Rack::Timeout
27
+ def register!(target = ::Rack::Timeout)
28
+ target.register_state_change_observer(:logger, self)
29
+ end
30
+
31
+ def initialize(device = $stderr, *a)
32
+ super(device, *a)
33
+ self.formatter = SIMPLE_FORMATTER
34
+ self.level = self.class.determine_level
35
+ end
36
+
37
+ # callback method from Rack::Timeout state change notifications
38
+ def rack_timeout_request_did_change_state_in(env)
39
+ log_state_change(env[ENV_INFO_KEY])
40
+ end
41
+
42
+
43
+ private
44
+
45
+ # log level is, by precedence, one of: $RACK_TIMEOUT_LOG_LEVEL > $LOG_LEVEL > INFO
46
+ def self.determine_level
47
+ env_log_level = ENV.values_at("RACK_TIMEOUT_LOG_LEVEL", "LOG_LEVEL").compact.map(&:upcase).first
48
+ env_log_level = const_get(env_log_level) if env_log_level && const_defined?(env_log_level)
49
+ env_log_level || DEFAULT_LEVEL
50
+ end
51
+
52
+ # helper method used for formatting in #log_state_change
53
+ def ms(s)
54
+ '%.fms' % (s * 1000)
55
+ end
56
+
57
+ # generates the actual log string
58
+ def log_state_change(info)
59
+ add(STATE_LOG_LEVEL[info.state]) do
60
+ s = 'source=rack-timeout'
61
+ s << ' id=' << info.id if info.id
62
+ s << ' age=' << ms(info.age) if info.age
63
+ s << ' timeout=' << ms(info.timeout) if info.timeout
64
+ s << ' duration=' << ms(info.duration) if info.duration
65
+ s << ' state=' << info.state.to_s if info.state
66
+ s
67
+ end
68
+ end
69
+
70
+ end
71
+ end
72
+ end
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.0beta
4
+ version: 0.1.0beta2
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-04-26 00:00:00.000000000 Z
11
+ date: 2013-05-08 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.
@@ -19,6 +19,7 @@ extra_rdoc_files: []
19
19
  files:
20
20
  - MIT-LICENSE
21
21
  - README.markdown
22
+ - lib/rack/timeout/logger.rb
22
23
  - lib/rack/timeout.rb
23
24
  - lib/rack-timeout.rb
24
25
  homepage: http://github.com/kch/rack-timeout