rack-timeout 0.1.0beta3 → 0.1.0beta4
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 +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
|