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 +4 -4
- data/README.markdown +7 -8
- data/lib/rack-timeout.rb +1 -1
- data/lib/rack/timeout.rb +121 -54
- data/lib/rack/timeout/logger.rb +50 -60
- metadata +7 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 36207506f9394cfa73916a31f64a957c79677931
|
4
|
+
data.tar.gz: 5da1c876b06d0e29cd507aac3d2374d8006e2a36
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9035faeb15e556cc8a908124e81a2f4b76440baee9e77ed68f7685730bedb670928631db0def7c356ef3c2dc61924bc72f7261139f70e37f6f41f3b4b282d197
|
7
|
+
data.tar.gz: c08a2cd396936bad0970a21f251da1fd5c573cf3585b77d954a149f8b75230b03fbe4677f009690052c26d97d2b85d07bd812b818db324c1e255d197052a8fba
|
data/README.markdown
CHANGED
@@ -1,5 +1,4 @@
|
|
1
|
-
README is not
|
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
|
-
* `
|
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
|
-
* `
|
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 `
|
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
|
234
|
-
source=rack-timeout id=13793c
|
235
|
-
source=rack-timeout id=ea7bd3
|
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
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
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
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
|
25
|
-
info.id
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
if
|
33
|
-
|
34
|
-
|
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
|
-
|
38
|
-
|
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.
|
45
|
-
sleep_seconds = [1, info.timeout - info.
|
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
|
-
|
98
|
+
RT._set_state! env, :active
|
48
99
|
sleep(sleep_seconds)
|
49
100
|
end
|
50
|
-
|
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.
|
60
|
-
|
110
|
+
info.service = Time.now - time_started_service
|
111
|
+
RT._set_state! env, :completed
|
61
112
|
response
|
62
113
|
end
|
63
114
|
|
64
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
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
|
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.
|
172
|
+
@state_change_observers.values.each { |observer| observer.call(env) }
|
106
173
|
end
|
107
174
|
|
108
175
|
end
|
data/lib/rack/timeout/logger.rb
CHANGED
@@ -1,73 +1,63 @@
|
|
1
1
|
require 'logger'
|
2
2
|
|
3
|
-
|
4
|
-
class
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
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.
|
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:
|
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.
|
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
|