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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0115bc1c87b36ff9e6f74cb104e0c8fb13550685
4
- data.tar.gz: d2eaf62f154d099701b263db687897e15d3104c5
3
+ metadata.gz: 7c018ab1119888f57da3ec644e8433d14587cdd9
4
+ data.tar.gz: e7cdb91385abb79936cbcdc63f27b6626703ba57
5
5
  SHA512:
6
- metadata.gz: 61f9683c26d18d2da7b0b9d51834e86865af64b72ab2a54eea780b9522724c641a6156366122b9047f630298dd89d15c5d859ec539e88de0668a529aa2f71933
7
- data.tar.gz: 75c7d46b222be49a805e992f3d7c3a9abe769614cfc7edbb65393d9a7922ad6fab5de75a5c607ac8cee2fb629822fcb3bbd3bef37cccd4c74cf0799fb3fafcc7
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)
@@ -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
- rescue RequestTimeoutException => e
124
- raise RequestTimeoutError.new(env), e.message, e.backtrace
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
- ensure
127
- timeout_thread.kill
128
- timeout_thread.join
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
- info.service = Time.now - time_started_service
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,5 @@
1
+ # can be required by other files to prevent them from having to open and nest Rack and Timeout
2
+ module Rack
3
+ class Timeout
4
+ end
5
+ 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.1
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