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 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