rack-timeout 0.3.0.pre.beta.1 → 0.3.0.pre.beta.2
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/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
|