rack-timeout 0.3.0.pre.beta.1 → 0.3.0.pre.beta.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +4 -0
- data/lib/rack/timeout/core.rb +21 -22
- data/lib/rack/timeout/support/assert-types.rb +14 -0
- data/lib/rack/timeout/support/namespace.rb +5 -0
- data/lib/rack/timeout/support/scheduler.rb +165 -0
- data/lib/rack/timeout/support/timeout.rb +29 -0
- metadata +5 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7c018ab1119888f57da3ec644e8433d14587cdd9
|
4
|
+
data.tar.gz: e7cdb91385abb79936cbcdc63f27b6626703ba57
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5a135b2e0bef3a3c98cf7705d0356a52b188b0256e333ca560043edba9ecab1f2ceeabbc4913d928103018ac9d95052b55eb088faacb7bb72de8b8745959a514
|
7
|
+
data.tar.gz: c0e043f414aba0f18d4adbb15dbc1adc7a7ae70dce9eea38a93fdffd81338ae032626b042f6d96c294640037c0fb093d277c341eabfda6c8bc7419a1da228b9e
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
0.3.0-beta.2
|
2
|
+
============
|
3
|
+
- use a single scheduler thread to manage timeouts, instead of one timeout thread per request
|
4
|
+
|
1
5
|
0.3.0-beta.1
|
2
6
|
============
|
3
7
|
- instead of inserting middleware at position 0 for rails, insert before Rack::Runtime (which is right after Rack::Lock and the static file stuff)
|
data/lib/rack/timeout/core.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require "securerandom"
|
3
|
+
require_relative "support/scheduler"
|
4
|
+
require_relative "support/timeout"
|
3
5
|
|
4
6
|
module Rack
|
5
7
|
class Timeout
|
@@ -73,6 +75,7 @@ module Rack
|
|
73
75
|
@app = app
|
74
76
|
end
|
75
77
|
|
78
|
+
|
76
79
|
RT = self # shorthand reference
|
77
80
|
def call(env)
|
78
81
|
info = (env[ENV_INFO_KEY] ||= RequestDetails.new)
|
@@ -104,32 +107,28 @@ module Rack
|
|
104
107
|
info.timeout = seconds_service_left if !RT.service_past_wait && seconds_service_left && seconds_service_left > 0 && seconds_service_left < RT.service_timeout
|
105
108
|
|
106
109
|
RT._set_state! env, :ready
|
107
|
-
begin
|
108
|
-
app_thread = Thread.current
|
109
|
-
timeout_thread = Thread.start do
|
110
|
-
loop do
|
111
|
-
info.service = Time.now - time_started_service
|
112
|
-
sleep_seconds = [1 - (info.service % 1), info.timeout - info.service].min
|
113
|
-
break if sleep_seconds <= 0
|
114
|
-
RT._set_state! env, :active
|
115
|
-
sleep(sleep_seconds)
|
116
|
-
end
|
117
|
-
RT._set_state! env, :timed_out
|
118
|
-
app_thread.raise(RequestTimeoutException.new(env), "Request #{"waited #{info.ms(:wait)}, then " if info.wait}ran for longer than #{info.ms(:timeout)}")
|
119
|
-
end
|
120
|
-
|
121
|
-
response = @app.call(env)
|
122
110
|
|
123
|
-
|
124
|
-
|
111
|
+
heartbeat_event = nil
|
112
|
+
update_service = ->(status = :active) {
|
113
|
+
heartbeat_event.cancel! if status != :active
|
114
|
+
info.service = Time.now - time_started_service
|
115
|
+
RT._set_state! env, status
|
116
|
+
}
|
117
|
+
heartbeat_event = RT::Scheduler.run_every(1) { update_service.call :active }
|
118
|
+
|
119
|
+
timeout = RT::Scheduler::Timeout.new do |app_thread|
|
120
|
+
update_service.call :timed_out
|
121
|
+
app_thread.raise(RequestTimeoutException.new(env), "Request #{"waited #{info.ms(:wait)}, then " if info.wait}ran for longer than #{info.ms(:timeout)}")
|
122
|
+
end
|
125
123
|
|
126
|
-
|
127
|
-
|
128
|
-
|
124
|
+
response = timeout.timeout(info.timeout) do
|
125
|
+
begin @app.call(env)
|
126
|
+
rescue RequestTimeoutException => e
|
127
|
+
raise RequestTimeoutError.new(env), e.message, e.backtrace
|
128
|
+
end
|
129
129
|
end
|
130
130
|
|
131
|
-
|
132
|
-
RT._set_state! env, :completed
|
131
|
+
update_service.call :completed
|
133
132
|
response
|
134
133
|
end
|
135
134
|
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative "namespace"
|
2
|
+
|
3
|
+
module Rack::Timeout::AssertTypes
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def assert_types! value_type_map
|
7
|
+
value_type_map.each do |val, types|
|
8
|
+
types = [types] unless types.is_a? Array
|
9
|
+
next if types.any? { |type| val.is_a? type }
|
10
|
+
raise TypeError, "#{val.inspect} is not a #{types.join(" | ")}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require_relative "namespace"
|
3
|
+
require_relative "assert-types"
|
4
|
+
|
5
|
+
# Runs code at a later time
|
6
|
+
#
|
7
|
+
# Basic usage:
|
8
|
+
#
|
9
|
+
# Scheduler.run_in(5) { do_stuff } # <- calls do_stuff 5 seconds from now
|
10
|
+
#
|
11
|
+
# Scheduled events run in sequence in a separate thread, the main thread continues on.
|
12
|
+
# That means you may need to #join the scheduler if the main thread is only waiting on scheduled events to run.
|
13
|
+
#
|
14
|
+
# Scheduler.join
|
15
|
+
#
|
16
|
+
# Basic usage is through a singleton instance, its methods are available as class methods, as shown above.
|
17
|
+
# One could also instantiate separate instances which would get you separate run threads, but generally there's no point in it.
|
18
|
+
class Rack::Timeout::Scheduler
|
19
|
+
MAX_IDLE_SECS = 30 # how long the runner thread is allowed to live doing nothing
|
20
|
+
include Rack::Timeout::AssertTypes
|
21
|
+
|
22
|
+
# stores a proc to run later, and the time it should run at
|
23
|
+
class RunEvent < Struct.new(:time, :proc)
|
24
|
+
def initialize(time, proc)
|
25
|
+
Rack::Timeout::AssertTypes.assert_types! time => Time, proc => Proc
|
26
|
+
super
|
27
|
+
end
|
28
|
+
|
29
|
+
def cancel!
|
30
|
+
@cancelled = true
|
31
|
+
end
|
32
|
+
|
33
|
+
def cancelled?
|
34
|
+
!!@cancelled
|
35
|
+
end
|
36
|
+
|
37
|
+
def run!
|
38
|
+
return if @cancelled
|
39
|
+
proc.call(self)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class RepeatEvent < RunEvent
|
44
|
+
def initialize(time, proc, every)
|
45
|
+
@start = time
|
46
|
+
@every = every
|
47
|
+
@iter = 0
|
48
|
+
super(time, proc)
|
49
|
+
end
|
50
|
+
|
51
|
+
def run!
|
52
|
+
super
|
53
|
+
ensure
|
54
|
+
self.time = @start + @every * (@iter += 1) until time >= Time.now
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def initialize
|
59
|
+
@events = [] # array of `RunEvent`s
|
60
|
+
@mx_events = Mutex.new # mutex to change said array
|
61
|
+
@mx_runner = Mutex.new # mutex for creating a runner thread
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
# returns the runner thread, creating it if needed
|
68
|
+
def runner
|
69
|
+
@mx_runner.synchronize {
|
70
|
+
return @runner unless @runner.nil? || !@runner.alive?
|
71
|
+
@joined = false
|
72
|
+
@runner = Thread.new { run_loop! }
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
# the actual runner thread loop
|
77
|
+
def run_loop!
|
78
|
+
Thread.current.abort_on_exception = true # always be aborting
|
79
|
+
sleep_for, run, last_run = nil, nil, Time.now # sleep_for: how long to sleep before next run; last_run: time of last run; run: just initializing it outside of the synchronize scope, will contain events to run now
|
80
|
+
loop do # begin event reader loop
|
81
|
+
@mx_events.synchronize { #
|
82
|
+
@events.reject!(&:cancelled?) # get rid of cancelled events
|
83
|
+
if @events.empty? # if there are no further events …
|
84
|
+
return if @joined # exit the run loop if this runner thread has been joined (the thread will die and the join will return)
|
85
|
+
return if Time.now - last_run > MAX_IDLE_SECS # exit the run loop if done nothing for the past MAX_IDLE_SECS seconds
|
86
|
+
sleep_for = MAX_IDLE_SECS # sleep for MAX_IDLE_SECS (mind it that we get awaken when new events are scheduled)
|
87
|
+
else #
|
88
|
+
sleep_for = [@events.map(&:time).min - Time.now, 0].max # if we have events, set to sleep until it's time for the next one to run. (the max bit ensure we don't have negative sleep times)
|
89
|
+
end #
|
90
|
+
@mx_events.sleep sleep_for # do sleep
|
91
|
+
#
|
92
|
+
now = Time.now #
|
93
|
+
run, defer = @events.partition { |ev| ev.time <= now } # separate events to run now and events to run later
|
94
|
+
defer += run.select { |ev| ev.is_a? RepeatEvent } # repeat events both run and are deferred
|
95
|
+
@events.replace(defer) # keep only events to run later
|
96
|
+
} #
|
97
|
+
#
|
98
|
+
next if run.empty? # done here if there's nothing to run now
|
99
|
+
run.sort_by(&:time).each { |ev| ev.run! } # run the events scheduled to run now
|
100
|
+
last_run = Time.now # store that we did run things at this time, go immediately on to the next loop iteration as it may be time to run more things
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
public
|
106
|
+
|
107
|
+
# waits on the runner thread to finish
|
108
|
+
def join
|
109
|
+
@joined = true
|
110
|
+
runner.join
|
111
|
+
end
|
112
|
+
|
113
|
+
# adds a RunEvent struct to the run schedule
|
114
|
+
def schedule(event)
|
115
|
+
assert_types! event => RunEvent
|
116
|
+
@mx_events.synchronize { @events << event }
|
117
|
+
runner.run # wakes up the runner thread so it can recalculate sleep length taking this new event into consideration
|
118
|
+
return event
|
119
|
+
end
|
120
|
+
|
121
|
+
# reschedules an event to run at a different time. returns nil and does nothing if the event is not already in the queue (might've run already), otherwise updates the event time in-place; returns the updated event
|
122
|
+
def reschedule(event, time)
|
123
|
+
assert_types! event => RunEvent, time => Time
|
124
|
+
@mx_events.synchronize {
|
125
|
+
return unless @events.include? event
|
126
|
+
event.time = time
|
127
|
+
runner.run
|
128
|
+
return event
|
129
|
+
}
|
130
|
+
end
|
131
|
+
|
132
|
+
# reschedules an event by the given number of seconds. can be negative to run sooner.
|
133
|
+
def delay(event, secs)
|
134
|
+
reschedule(event, event.time + secs)
|
135
|
+
end
|
136
|
+
|
137
|
+
# schedules a block to run at a given time; returns the created event object
|
138
|
+
def run_at(time, &block)
|
139
|
+
schedule RunEvent.new(time, block)
|
140
|
+
end
|
141
|
+
|
142
|
+
# schedules a block to run in the given number of seconds; returns the created event object
|
143
|
+
def run_in(secs, &block)
|
144
|
+
run_at(Time.now + secs, &block)
|
145
|
+
end
|
146
|
+
|
147
|
+
# schedules a block to run every x seconds; returns the created event object
|
148
|
+
def run_every(seconds, &block)
|
149
|
+
schedule RepeatEvent.new(Time.now, block, seconds)
|
150
|
+
end
|
151
|
+
|
152
|
+
|
153
|
+
### Singleton access
|
154
|
+
|
155
|
+
# accessor to the singleton instance
|
156
|
+
def self.singleton
|
157
|
+
@singleton ||= new
|
158
|
+
end
|
159
|
+
|
160
|
+
# define public instance methods as class methods that delegate to the singleton instance
|
161
|
+
instance_methods(false).each do |m|
|
162
|
+
define_singleton_method(m) { |*a, &b| singleton.send(m, *a, &b) }
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require_relative "namespace"
|
2
|
+
require_relative "scheduler"
|
3
|
+
|
4
|
+
class Rack::Timeout::Scheduler::Timeout
|
5
|
+
class Error < RuntimeError; end
|
6
|
+
ON_TIMEOUT = ->thr { thr.raise Error, "execution expired" } # default action to take when a timeout happens
|
7
|
+
|
8
|
+
# initializes a timeout object with an optional block to handle the timeout differently. the block is passed the thread that's gone overtime.
|
9
|
+
def initialize(&on_timeout)
|
10
|
+
@on_timeout = on_timeout || ON_TIMEOUT
|
11
|
+
@scheduler = Rack::Timeout::Scheduler.singleton
|
12
|
+
end
|
13
|
+
|
14
|
+
# takes number of seconds to wait before timing out, and code block subject to time out
|
15
|
+
def timeout(secs, &block)
|
16
|
+
return block.call if secs.nil? || secs.zero? # skip timeout flow entirely for zero or nil
|
17
|
+
thr = Thread.current # reference to current thread to be used in timeout thread
|
18
|
+
job = @scheduler.run_in(secs) { @on_timeout.call thr } # schedule this thread to be timed out; should get cancelled if block completes on time
|
19
|
+
return block.call # do what you gotta do
|
20
|
+
ensure #
|
21
|
+
job.cancel! # cancel the scheduled timeout job; if the block completed on time, this
|
22
|
+
end # will get called before the timeout code's had a chance to run.
|
23
|
+
|
24
|
+
# timeout method on singleton instance for when a custom on_timeout is not required
|
25
|
+
def self.timeout(secs, &block)
|
26
|
+
(@singleton ||= new).timeout(secs, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rack-timeout
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.0.pre.beta.
|
4
|
+
version: 0.3.0.pre.beta.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Caio Chassot
|
@@ -27,6 +27,10 @@ files:
|
|
27
27
|
- lib/rack/timeout/logging-observer.rb
|
28
28
|
- lib/rack/timeout/rails.rb
|
29
29
|
- lib/rack/timeout/rollbar.rb
|
30
|
+
- lib/rack/timeout/support/assert-types.rb
|
31
|
+
- lib/rack/timeout/support/namespace.rb
|
32
|
+
- lib/rack/timeout/support/scheduler.rb
|
33
|
+
- lib/rack/timeout/support/timeout.rb
|
30
34
|
homepage: http://github.com/heroku/rack-timeout
|
31
35
|
licenses:
|
32
36
|
- MIT
|