rack-timeout 0.1.0beta3 → 0.1.0beta4

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: d8cadc6366c07e2e809ab55bbcbc66ea82e387cd
4
- data.tar.gz: 600c25828d8e91b1556cd4b458833e109c02788d
3
+ metadata.gz: 36207506f9394cfa73916a31f64a957c79677931
4
+ data.tar.gz: 5da1c876b06d0e29cd507aac3d2374d8006e2a36
5
5
  SHA512:
6
- metadata.gz: a4f2117b12ddac87a0398622924219d12eaf84ec11fbcbb55e31f8fa9163eb579a1cb4c7a61d6ec3c0011de3699cf83a1688c80d40de173d1235294fa4b83184
7
- data.tar.gz: b54a7b0085fbefa6ecad6e2438b7ec6c99ba3c907a806ef34b8b6e2238b442dd9050e92a47aa9e738138467c081f0965419f085950570d6316bd8fa3d6aa4959
6
+ metadata.gz: 9035faeb15e556cc8a908124e81a2f4b76440baee9e77ed68f7685730bedb670928631db0def7c356ef3c2dc61924bc72f7261139f70e37f6f41f3b4b282d197
7
+ data.tar.gz: c08a2cd396936bad0970a21f251da1fd5c573cf3585b77d954a149f8b75230b03fbe4677f009690052c26d97d2b85d07bd812b818db324c1e255d197052a8fba
data/README.markdown CHANGED
@@ -1,5 +1,4 @@
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.
1
+ README is not very out-of-date for this release. Lots of comments in source though. README updates coming before next release.
3
2
 
4
3
  Rack::Timeout
5
4
  =============
@@ -101,13 +100,13 @@ containing the following fields:
101
100
  * `id`: a unique ID per request. Either `Heroku-Request-ID`, `X-Request-ID`, or a random ID
102
101
  generated internally.
103
102
 
104
- * `age`: time in seconds since `X-Request-Start` when the request is first seen by Rack::Timeout.
103
+ * `wait`: time in seconds since `X-Request-Start` when the request is first seen by Rack::Timeout.
105
104
  Only set if `X-Request-Start` is present.
106
105
 
107
106
  * `timeout`: timeout to be used, in seconds. Generally `Rack::Timeout.timeout`, unless
108
107
  `X-Request-Start` is present. See discussion above, under the Heroku Niceties section.
109
108
 
110
- * `duration`: set after a request completes (or times out). The time in seconds it took. This is
109
+ * `service`: set after a request completes (or times out). The time in seconds it took. This is
111
110
  also updated while a request is still active, around every second, with the time it's taken so
112
111
  far.
113
112
 
@@ -225,14 +224,14 @@ but can be removed by unregistering its observer:
225
224
 
226
225
  Each log line is a set of `key=value` pairs, containing the entries from the
227
226
  `env["rack-timeout.info"]` struct that are not `nil`. See the Request Lifetime section above for a
228
- description of each field. Note that while the values for `age`, `timeout`, and `duration` are
227
+ description of each field. Note that while the values for `wait`, `timeout`, and `service` are
229
228
  stored internally as seconds, they are logged as milliseconds for readability.
230
229
 
231
230
  A sample log excerpt might look like:
232
231
 
233
- source=rack-timeout id=13793c age=369ms timeout=10000ms state=ready at=info
234
- source=rack-timeout id=13793c age=369ms timeout=10000ms duration=15ms state=completed at=info
235
- source=rack-timeout id=ea7bd3 age=371ms timeout=10000ms state=timed_out at=error
232
+ source=rack-timeout id=13793c wait=369ms timeout=10000ms state=ready at=info
233
+ source=rack-timeout id=13793c wait=369ms timeout=10000ms service=15ms state=completed at=info
234
+ source=rack-timeout id=ea7bd3 wait=371ms timeout=10000ms state=timed_out at=error
236
235
 
237
236
  (IDs shortened for readability.)
238
237
 
data/lib/rack-timeout.rb CHANGED
@@ -8,4 +8,4 @@ if defined?(Rails) && [3,4].include?(Rails::VERSION::MAJOR)
8
8
  end
9
9
  end
10
10
 
11
- Rack::Timeout::StateChangeLogger.register!
11
+ Rack::Timeout::StageChangeLoggingObserver.register!
data/lib/rack/timeout.rb CHANGED
@@ -3,51 +3,102 @@ require 'securerandom'
3
3
 
4
4
  module Rack
5
5
  class Timeout
6
- class Error < RuntimeError; end
7
- class RequestExpiryError < Error; end
8
- class RequestTimeoutError < Error; end
9
-
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
6
+ class Error < RuntimeError; end # superclass for the following…
7
+ class RequestExpiryError < Error; end # raised when a request is dropped without being given a chance to run (because too old)
8
+ class RequestTimeoutError < Error; end # raised when a request has run for too long
9
+
10
+ RequestDetails = Struct.new(
11
+ :id, # a unique identifier for the request. informative-only.
12
+ :wait, # seconds the request spent in the web server before being serviced by rack
13
+ :service, # time rack spent processing the request (updated ~ every second)
14
+ :timeout, # the actual computed timeout to be used for this request
15
+ :state, # the request's current state, see below:
16
+ )
17
+ VALID_STATES = [
18
+ :expired, # The request was too old by the time it reached rack (see wait_timeout, wait_overtime)
19
+ :ready, # We're about to start processing this request
20
+ :active, # This request is currently being handled
21
+ :timed_out, # This request has run for too long and we're raising a timeout error in it
22
+ :completed, # We're done with this request (also set after having timed out a request)
23
+ ]
24
+ ENV_INFO_KEY = 'rack-timeout.info' # key under which each request's RequestDetails instance is stored in its env.
25
+
26
+ # 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.
15
27
  class << self
16
- attr_accessor :timeout
28
+ def set_timeout_property(property_name, value)
29
+ unless value == false || (value.is_a?(Numeric) && value >= 0)
30
+ raise ArgumentError, "value for #{property_name} should be false, zero, or a positive number."
31
+ end
32
+ value = false if value.zero? # zero means we're disabling the feature
33
+ instance_variable_set("@#{property_name}", value)
34
+ end
35
+
36
+ def timeout_property(property_name, start_value)
37
+ singleton_class.instance_eval do
38
+ attr_reader property_name
39
+ define_method("#{property_name}=") { |v| set_timeout_property(property_name, v) }
40
+ end
41
+ set_timeout_property(property_name, start_value)
42
+ end
43
+ end
44
+
45
+ # all values are in seconds
46
+ 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.
47
+ 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.
48
+ timeout_property :service_timeout, 15 # How long the application can take to complete handling the request once it's passed down to it.
49
+
50
+ class << self
51
+ alias_method :timeout=, :service_timeout= # legacy compatibility setter
52
+ 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.
53
+ @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.
17
54
  end
18
55
 
19
56
  def initialize(app)
20
57
  @app = app
21
58
  end
22
59
 
60
+ RT = self # shorthand reference
23
61
  def call(env)
24
- info = env[ENV_INFO_KEY] ||= RequestDetails.new
25
- info.id ||= env['HTTP_HEROKU_REQUEST_ID'] || env['HTTP_X_REQUEST_ID'] || SecureRandom.hex
26
- request_start = env['HTTP_X_REQUEST_START'] # unix timestamp in ms
27
- request_start = Time.at(request_start.to_f / 1000) if request_start
28
- info.age = Time.now - request_start if request_start
29
- time_left = MAX_REQUEST_AGE - info.age if info.age
30
- info.timeout = [self.class.timeout, time_left].compact.select { |n| n >= 0 }.min
31
-
32
- if time_left && time_left <= 0
33
- Rack::Timeout._set_state! env, :expired
34
- raise RequestExpiryError, "Request older than #{MAX_REQUEST_AGE} seconds."
62
+ info = (env[ENV_INFO_KEY] ||= RequestDetails.new)
63
+ info.id ||= env['HTTP_X_REQUEST_ID'] || SecureRandom.hex
64
+
65
+ time_started_service = Time.now # The time the request started being processed by rack
66
+ time_started_wait = RT._read_x_request_start(env) # The time the request was initially receibed by the web server (if available)
67
+ effective_overtime = (RT.wait_overtime && RT._request_has_body?(env)) ? RT.wait_overtime : 0 # additional wait timeout (if set and applicable)
68
+ seconds_service_left = nil
69
+
70
+ # if X-Request-Start is present and wait_timeout is set, expire requests older than wait_timeout (+wait_overtime when applicable)
71
+ if time_started_wait && RT.wait_timeout
72
+ 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
73
+ seconds_waited = 0 if seconds_waited < 0 # make up for potential time drift between the routing server and the application server
74
+ final_wait_timeout = RT.wait_timeout + effective_overtime # how long the request will be allowed to have waited
75
+ seconds_service_left = final_wait_timeout - seconds_waited # first calculation of service timeout (relevant if request doesn't get expired, may be overriden later)
76
+ info.wait, info.timeout = seconds_waited, final_wait_timeout # updating the info properties; info.timeout will be the wait timeout at this point
77
+ 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)
78
+ RT._set_state! env, :expired
79
+ raise RequestExpiryError, "Request older than #{final_wait_timeout} seconds."
80
+ end
35
81
  end
36
82
 
37
- Rack::Timeout._set_state! env, :ready
38
- ready_time = Time.now
83
+ # pass request through if service_timeout is false (i.e., don't time it out at all.)
84
+ return @app.call(env) unless RT.service_timeout
85
+
86
+ # 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.
87
+ info.timeout = RT.service_timeout # nice and simple, when service_past_wait is true, not so much otherwise:
88
+ info.timeout = seconds_service_left if !RT.service_past_wait && seconds_service_left && seconds_service_left > 0 && seconds_service_left < RT.service_timeout
39
89
 
90
+ RT._set_state! env, :ready
40
91
  begin
41
92
  app_thread = Thread.current
42
93
  timeout_thread = Thread.start do
43
94
  loop do
44
- info.duration = Time.now - ready_time
45
- sleep_seconds = [1, info.timeout - info.duration].min
95
+ info.service = Time.now - time_started_service
96
+ sleep_seconds = [1 - (info.service % 1), info.timeout - info.service].min
46
97
  break if sleep_seconds <= 0
47
- Rack::Timeout._set_state! env, :active
98
+ RT._set_state! env, :active
48
99
  sleep(sleep_seconds)
49
100
  end
50
- Rack::Timeout._set_state! env, :timed_out
101
+ RT._set_state! env, :timed_out
51
102
  app_thread.raise(RequestTimeoutError, "Request ran for longer than #{info.timeout} seconds.")
52
103
  end
53
104
  response = @app.call(env)
@@ -56,53 +107,69 @@ module Rack
56
107
  timeout_thread.join
57
108
  end
58
109
 
59
- info.duration = Time.now - ready_time
60
- Rack::Timeout._set_state! env, :completed
110
+ info.service = Time.now - time_started_service
111
+ RT._set_state! env, :completed
61
112
  response
62
113
  end
63
114
 
64
- # used internally
115
+ ### following methods are used internally (called by instances, so can't be private. _ marker should discourage people from calling them)
116
+
117
+ # X-Request-Start contains the time the request was first seen by the server. Format varies wildly amongst servers, yay!
118
+ # - nginx gives the time since epoch as seconds.milliseconds[1]. New Relic documentation recommends preceding it with t=[2], so might as well detect it.
119
+ # - Heroku gives the time since epoch in milliseconds. [3]
120
+ # - Apache uses t=microseconds[4], so we're not even going there.
121
+ #
122
+ # The sane way to handle this would be by knowing the server being used, instead let's just hack around with regular expressions and ignore apache entirely.
123
+ # [1]: http://nginx.org/en/docs/http/ngx_http_log_module.html#var_msec
124
+ # [2]: https://docs.newrelic.com/docs/apm/other-features/request-queueing/request-queue-server-configuration-examples#nginx
125
+ # [3]: https://devcenter.heroku.com/articles/http-routing#heroku-headers
126
+ # [4]: http://httpd.apache.org/docs/current/mod/mod_headers.html#header
127
+ #
128
+ # This is a code extraction for readability, this method is only called from a single point.
129
+ RX_NGINX_X_REQUEST_START = /^(?:t=)?(\d+)\.(\d{3})$/
130
+ RX_HEROKU_X_REQUEST_START = /^(\d+)$/
131
+ def self._read_x_request_start(env)
132
+ return unless s = env['HTTP_X_REQUEST_START']
133
+ return unless m = s.match(RX_HEROKU_X_REQUEST_START) || s.match(RX_NGINX_X_REQUEST_START)
134
+ Time.at(m[1,2].join.to_f / 1000)
135
+ end
136
+
137
+ # This method determines if a body is present. requests with a body (generally POST, PUT) can have a lengthy body which may have taken a while to be received by the web server, inflating their computed wait time. This in turn could lead to unwanted expirations. See wait_overtime property as a way to overcome those.
138
+ # This is a code extraction for readability, this method is only called from a single point.
139
+ def self._request_has_body?(env)
140
+ return true if env['HTTP_TRANSFER_ENCODING'] == 'chunked'
141
+ return false if env['CONTENT_LENGTH'].nil?
142
+ return false if env['CONTENT_LENGTH'].to_i.zero?
143
+ true
144
+ end
145
+
65
146
  def self._set_state!(env, state)
66
147
  raise "Invalid state: #{state.inspect}" unless VALID_STATES.include? state
67
- info = env[ENV_INFO_KEY]
68
- info.state = state
148
+ env[ENV_INFO_KEY].state = state
69
149
  notify_state_change_observers(env)
70
150
  end
71
151
 
72
-
73
152
  ### state change notification-related methods
153
+ @state_change_observers = {}
74
154
 
75
- OBSERVER_CALLBACK_METHOD_NAME = :rack_timeout_request_did_change_state_in
76
- @state_change_observers = {}
77
-
78
- # Registers an object or a block to be called back when a request changes state in rack-timeout.
155
+ # Registers a block to be called back when a request changes state in rack-timeout. The block will receive the request's env.
79
156
  #
80
157
  # `id` is anything that uniquely identifies this particular callback, mostly so it may be removed via `unregister_state_change_observer`.
81
- #
82
- # 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.
83
- #
84
- # Example calls:
85
- # Rack::Timeout.register_state_change_observer(:foo_reporter, FooStateReporter.new)
86
- # Rack::Timeout.register_state_change_observer(:bar) { |env| do_bar_things(env) }
87
- def self.register_state_change_observer(id, object = nil, &callback)
88
- raise RuntimeError, "An observer with the id #{id.inspect} is already set." if @state_change_observers.key? id
89
- raise ArgumentError, "Pass either a callback object or a block; never both." unless [object, callback].compact.length == 1
90
- raise RuntimeError, "Object must respond to rack_timeout_request_did_change_state_in" if object && !object.respond_to?(OBSERVER_CALLBACK_METHOD_NAME)
91
- callback.singleton_class.send :alias_method, OBSERVER_CALLBACK_METHOD_NAME, :call if callback
92
- @state_change_observers[id] = object || callback
158
+ def self.register_state_change_observer(id, &callback)
159
+ raise RuntimeError, "An observer with the id #{id.inspect} is already set." if @state_change_observers.key? id
160
+ raise ArgumentError, "A callback block is required." unless callback
161
+ @state_change_observers[id] = callback
93
162
  end
94
163
 
95
164
  # Removes the observer with the given id
96
165
  def self.unregister_state_change_observer(id)
97
- @state_change_observers.delete id
166
+ @state_change_observers.delete(id)
98
167
  end
99
168
 
100
-
101
169
  private
102
-
103
- # Sends out the notifications. Called internally at the end of `set_state!`
170
+ # Sends out the notifications. Called internally at the end of `_set_state!`
104
171
  def self.notify_state_change_observers(env)
105
- @state_change_observers.values.each { |observer| observer.send(OBSERVER_CALLBACK_METHOD_NAME, env) }
172
+ @state_change_observers.values.each { |observer| observer.call(env) }
106
173
  end
107
174
 
108
175
  end
@@ -1,73 +1,63 @@
1
1
  require 'logger'
2
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]
3
+ class Rack::Timeout
4
+ class StageChangeLoggingObserver
5
+ STATE_LOG_LEVEL = { :expired => :error,
6
+ :ready => :info,
7
+ :active => :debug,
8
+ :timed_out => :error,
9
+ :completed => :info,
10
+ }
11
+
12
+ # creates a logger and registers for state change notifications in Rack::Timeout
13
+ def self.register!(logger = nil)
14
+ new.register!(logger)
9
15
  end
10
16
 
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
- active: DEBUG,
16
- completed: INFO,
17
- expired: ERROR,
18
- timed_out: ERROR,
19
- }
20
-
21
-
22
- # creates a logger and registers for state change notifications in Rack::Timeout
23
- def self.register!(*a)
24
- new(*a).register!
25
- end
26
-
27
- # registers for state change notifications in Rack::Timeout
28
- def register!(target = ::Rack::Timeout)
29
- target.register_state_change_observer(:logger, self)
30
- end
31
-
32
- def initialize(device = $stderr, *a)
33
- super(device, *a)
34
- self.formatter = SIMPLE_FORMATTER
35
- self.level = self.class.determine_level
36
- end
17
+ # registers for state change notifications in Rack::Timeout (or other explicit target (potentially useful for testing))
18
+ def register!(logger = nil, target = ::Rack::Timeout)
19
+ @logger = logger
20
+ target.register_state_change_observer(:logger, &method(:log_state_change))
21
+ end
37
22
 
38
- # callback method from Rack::Timeout state change notifications
39
- def rack_timeout_request_did_change_state_in(env)
40
- log_state_change(env[ENV_INFO_KEY])
23
+ SIMPLE_FORMATTER = ->(severity, timestamp, progname, msg) { "#{msg} at=#{severity.downcase}\n" }
24
+ def self.mk_logger(device, level = ::Logger::INFO)
25
+ ::Logger.new(device).tap do |logger|
26
+ logger.level = level
27
+ logger.formatter = SIMPLE_FORMATTER
41
28
  end
29
+ end
42
30
 
31
+ class << self
32
+ attr_accessor :logger
33
+ end
34
+ def logger(env = nil)
35
+ self.class.logger ||
36
+ (defined?(::Rails) && Rails.logger) ||
37
+ (env && env['rack.logger']) ||
38
+ (env && env['rack.errors'] && self.class.mk_logger(env['rack.errors'])) ||
39
+ (@fallback_logger ||= self.class.mk_logger($stderr))
40
+ end
43
41
 
44
- private
45
-
46
- # log level is, by precedence, one of: $RACK_TIMEOUT_LOG_LEVEL > $LOG_LEVEL > INFO
47
- def self.determine_level
48
- env_log_level = ENV.values_at("RACK_TIMEOUT_LOG_LEVEL", "LOG_LEVEL").compact.map(&:upcase).first
49
- env_log_level = const_get(env_log_level) if env_log_level && const_defined?(env_log_level)
50
- env_log_level || DEFAULT_LEVEL
51
- end
52
-
53
- # helper method used for formatting in #log_state_change
54
- def ms(s)
55
- '%.fms' % (s * 1000)
56
- end
42
+ # helper method used for formatting in #log_state_change
43
+ def ms(s)
44
+ '%.fms' % (s * 1000)
45
+ end
57
46
 
58
- # generates the actual log string
59
- def log_state_change(info)
60
- add(STATE_LOG_LEVEL[info.state]) do
61
- s = 'source=rack-timeout'
62
- s << ' id=' << info.id if info.id
63
- s << ' age=' << ms(info.age) if info.age
64
- s << ' timeout=' << ms(info.timeout) if info.timeout
65
- s << ' duration=' << ms(info.duration) if info.duration
66
- s << ' state=' << info.state.to_s if info.state
67
- s
68
- end
47
+ # generates the actual log string
48
+ def log_state_change(env)
49
+ info = env[ENV_INFO_KEY]
50
+ level = STATE_LOG_LEVEL[info.state]
51
+ logger(env).send(level) do
52
+ s = 'source=rack-timeout'
53
+ s << ' id=' << info.id if info.id
54
+ s << ' wait=' << ms(info.wait) if info.wait
55
+ s << ' timeout=' << ms(info.timeout) if info.timeout
56
+ s << ' service=' << ms(info.service) if info.service
57
+ s << ' state=' << info.state.to_s if info.state
58
+ s
69
59
  end
70
-
71
60
  end
61
+
72
62
  end
73
63
  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.0beta3
4
+ version: 0.1.0beta4
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-07-01 00:00:00.000000000 Z
11
+ date: 2014-09-30 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,9 +19,9 @@ extra_rdoc_files: []
19
19
  files:
20
20
  - MIT-LICENSE
21
21
  - README.markdown
22
- - lib/rack/timeout/logger.rb
23
- - lib/rack/timeout.rb
24
22
  - lib/rack-timeout.rb
23
+ - lib/rack/timeout.rb
24
+ - lib/rack/timeout/logger.rb
25
25
  homepage: http://github.com/kch/rack-timeout
26
26
  licenses:
27
27
  - MIT
@@ -32,17 +32,17 @@ 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: []
44
44
  rubyforge_project:
45
- rubygems_version: 2.0.3
45
+ rubygems_version: 2.2.2
46
46
  signing_key:
47
47
  specification_version: 4
48
48
  summary: Abort requests that are taking too long