system_timer 1.0 → 1.2.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,11 +1,26 @@
1
- require 'rubygems'
1
+ # Copyright 2008 David Vollbracht & Philippe Hanrigou
2
+
3
+ if defined?(RUBY_ENGINE) and RUBY_ENGINE == "rbx"
4
+ require File.dirname(__FILE__) + '/system_timer_stub'
5
+ else
6
+
7
+ require 'thread'
2
8
  require 'timeout'
9
+ require 'forwardable'
10
+ require 'monitor'
11
+ require File.dirname(__FILE__) + '/system_timer/thread_timer'
12
+ require File.dirname(__FILE__) + '/system_timer/concurrent_timer_pool'
3
13
 
4
- # Timer based on underlying SIGALRM system timers, is a
14
+ # Timer based on underlying +ITIMER_REAL+ system timer. It is a
5
15
  # solution to Ruby processes which hang beyond the time limit when accessing
6
16
  # external resources. This is useful when timeout.rb, which relies on green
7
17
  # threads, does not work consistently.
8
18
  #
19
+ # For more information and background check out:
20
+ #
21
+ # * http://ph7spot.com/articles/system_timer
22
+ # * http://davidvollbracht.com/2008/6/2/30-days-of-teach-day-1-systemtimer
23
+ #
9
24
  # == Usage
10
25
  #
11
26
  # require 'systemtimer'
@@ -17,56 +32,84 @@ require 'timeout'
17
32
  #
18
33
  # end
19
34
  #
20
- module SystemTimer
21
- class << self
22
-
23
- # Executes the method's block. If the block execution terminates before
24
- # +seconds+ seconds has passed, it returns true. If not, it terminates
25
- # the execution and raises a +Timeout::Error+.
26
- def timeout_after(seconds)
27
- install_timer(seconds)
28
- return yield
29
- ensure
30
- cleanup_timer
31
- end
35
+ module SystemTimer
36
+
37
+ Thread.exclusive do # Avoid race conditions for monitor and pool creation
38
+ @timer_pool = ConcurrentTimerPool.new
39
+ @monitor = Monitor.new
40
+ end
41
+
42
+ class << self
43
+ attr_reader :timer_pool
44
+
45
+ # Executes the method's block. If the block execution terminates before
46
+ # +seconds+ seconds has passed, it returns true. If not, it terminates
47
+ # the execution and raises a +Timeout::Error+.
48
+ def timeout_after(seconds, exception_class = nil)
49
+ new_timer = nil # just for scope
50
+ @monitor.synchronize do
51
+ new_timer = timer_pool.add_timer seconds, exception_class
52
+ timer_interval = timer_pool.next_trigger_interval_in_seconds
53
+ debug "==== Install Timer ==== at #{Time.now.to_f}, next interval: #{timer_interval}"
54
+ if timer_pool.first_timer?
55
+ install_first_timer_and_save_original_configuration timer_interval
56
+ else
57
+ install_next_timer timer_interval
58
+ end
59
+ end
60
+ return yield
61
+ ensure
62
+ @monitor.synchronize do
63
+ debug "==== Cleanup Timer ==== at #{Time.now.to_f}, #{new_timer} "
64
+ timer_pool.cancel new_timer
65
+ timer_pool.log_registered_timers if debug_enabled?
66
+ next_interval = timer_pool.next_trigger_interval_in_seconds
67
+ debug "Cleanup Timer : next interval #{next_interval.inspect} "
68
+ if next_interval
69
+ install_next_timer next_interval
70
+ else
71
+ restore_original_configuration
72
+ end
73
+ end
74
+ end
75
+
76
+ # Backward compatibility with timeout.rb
77
+ alias timeout timeout_after
32
78
 
33
- # Backward compatibility with timeout.rb
34
- alias timeout timeout_after
35
-
36
79
  protected
37
-
38
- def install_ruby_sigalrm_handler #:nodoc:
39
- timed_thread = Thread.current # Ruby signals are always delivered to main thread by default.
80
+
81
+ def install_ruby_sigalrm_handler #:nodoc:
40
82
  @original_ruby_sigalrm_handler = trap('SIGALRM') do
41
- log_timeout_received(timed_thread) if SystemTimer.debug_enabled?
42
- timed_thread.raise Timeout::Error.new("time's up!")
43
- end
83
+ @monitor.synchronize do
84
+ # Triggers timers one at a time to ensure more deterministic results
85
+ timer_pool.trigger_next_expired_timer
86
+ end
87
+ end
44
88
  end
45
-
46
- def restore_original_ruby_sigalrm_handler #:nodoc:
89
+
90
+ def restore_original_ruby_sigalrm_handler #:nodoc:
47
91
  trap('SIGALRM', original_ruby_sigalrm_handler || 'DEFAULT')
48
92
  ensure
49
93
  reset_original_ruby_sigalrm_handler
50
94
  end
51
-
52
- def original_ruby_sigalrm_handler #:nodoc:
95
+
96
+ def original_ruby_sigalrm_handler #:nodoc:
53
97
  @original_ruby_sigalrm_handler
54
98
  end
55
-
56
- def reset_original_ruby_sigalrm_handler #:nodoc:
99
+
100
+ def reset_original_ruby_sigalrm_handler #:nodoc:
57
101
  @original_ruby_sigalrm_handler = nil
58
102
  end
59
103
 
60
- def log_timeout_received(timed_thread) #:nodoc:
61
- puts <<-EOS
62
- install_ruby_sigalrm_handler: Got Timeout in #{Thread.current}
63
- Main thread : #{Thread.main}
64
- Timed_thread : #{timed_thread}
65
- All Threads : #{Thread.list.inspect}
66
- EOS
104
+ def debug(message) #:nodoc
105
+ puts message if debug_enabled?
67
106
  end
107
+
68
108
  end
69
109
 
70
110
  end
71
111
 
72
- require 'system_timer_native'
112
+ require 'system_timer_native'
113
+
114
+
115
+ end # stub guard
@@ -0,0 +1,89 @@
1
+ # Copyright 2008 David Vollbracht & Philippe Hanrigou
2
+
3
+ module SystemTimer
4
+
5
+ class ConcurrentTimerPool
6
+
7
+ def registered_timers
8
+ @timers ||= []
9
+ end
10
+
11
+ def register_timer(trigger_time, thread, exception_class=nil)
12
+ new_timer = ThreadTimer.new(trigger_time, thread, exception_class)
13
+ registered_timers << new_timer
14
+ new_timer
15
+ end
16
+
17
+ def add_timer(interval_in_seconds, exception_class=nil)
18
+ new_timer = register_timer(Time.now.to_f + interval_in_seconds, Thread.current, exception_class)
19
+ log_registered_timers if SystemTimer.debug_enabled?
20
+ new_timer
21
+ end
22
+
23
+ def cancel(registered_timer)
24
+ registered_timers.delete registered_timer
25
+ end
26
+
27
+ def first_timer?
28
+ registered_timers.size == 1
29
+ end
30
+
31
+ def next_timer
32
+ registered_timers.sort {|x,y| x.trigger_time <=> y.trigger_time}.first
33
+ end
34
+
35
+ def next_trigger_time
36
+ timer = next_timer
37
+ timer.trigger_time unless timer.nil?
38
+ end
39
+
40
+ def next_trigger_interval_in_seconds
41
+ timer = next_timer
42
+ [0, (timer.trigger_time - Time.now.to_f)].max unless timer.nil?
43
+ end
44
+
45
+ def next_expired_timer(now_in_seconds_since_epoch)
46
+ candidate_timer = next_timer
47
+ if SystemTimer.debug_enabled?
48
+ puts "Candidate timer at #{now_in_seconds_since_epoch} : " +
49
+ candidate_timer.inspect
50
+ end
51
+ return nil if candidate_timer.nil? ||
52
+ candidate_timer.trigger_time > now_in_seconds_since_epoch
53
+ candidate_timer
54
+ end
55
+
56
+ def trigger_next_expired_timer_at(now_in_seconds_since_epoch)
57
+ timer = next_expired_timer(now_in_seconds_since_epoch)
58
+ puts "Next expired timer : #{timer.inspect}" if SystemTimer.debug_enabled?
59
+ return if timer.nil?
60
+
61
+ cancel timer
62
+ log_timeout_received(timer) if SystemTimer.debug_enabled?
63
+ timer.thread.raise timer.exception_class.new("time's up!")
64
+ end
65
+
66
+ def trigger_next_expired_timer
67
+ puts "Trigger next expired timer" if SystemTimer.debug_enabled?
68
+ trigger_next_expired_timer_at Time.now.to_f
69
+ end
70
+
71
+ def log_timeout_received(thread_timer) #:nodoc:
72
+ puts <<-EOS
73
+ ==== Triger Timer ==== #{thread_timer}
74
+ Main thread : #{Thread.main}
75
+ Timed_thread : #{thread_timer.thread}
76
+ All Threads : #{Thread.list.inspect}
77
+ EOS
78
+ log_registered_timers
79
+ end
80
+
81
+ def log_registered_timers #:nodoc:
82
+ puts <<-EOS
83
+ Registered Timers: #{registered_timers.map {|t| t.to_s}.join("\n ")}
84
+ EOS
85
+ end
86
+
87
+ end
88
+
89
+ end
@@ -0,0 +1,22 @@
1
+ # Copyright 2008 David Vollbracht & Philippe Hanrigou
2
+
3
+ module SystemTimer
4
+
5
+ # Timer saving associated thread. This is needed because we trigger timers
6
+ # from a Ruby signal handler and Ruby signals are always delivered to
7
+ # main thread.
8
+ class ThreadTimer
9
+ attr_reader :trigger_time, :thread, :exception_class
10
+
11
+ def initialize(trigger_time, thread, exception_class = nil)
12
+ @trigger_time = trigger_time
13
+ @thread = thread
14
+ @exception_class = exception_class || Timeout::Error
15
+ end
16
+
17
+ def to_s
18
+ "<ThreadTimer :time => #{trigger_time}, :thread => #{thread}, :exception_class => #{exception_class}>"
19
+ end
20
+
21
+ end
22
+ end
@@ -1,4 +1,5 @@
1
- require 'rubygems'
1
+ # Copyright 2008 David Vollbracht & Philippe Hanrigou
2
+
2
3
  require 'timeout'
3
4
 
4
5
  module SystemTimer
@@ -9,6 +10,9 @@ module SystemTimer
9
10
  yield
10
11
  end
11
12
  end
13
+
14
+ # Backward compatibility with timeout.rb
15
+ alias timeout timeout_after
12
16
 
13
17
  end
14
18
 
@@ -1 +1,3 @@
1
- Dir["#{File.dirname __FILE__}/*_test.rb"].each { |test_case| require test_case }
1
+ Dir["#{File.dirname __FILE__}/**/*_test.rb"].each do |test_case|
2
+ require test_case
3
+ end
@@ -0,0 +1,291 @@
1
+ require File.dirname(__FILE__) + '/../test_helper'
2
+
3
+ unit_tests do
4
+
5
+ test "registered_timers is empty when there is no registered timers" do
6
+ assert_equal [], SystemTimer::ConcurrentTimerPool.new.registered_timers
7
+ end
8
+
9
+ test "a new timer is added to the registered timer list when you register a timer" do
10
+ pool = SystemTimer::ConcurrentTimerPool.new
11
+ pool.register_timer :a_trigger_time, :a_thread
12
+ assert_equal [[:a_trigger_time, :a_thread]],
13
+ pool.registered_timers.collect {|t| [t.trigger_time, t.thread] }
14
+ end
15
+
16
+ test "register_timer returns the timer that was just added to the pool" do
17
+ pool = SystemTimer::ConcurrentTimerPool.new
18
+ timer = pool.register_timer :a_trigger_time, :a_thread
19
+ assert_equal [:a_trigger_time, :a_thread], [timer.trigger_time, timer.thread]
20
+ end
21
+
22
+ test "add_timer is a shortcut method to register a timer given its interval" do
23
+ pool = SystemTimer::ConcurrentTimerPool.new
24
+ Thread.stubs(:current).returns(:the_current_thread)
25
+ now = Time.now
26
+ Time.stubs(:now).returns(now)
27
+
28
+ pool.expects(:register_timer).with(now.to_f + 15, :the_current_thread, nil)
29
+ pool.add_timer 15
30
+ end
31
+
32
+ test "cancel removes a timer from the registered timer list" do
33
+ pool = SystemTimer::ConcurrentTimerPool.new
34
+ registered_timer = pool.register_timer :a_trigger_time, :a_thread
35
+ pool.cancel registered_timer
36
+ assert_equal [], pool.registered_timers
37
+ end
38
+
39
+ test "cancel does not complain when timer is cancelled " +
40
+ "(useful for ensure blocks)" do
41
+
42
+ pool = SystemTimer::ConcurrentTimerPool.new
43
+ a_timer = pool.add_timer 123
44
+ another_timer = pool.add_timer 456
45
+ pool.cancel(another_timer)
46
+ pool.cancel(another_timer)
47
+ assert_equal [a_timer], pool.registered_timers
48
+ end
49
+
50
+ test "first_timer? returns false when there is no timer" do
51
+ assert_equal false, SystemTimer::ConcurrentTimerPool.new.first_timer?
52
+ end
53
+
54
+ test "first_timer? returns true when there is a single timer" do
55
+ pool = SystemTimer::ConcurrentTimerPool.new
56
+ pool.add_timer 7
57
+ assert_equal true, pool.first_timer?
58
+ end
59
+
60
+ test "first_timer? returns false when there is more than one timer" do
61
+ pool = SystemTimer::ConcurrentTimerPool.new
62
+ pool.add_timer 7
63
+ pool.add_timer 3
64
+ assert_equal false, pool.first_timer?
65
+ end
66
+
67
+ test "first_timer? returns false when there is a single timer left" do
68
+ pool = SystemTimer::ConcurrentTimerPool.new
69
+ first_timer = pool.add_timer 7
70
+ pool.add_timer 3
71
+ pool.cancel first_timer
72
+ assert_equal true, pool.first_timer?
73
+ end
74
+
75
+ test "next expired timer return nil when there is no registered timer" do
76
+ assert_nil SystemTimer::ConcurrentTimerPool.new.next_expired_timer(24)
77
+ end
78
+
79
+ test "next_timer returns nil when there is no registered timer" do
80
+ assert_nil SystemTimer::ConcurrentTimerPool.new.next_timer
81
+ end
82
+
83
+ test "next_timer returns the registered timer when " +
84
+ "there is only one registered timer" do
85
+
86
+ pool = SystemTimer::ConcurrentTimerPool.new
87
+ the_timer = pool.register_timer 24, stub_everything
88
+ assert_equal the_timer, pool.next_timer
89
+ end
90
+
91
+ test "next_timer returns the trigger time of the first timer to" +
92
+ "expire when there is more than one registered timer" do
93
+
94
+ pool = SystemTimer::ConcurrentTimerPool.new
95
+ late_timer = pool.register_timer 64, stub_everything
96
+ early_timer = pool.register_timer 24, stub_everything
97
+ assert_equal early_timer, pool.next_timer
98
+ end
99
+
100
+ test "next_trigger_time returns nil when next_timer is nil" do
101
+ pool = SystemTimer::ConcurrentTimerPool.new
102
+ pool.expects(:next_timer).returns(nil)
103
+ assert_nil pool.next_trigger_time
104
+ end
105
+
106
+ test "next_trigger_time returns trigger time of next timer when " +
107
+ "next timer is not nil" do
108
+
109
+ pool = SystemTimer::ConcurrentTimerPool.new
110
+ the_timer = SystemTimer::ThreadTimer.new 24, stub_everything
111
+ pool.expects(:next_timer).returns(the_timer)
112
+ assert_equal 24, pool.next_trigger_time
113
+ end
114
+
115
+ test "next_trigger_interval_in_seconds returns nil when next_timer is nil" do
116
+ pool = SystemTimer::ConcurrentTimerPool.new
117
+ pool.expects(:next_timer).returns(nil)
118
+ assert_nil pool.next_trigger_interval_in_seconds
119
+ end
120
+
121
+ test "next_trigger_interval_in_seconds returns the interval between now and " +
122
+ "next_timer timer time when next timer is in the future" do
123
+ pool = SystemTimer::ConcurrentTimerPool.new
124
+ now = Time.now
125
+ Time.stubs(:now).returns(now)
126
+ next_timer = SystemTimer::ThreadTimer.new((now.to_f + 7), stub_everything)
127
+ pool.expects(:next_timer).returns(next_timer)
128
+ assert_equal 7, pool.next_trigger_interval_in_seconds
129
+ end
130
+
131
+ test "next_trigger_interval_in_seconds returns 0 when next timer is now" do
132
+ pool = SystemTimer::ConcurrentTimerPool.new
133
+ now = Time.now
134
+ Time.stubs(:now).returns(now)
135
+ next_timer = SystemTimer::ThreadTimer.new now.to_f, stub_everything
136
+ pool.expects(:next_timer).returns(next_timer)
137
+ assert_equal 0, pool.next_trigger_interval_in_seconds
138
+ end
139
+
140
+ test "next_trigger_interval_in_seconds returns 0 when next timer is in the past" do
141
+ pool = SystemTimer::ConcurrentTimerPool.new
142
+ now = Time.now
143
+ Time.stubs(:now).returns(now)
144
+ next_timer = SystemTimer::ThreadTimer.new((now.to_f - 3), stub_everything)
145
+ pool.expects(:next_timer).returns(next_timer)
146
+ assert_equal 0, pool.next_trigger_interval_in_seconds
147
+ end
148
+
149
+ test "next_expired_timer returns the timer that was trigerred" +
150
+ "when a timer has expired" do
151
+
152
+ pool = SystemTimer::ConcurrentTimerPool.new
153
+ the_timer = pool.register_timer 24, :a_thread
154
+ assert_equal the_timer, pool.next_expired_timer(24)
155
+ end
156
+
157
+ test "next_expired_timer returns nil when no timer has expired yet" do
158
+ pool = SystemTimer::ConcurrentTimerPool.new
159
+ pool.register_timer 24, :a_thread
160
+ assert_nil pool.next_expired_timer(23)
161
+ end
162
+
163
+ test "next_expired_timer returns the timer that first expired " +
164
+ "when there is more than one expired timer" do
165
+
166
+ pool = SystemTimer::ConcurrentTimerPool.new
167
+ last_to_expire = pool.register_timer 64, :a_thread
168
+ first_to_expire = pool.register_timer 24, :a_thread
169
+ assert_equal first_to_expire, pool.next_expired_timer(100)
170
+ end
171
+
172
+ test "trigger_next_expired_timer_at does not raise when there is no registered timer" do
173
+ SystemTimer::ConcurrentTimerPool.new.trigger_next_expired_timer_at 1234
174
+ end
175
+
176
+ test "trigger_next_expired_timer_at raises a TimeoutError in the context of " +
177
+ "its thread when there is a registered timer that has expired" do
178
+
179
+ pool = SystemTimer::ConcurrentTimerPool.new
180
+ the_thread = mock('thread')
181
+
182
+ Timeout::Error.expects(:new).with("time's up!").returns(:the_exception)
183
+ the_thread.expects(:raise).with(:the_exception)
184
+ pool.register_timer 24, the_thread
185
+ pool.trigger_next_expired_timer_at 24
186
+ end
187
+
188
+ test "trigger_next_expired_timer_at does not raise when registered timer has not expired" do
189
+ pool = SystemTimer::ConcurrentTimerPool.new
190
+ pool.register_timer 24, stub_everything
191
+ pool.trigger_next_expired_timer_at(10)
192
+ end
193
+
194
+ test "trigger_next_expired_timer_at triggers the first registered timer that expired" do
195
+ pool = SystemTimer::ConcurrentTimerPool.new
196
+ first_to_expire = pool.register_timer 24, stub_everything
197
+ second_to_expire = pool.register_timer 64, stub_everything
198
+ pool.trigger_next_expired_timer_at(100)
199
+ assert_equal [second_to_expire], pool.registered_timers
200
+ end
201
+
202
+ test "trigger_next_expired_timer_at triggers the first registered timer that " +
203
+ "expired whatever the timer insertion order is" do
204
+
205
+ pool = SystemTimer::ConcurrentTimerPool.new
206
+ second_to_expire = pool.register_timer 64, stub_everything
207
+ first_to_expire = pool.register_timer 24, stub_everything
208
+ pool.trigger_next_expired_timer_at(100)
209
+ assert_equal [second_to_expire], pool.registered_timers
210
+ end
211
+
212
+ test "trigger_next_expired_timer_at remove the expired timer from the pool" do
213
+ pool = SystemTimer::ConcurrentTimerPool.new
214
+ pool.register_timer 24, stub_everything
215
+ pool.trigger_next_expired_timer_at 24
216
+ end
217
+
218
+ test "trigger_next_expired_timer_at logs timeout a registered timer has expired" +
219
+ "and SystemTimer debug mode is enabled " do
220
+
221
+ original_stdout = $stdout
222
+ begin
223
+ stdout = StringIO.new
224
+ $stdout = stdout
225
+
226
+ pool = SystemTimer::ConcurrentTimerPool.new
227
+ the_timer = pool.register_timer 24, stub_everything
228
+ SystemTimer.stubs(:debug_enabled?).returns(true)
229
+
230
+ pool.expects(:log_timeout_received).with(the_timer)
231
+ pool.trigger_next_expired_timer_at 24
232
+ ensure
233
+ $stdout = original_stdout
234
+ end
235
+ end
236
+
237
+ test "trigger_next_expired_timer_at does not logs timeoout when SystemTimer " +
238
+ "debug mode is disabled " do
239
+
240
+ pool = SystemTimer::ConcurrentTimerPool.new
241
+ the_timer = pool.register_timer 24, stub_everything
242
+ SystemTimer.stubs(:debug_enabled?).returns(false)
243
+
244
+ pool.expects(:log_timeout_received).never
245
+ pool.trigger_next_expired_timer_at 24
246
+ end
247
+
248
+ test "trigger_next_expired_timer_at does not logs timeout no registered timer " +
249
+ "has expired and SystemTimer debug mode is enabled " do
250
+
251
+ original_stdout = $stdout
252
+ begin
253
+ stdout = StringIO.new
254
+ $stdout = stdout
255
+
256
+ pool = SystemTimer::ConcurrentTimerPool.new
257
+ the_timer = pool.register_timer 24, stub_everything
258
+ SystemTimer.stubs(:debug_enabled?).returns(true)
259
+
260
+ pool.expects(:log_timeout_received).never
261
+ pool.trigger_next_expired_timer_at 23
262
+ ensure
263
+ $stdout = original_stdout
264
+ end
265
+ end
266
+
267
+ test "trigger_next_expired_timer is a shorcut method calling " +
268
+ "trigger_next_expired_timer_at with current epoch time" do
269
+
270
+ now = Time.now
271
+ pool = SystemTimer::ConcurrentTimerPool.new
272
+ Time.stubs(:now).returns(now)
273
+
274
+ pool.expects(:trigger_next_expired_timer_at).with(now.to_f)
275
+ pool.trigger_next_expired_timer
276
+ end
277
+
278
+ test "log_timeout_received does not raise" do
279
+ original_stdout = $stdout
280
+ begin
281
+ stdout = StringIO.new
282
+ $stdout = stdout
283
+
284
+ SystemTimer::ConcurrentTimerPool.new.log_timeout_received(SystemTimer::ThreadTimer.new(:a_time, :a_thread))
285
+ assert_match %r{==== Triger Timer ====}, stdout.string
286
+ ensure
287
+ $stdout = original_stdout
288
+ end
289
+ end
290
+
291
+ end