rack-timeout 0.1.0beta → 0.1.0beta2

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,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