Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

First pass at making SystemTimer handle concurrent timers

  • Loading branch information...
commit c359179a58ebb50f9dd6a6dfbb41c92e305e16d4 1 parent 3ebbc0b
@ph7 authored
View
13 experiments/Rakefile
@@ -0,0 +1,13 @@
+require 'rake/clean'
+
+PROG = "setitimer_process_isolation"
+
+CLEAN.include(PROG)
+
+task :default => PROG
+
+file PROG => "#{PROG}.c" do
+ sh "gcc -o #{PROG} #{PROG}.c"
+end
+
+
View
59 experiments/setitimer_process_isolation.c
@@ -0,0 +1,59 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <signal.h>
+#include <sys/time.h>
+
+/*
+ * Launch multiple instances of this program on your machine with
+ * different timeout values to convince yourself that itmer alarms
+ * are specific to a particular process.
+ */
+
+int timeout;
+
+void install_next_timer() {
+ struct itimerval val;
+
+ val.it_interval.tv_sec = 0; /* ie. no repeat count */
+ val.it_interval.tv_usec = 0;
+ val.it_value.tv_sec = timeout; /* initialise counter */
+ val.it_value.tv_usec = 0;
+
+ setitimer(ITIMER_REAL, &val, 0);
+}
+
+
+void custom_signal_handler()
+{
+ time_t the_time;
+
+ time(&the_time);
+ fprintf(stdout, "alarm ticked at %d (epoch time)\n", the_time);
+ install_next_timer();
+}
+
+
+main(int argc, char *argv[])
+{
+ int i;
+ struct itimerval val, curval;
+
+ if (argc != 2 ) {
+ fprintf(stderr, "Usage: %s seconds\n", argv[0]);
+ exit(1);
+ }
+
+ timeout = atoi(argv[1]);
+
+ fprintf(stdout, "Setting alarm every %ds\n", timeout);
+
+ signal(SIGALRM, custom_signal_handler);
+
+ install_next_timer();
+
+ setitimer(ITIMER_REAL, &val, 0);
+ for (;;) {
+ sleep(1);
+ }
+
+}
View
138 ext/system_timer/system_timer_native.c
@@ -2,6 +2,7 @@
#include "rubysig.h"
#include <signal.h>
#include <errno.h>
+#include <stdarg.h>
#define DISPLAY_ERRNO 1
#define DO_NOT_DISPLAY_ERRNO 0
@@ -13,7 +14,7 @@ struct sigaction original_signal_handler;
struct itimerval original_timer_interval;
static void clear_pending_sigalrm_for_ruby_threads();
-static void log_debug(char*);
+static void log_debug(char*, ...);
static void log_error(char*, int);
static void install_ruby_sigalrm_handler(VALUE);
static void restore_original_ruby_sigalrm_handler(VALUE);
@@ -23,73 +24,139 @@ static void set_itimerval(struct itimerval *, int);
static int debug_enabled = 0;
-static VALUE install_timer(VALUE self, VALUE seconds)
+static VALUE install_first_timer(VALUE self, VALUE seconds)
{
struct itimerval timer_interval;
+ struct itimerval *captured_timer_interval;
+ int sanitized_interval_seconds;
+
+ if (debug_enabled) {
+ log_debug("[install_first_timer] %d s\n", NUM2INT(seconds));
+ }
/*
* Block SIG_ALRM for safe processing of SIG_ALRM configuration and save mask.
*/
if (0 != sigprocmask(SIG_BLOCK, &sigalarm_mask, &original_mask)) {
- log_error("install_timer: Could not block SIG_ALRM", DISPLAY_ERRNO);
+ log_error("[install_first_timer] Could not block SIG_ALRM\n", DISPLAY_ERRNO);
return Qnil;
}
clear_pending_sigalrm_for_ruby_threads();
- log_debug("install_timer: Successfully blocked SIG_ALRM at O.S. level");
+ log_debug("[install_first_timer] Successfully blocked SIG_ALRM at O.S. level\n");
/*
* Save previous signal handler.
*/
+ log_debug("[install_first_timer] Saving original system handler\n");
original_signal_handler.sa_handler = NULL;
if (0 != sigaction(SIGALRM, NULL, &original_signal_handler)) {
- log_error("install_timer: Could not save existing handler for SIG_ALRM", DISPLAY_ERRNO);
+ log_error("[install_first_timer] Could not save existing handler for SIG_ALRM\n", DISPLAY_ERRNO);
restore_original_sigalrm_mask_when_blocked();
return Qnil;
}
- log_debug("install_timer: Successfully saved existing SIG_ALRM handler");
-
+ log_debug("[install_first_timer] Successfully saved existing SIG_ALRM handler\n");
+
/*
* Install Ruby Level SIG_ALRM handler
*/
install_ruby_sigalrm_handler(self);
/*
- * Set new real time interval timer and save the original if any.
+ * Save original real time interval timer if required.
*/
set_itimerval(&original_timer_interval, 0);
- set_itimerval(&timer_interval, NUM2INT(seconds));
- if (0 != setitimer(ITIMER_REAL, &timer_interval, &original_timer_interval)) {
- log_error("install_timer: Could not install our own timer, timeout will not work", DISPLAY_ERRNO);
+ captured_timer_interval = &original_timer_interval;
+ sanitized_interval_seconds = NUM2INT(seconds);
+ if (sanitized_interval_seconds <= 0 ) {
+ sanitized_interval_seconds = 1;
+ }
+
+ /*
+ * Set new real time interval timer.
+ */
+ set_itimerval(&timer_interval, sanitized_interval_seconds);
+ if (0 != setitimer(ITIMER_REAL, &timer_interval, captured_timer_interval)) {
+ log_error("[install_first_timer] Could not install our own timer, timeout will not work", DISPLAY_ERRNO);
restore_original_ruby_sigalrm_handler(self);
restore_original_sigalrm_mask_when_blocked();
return Qnil;
}
- log_debug("install_timer: Successfully installed timer");
+ log_debug("[install_first_timer] Successfully installed timer (%ds)\n", timer_interval.it_value.tv_sec);
/*
* Unblock SIG_ALRM
*/
if (0 != sigprocmask(SIG_UNBLOCK, &sigalarm_mask, NULL)) {
- log_error("install_timer: Could not unblock SIG_ALRM, timeout will not work", DISPLAY_ERRNO);
+ log_error("[install_first_timer] Could not unblock SIG_ALRM, timeout will not work", DISPLAY_ERRNO);
restore_original_timer_interval();
restore_original_ruby_sigalrm_handler(self);
restore_original_sigalrm_mask_when_blocked();
}
- log_debug("install_timer: Successfully unblocked SIG_ALRM.");
+ log_debug("[install_first_timer] Successfully unblocked SIG_ALRM.\n");
return Qnil;
}
-static VALUE cleanup_timer(VALUE self, VALUE seconds)
+static VALUE install_next_timer(VALUE self, VALUE seconds)
+{
+ struct itimerval timer_interval;
+ int sanitized_interval_seconds;
+
+ if (debug_enabled) {
+ log_debug("[install_next_timer] %ds\n", NUM2INT(seconds));
+ }
+
+ /*
+ * Block SIG_ALRM for safe processing of SIG_ALRM configuration and save mask.
+ */
+ if (0 != sigprocmask(SIG_BLOCK, &sigalarm_mask, &original_mask)) {
+ log_error("[install_next_timer] Could not block SIG_ALRM\n", DISPLAY_ERRNO);
+ return Qnil;
+ }
+ clear_pending_sigalrm_for_ruby_threads();
+ log_debug("[install_next_timer] Successfully blocked SIG_ALRM at O.S. level\n");
+
+ /*
+ * Save original real time interval timer if required.
+ */
+ sanitized_interval_seconds = NUM2INT(seconds);
+ if (sanitized_interval_seconds <= 0 ) {
+ sanitized_interval_seconds = 1;
+ }
+
+ /*
+ * Set new real time interval timer.
+ */
+ set_itimerval(&timer_interval, sanitized_interval_seconds);
+ if (0 != setitimer(ITIMER_REAL, &timer_interval, NULL)) {
+ log_error("[install_next_timer] Could not install our own timer, timeout will not work", DISPLAY_ERRNO);
+ restore_original_sigalrm_mask_when_blocked();
+ return Qnil;
+ }
+ log_debug("[install_next_timer] Successfully installed timer (%ds)\n", timer_interval.it_value.tv_sec);
+
+ /*
+ * Unblock SIG_ALRM
+ */
+ if (0 != sigprocmask(SIG_UNBLOCK, &sigalarm_mask, NULL)) {
+ log_error("[install_next_timer] Could not unblock SIG_ALRM, timeout will not work", DISPLAY_ERRNO);
+ restore_original_sigalrm_mask_when_blocked();
+ }
+ log_debug("[install_next_timer] Successfully unblocked SIG_ALRM.\n");
+
+ return Qnil;
+}
+
+static VALUE restore_original_configuration(VALUE self)
{
/*
* Block SIG_ALRM for safe processing of SIG_ALRM configuration.
*/
if (0 != sigprocmask(SIG_BLOCK, &sigalarm_mask, NULL)) {
- log_error("cleanup_timer: Could not block SIG_ALRM", errno);
+ log_error("restore_original_configuration: Could not block SIG_ALRM", errno);
}
clear_pending_sigalrm_for_ruby_threads();
- log_debug("cleanup_timer: Blocked SIG_ALRM");
+ log_debug("[restore_original_configuration] Blocked SIG_ALRM\n");
/*
* Install Ruby Level SIG_ALRM handler
@@ -97,15 +164,15 @@ static VALUE cleanup_timer(VALUE self, VALUE seconds)
restore_original_ruby_sigalrm_handler(self);
if (original_signal_handler.sa_handler == NULL) {
- log_error("cleanup_timer: Previous SIG_ALRM handler not initialized!", DO_NOT_DISPLAY_ERRNO);
+ log_error("[restore_original_configuration] Previous SIG_ALRM handler not initialized!", DO_NOT_DISPLAY_ERRNO);
} else if (0 == sigaction(SIGALRM, &original_signal_handler, NULL)) {
- log_debug("cleanup_timer: Successfully restored previous handler for SIG_ALRM");
+ log_debug("[restore_original_configuration] Successfully restored previous handler for SIG_ALRM\n");
} else {
- log_error("cleanup_timer: Could not restore previous handler for SIG_ALRM", DISPLAY_ERRNO);
+ log_error("[restore_original_configuration] Could not restore previous handler for SIG_ALRM", DISPLAY_ERRNO);
}
original_signal_handler.sa_handler = NULL;
- restore_original_timer_interval();
+ restore_original_timer_interval();
restore_original_sigalrm_mask_when_blocked();
}
@@ -119,18 +186,18 @@ static VALUE cleanup_timer(VALUE self, VALUE seconds)
*/
static void restore_original_timer_interval() {
if (0 != setitimer(ITIMER_REAL, &original_timer_interval, NULL)) {
- log_error("cleanup_timer: Could not restore original timer", DISPLAY_ERRNO);
+ log_error("restore_original_configuration: Could not restore original timer", DISPLAY_ERRNO);
}
- log_debug("cleanup_timer: Successfully restored timer");
+ log_debug("[restore_original_configuration] Successfully restored original timer\n");
}
static void restore_original_sigalrm_mask_when_blocked()
{
if (!sigismember(&original_mask, SIGALRM)) {
sigprocmask(SIG_UNBLOCK, &sigalarm_mask, NULL);
- log_debug("cleanup_timer: Unblocked SIG_ALRM");
+ log_debug("[restore_original_configuration] Unblocked SIG_ALRM\n");
} else {
- log_debug("cleanup_timer: No Need to unblock SIG_ALRM");
+ log_debug("[restore_original_configuration] No Need to unblock SIG_ALRM\n");
}
}
@@ -161,12 +228,16 @@ static VALUE disable_debug(VALUE self) {
return Qnil;
}
-static void log_debug(char* message)
+static void log_debug(char* message, ...)
{
- if (0 != debug_enabled) {
- printf("%s\n", message);
- }
- return;
+ va_list argp;
+
+ if (0 != debug_enabled) {
+ va_start(argp, message);
+ vfprintf(stdout, message, argp);
+ va_end(argp);
+ }
+ return;
}
static void log_error(char* message, int display_errno)
@@ -186,7 +257,7 @@ static void log_error(char* message, int display_errno)
static void clear_pending_sigalrm_for_ruby_threads()
{
CHECK_INTS;
- log_debug("Successfully triggered all pending signals at Green Thread level");
+ log_debug("[native] Successfully triggered all pending signals at Green Thread level\n");
}
static void init_sigalarm_mask()
@@ -208,8 +279,9 @@ void Init_system_timer_native()
{
init_sigalarm_mask();
rb_cSystemTimer = rb_define_module("SystemTimer");
- rb_define_singleton_method(rb_cSystemTimer, "install_timer", install_timer, 1);
- rb_define_singleton_method(rb_cSystemTimer, "cleanup_timer", cleanup_timer, 0);
+ rb_define_singleton_method(rb_cSystemTimer, "install_first_timer", install_first_timer, 1);
+ rb_define_singleton_method(rb_cSystemTimer, "install_next_timer", install_next_timer, 1);
+ rb_define_singleton_method(rb_cSystemTimer, "restore_original_configuration", restore_original_configuration, 0);
rb_define_singleton_method(rb_cSystemTimer, "debug_enabled?", debug_enabled_p, 0);
rb_define_singleton_method(rb_cSystemTimer, "enable_debug", enable_debug, 0);
rb_define_singleton_method(rb_cSystemTimer, "disable_debug", disable_debug, 0);
View
63 lib/system_timer.rb
@@ -1,5 +1,8 @@
require 'rubygems'
require 'timeout'
+require 'forwardable'
+require File.dirname(__FILE__) + '/system_timer/thread_timer'
+require File.dirname(__FILE__) + '/system_timer/concurrent_timer_pool'
# Timer based on underlying SIGALRM system timers, is a
# solution to Ruby processes which hang beyond the time limit when accessing
@@ -17,17 +20,45 @@
#
# end
#
-module SystemTimer
+module SystemTimer
+
+ # TODO - Race condition for these 2 class instance variables
+ @timer_pool = ConcurrentTimerPool.new
+ @mutex = Mutex.new
+
class << self
+ attr_reader :timer_pool
# Executes the method's block. If the block execution terminates before
# +seconds+ seconds has passed, it returns true. If not, it terminates
# the execution and raises a +Timeout::Error+.
def timeout_after(seconds)
- install_timer(seconds)
+ new_timer = nil # just for scope
+ @mutex.synchronize do
+ save_original_configuration = timer_pool.empty?
+ new_timer = timer_pool.add_timer seconds
+ next_interval = timer_pool.next_trigger_interval
+ debug "==== Install Timer ==== at #{Time.now.to_i}, next interval: #{next_interval}"
+ if save_original_configuration
+ install_first_timer next_interval
+ else
+ install_next_timer next_interval
+ end
+ end
return yield
ensure
- cleanup_timer
+ @mutex.synchronize do
+ debug "==== Cleanup Timer ==== at #{Time.now.to_i}, #{new_timer} "
+ timer_pool.cancel(new_timer)
+ timer_pool.log_registered_timers if debug_enabled?
+ next_interval = timer_pool.next_trigger_interval
+ debug "Cleanup Timer : next interval #{next_interval.inspect} "
+ if next_interval
+ install_next_timer next_interval
+ else
+ restore_original_configuration
+ end
+ end
end
# Backward compatibility with timeout.rb
@@ -35,36 +66,32 @@ def timeout_after(seconds)
protected
- def install_ruby_sigalrm_handler #:nodoc:
- timed_thread = Thread.current # Ruby signals are always delivered to main thread by default.
+ def install_ruby_sigalrm_handler #:nodoc:
@original_ruby_sigalrm_handler = trap('SIGALRM') do
- log_timeout_received(timed_thread) if SystemTimer.debug_enabled?
- timed_thread.raise Timeout::Error.new("time's up!")
- end
+ @mutex.synchronize do
+ timer_pool.trigger_next_expired_timer(Time.now.to_i)
+ end
+ end
end
- def restore_original_ruby_sigalrm_handler #:nodoc:
+ def restore_original_ruby_sigalrm_handler #:nodoc:
trap('SIGALRM', original_ruby_sigalrm_handler || 'DEFAULT')
ensure
reset_original_ruby_sigalrm_handler
end
- def original_ruby_sigalrm_handler #:nodoc:
+ def original_ruby_sigalrm_handler #:nodoc:
@original_ruby_sigalrm_handler
end
- def reset_original_ruby_sigalrm_handler #:nodoc:
+ def reset_original_ruby_sigalrm_handler #:nodoc:
@original_ruby_sigalrm_handler = nil
end
- def log_timeout_received(timed_thread) #:nodoc:
- puts <<-EOS
- install_ruby_sigalrm_handler: Got Timeout in #{Thread.current}
- Main thread : #{Thread.main}
- Timed_thread : #{timed_thread}
- All Threads : #{Thread.list.inspect}
- EOS
+ def debug(message) #:nodoc
+ puts message if debug_enabled?
end
+
end
end
View
79 lib/system_timer/concurrent_timer_pool.rb
@@ -0,0 +1,79 @@
+module SystemTimer
+
+ class ConcurrentTimerPool
+ extend Forwardable
+
+ def_delegators :registered_timers, :empty?
+
+ def registered_timers
+ @timers ||= []
+ end
+
+ def add_timer(interval_in_seconds)
+ new_timer = register_timer(Time.now.to_i + interval_in_seconds, Thread.current)
+ log_registered_timers if SystemTimer.debug_enabled?
+ new_timer
+ end
+
+ def cancel(registered_timer)
+ registered_timers.delete registered_timer
+ end
+
+ def next_timer
+ registered_timers.sort {|x,y| x.trigger_time <=> y.trigger_time}.first
+ end
+
+ def next_trigger_time
+ timer = next_timer
+ timer.trigger_time unless timer.nil?
+ end
+
+ def next_trigger_interval
+ timer = next_timer
+ [0, (timer.trigger_time - Time.now.to_i)].max unless timer.nil?
+ end
+
+ def next_timer_interval_in_seconds
+ end
+
+ def next_expired_timer(now_in_seconds_since_epoch)
+ candidate_timer = next_timer
+ return nil if candidate_timer.nil? ||
+ candidate_timer.trigger_time > now_in_seconds_since_epoch
+ candidate_timer
+ end
+
+ def trigger_next_expired_timer(now_in_seconds_since_epoch)
+ timer = next_expired_timer(now_in_seconds_since_epoch)
+ return if timer.nil?
+
+ cancel timer
+ log_timeout_received(timer) if SystemTimer.debug_enabled?
+ timer.thread.raise Timeout::Error.new("time's up!")
+ end
+
+ def register_timer(trigger_time, thread)
+ new_timer = ThreadTimer.new(trigger_time, thread)
+ registered_timers << new_timer
+ new_timer
+ end
+
+ def log_timeout_received(thread_timer) #:nodoc:
+ puts <<-EOS
+ ==== Triger Timer ==== #{thread_timer}
+ Main thread : #{Thread.main}
+ Timed_thread : #{thread_timer.thread}
+ All Threads : #{Thread.list.inspect}
+ EOS
+ log_registered_timers
+ end
+
+ def log_registered_timers #:nodoc:
+ puts <<-EOS
+ Registered Timers: #{registered_timers.collect do |t| t.to_s end}
+ EOS
+ end
+
+ end
+
+end
View
16 lib/system_timer/thread_timer.rb
@@ -0,0 +1,16 @@
+module SystemTimer
+ class ThreadTimer
+ attr_reader :trigger_time, :thread
+
+ # Save current thread as Ruby signals are always delivered to main thread.
+ def initialize(trigger_time, thread)
+ @trigger_time = trigger_time
+ @thread = thread
+ end
+
+ def to_s
+ "<ThreadTimer :time => #{trigger_time}, :thread => #{thread}>"
+ end
+
+ end
+end
View
4 test/all_tests.rb
@@ -1 +1,3 @@
-Dir["#{File.dirname __FILE__}/*_test.rb"].each { |test_case| require test_case }
+Dir["#{File.dirname __FILE__}/**/*_test.rb"].each do |test_case|
+ require test_case
+end
View
255 test/system_timer/concurrent_timer_pool_unit_test.rb
@@ -0,0 +1,255 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+unit_tests do
+
+ test "registered_timers is empty when there is no registered timers" do
+ assert_equal [], SystemTimer::ConcurrentTimerPool.new.registered_timers
+ end
+
+ test "a new timer is added to the registered timer list when you register a timer" do
+ pool = SystemTimer::ConcurrentTimerPool.new
+ pool.register_timer :a_trigger_time, :a_thread
+ assert_equal [[:a_trigger_time, :a_thread]],
+ pool.registered_timers.collect {|t| [t.trigger_time, t.thread] }
+ end
+
+ test "register_timer returns the timer that was just added to the pool" do
+ pool = SystemTimer::ConcurrentTimerPool.new
+ timer = pool.register_timer :a_trigger_time, :a_thread
+ assert_equal [:a_trigger_time, :a_thread], [timer.trigger_time, timer.thread]
+ end
+
+ test "add_timer is a shortcut method to register a timer given its interval" do
+ pool = SystemTimer::ConcurrentTimerPool.new
+ Thread.stubs(:current).returns(:the_current_thread)
+ now = Time.now
+ Time.stubs(:now).returns(now)
+
+ pool.expects(:register_timer).with(now.to_i + 15, :the_current_thread)
+ pool.add_timer 15
+ end
+
+ test "cancel removes a timer from the registered timer list" do
+ pool = SystemTimer::ConcurrentTimerPool.new
+ registered_timer = pool.register_timer :a_trigger_time, :a_thread
+ pool.cancel registered_timer
+ assert_equal [], pool.registered_timers
+ end
+
+ test "cancel does not complain when timer is cancelled " +
+ "(useful for ensure blocks)" do
+
+ pool = SystemTimer::ConcurrentTimerPool.new
+ a_timer = pool.add_timer 123
+ another_timer = pool.add_timer 456
+ pool.cancel(another_timer)
+ pool.cancel(another_timer)
+ assert_equal [a_timer], pool.registered_timers
+ end
+
+ test "empty? returns false when there is no registered timers" do
+ assert_equal true, SystemTimer::ConcurrentTimerPool.new.empty?
+ end
+
+ test "empty? returns false when there is a registered timers" do
+ pool = SystemTimer::ConcurrentTimerPool.new
+ pool.register_timer :a_trigger_time, :a_thread
+ assert_equal false, pool.empty?
+ end
+
+ test "empty? returns false once all registered timers are cancelled" do
+ pool = SystemTimer::ConcurrentTimerPool.new
+ pool.cancel pool.add_timer(123)
+ assert_equal true, pool.empty?
+ end
+
+ test "next expired timer return nil when there is no registered timer" do
+ assert_nil SystemTimer::ConcurrentTimerPool.new.next_expired_timer(24)
+ end
+
+ test "next_timer returns nil when there is no registered timer" do
+ assert_nil SystemTimer::ConcurrentTimerPool.new.next_timer
+ end
+
+ test "next_timer returns the registered timer when " +
+ "there is only one registered timer" do
+
+ pool = SystemTimer::ConcurrentTimerPool.new
+ the_timer = pool.register_timer 24, stub_everything
+ assert_equal the_timer, pool.next_timer
+ end
+
+ test "next_timer returns the trigger time of the first timer to" +
+ "expire when there is more than one registered timer" do
+
+ pool = SystemTimer::ConcurrentTimerPool.new
+ late_timer = pool.register_timer 64, stub_everything
+ early_timer = pool.register_timer 24, stub_everything
+ assert_equal early_timer, pool.next_timer
+ end
+
+ test "next_trigger_time returns nil when next_timer is nil" do
+ pool = SystemTimer::ConcurrentTimerPool.new
+ pool.expects(:next_timer).returns(nil)
+ assert_nil pool.next_trigger_time
+ end
+
+ test "next_trigger_time returns trigger time of next timer when " +
+ "next timer is not nil" do
+
+ pool = SystemTimer::ConcurrentTimerPool.new
+ the_timer = SystemTimer::ThreadTimer.new 24, stub_everything
+ pool.expects(:next_timer).returns(the_timer)
+ assert_equal 24, pool.next_trigger_time
+ end
+
+ test "next_trigger_interval returns nil when next_timer is nil" do
+ pool = SystemTimer::ConcurrentTimerPool.new
+ pool.expects(:next_timer).returns(nil)
+ assert_nil pool.next_trigger_interval
+ end
+
+ test "next_trigger_interval returns the interval between now and " +
+ "next_timer timer time when next timer is in the future" do
+ pool = SystemTimer::ConcurrentTimerPool.new
+ now = Time.now
+ Time.stubs(:now).returns(now)
+ next_timer = SystemTimer::ThreadTimer.new((now.to_i + 7), stub_everything)
+ pool.expects(:next_timer).returns(next_timer)
+ assert_equal 7, pool.next_trigger_interval
+ end
+
+ test "next_trigger_interval returns 0 when next timer is now" do
+ pool = SystemTimer::ConcurrentTimerPool.new
+ now = Time.now
+ Time.stubs(:now).returns(now)
+ next_timer = SystemTimer::ThreadTimer.new now.to_i, stub_everything
+ pool.expects(:next_timer).returns(next_timer)
+ assert_equal 0, pool.next_trigger_interval
+ end
+
+ test "next_trigger_interval returns 0 when next timer is in the past" do
+ pool = SystemTimer::ConcurrentTimerPool.new
+ now = Time.now
+ Time.stubs(:now).returns(now)
+ next_timer = SystemTimer::ThreadTimer.new((now.to_i - 3), stub_everything)
+ pool.expects(:next_timer).returns(next_timer)
+ assert_equal 0, pool.next_trigger_interval
+ end
+
+ test "next_expired_timer returns the timer that was trigerred" +
+ "when a timer has expired" do
+
+ pool = SystemTimer::ConcurrentTimerPool.new
+ the_timer = pool.register_timer 24, :a_thread
+ assert_equal the_timer, pool.next_expired_timer(24)
+ end
+
+ test "next_expired_timer returns nil when no timer has expired yet" do
+ pool = SystemTimer::ConcurrentTimerPool.new
+ pool.register_timer 24, :a_thread
+ assert_nil pool.next_expired_timer(23)
+ end
+
+ test "next_expired_timer returns the timer that first expired " +
+ "when there is more than one expired timer" do
+
+ pool = SystemTimer::ConcurrentTimerPool.new
+ last_to_expire = pool.register_timer 64, :a_thread
+ first_to_expire = pool.register_timer 24, :a_thread
+ assert_equal first_to_expire, pool.next_expired_timer(100)
+ end
+
+ test "trigger_next_expired_timer does not raise when there is no registered timer" do
+ SystemTimer::ConcurrentTimerPool.new.trigger_next_expired_timer 1234
+ end
+
+ test "trigger_next_expired_timer raises a TimeoutError in the context of " +
+ "its thread when there is a registered timer that has expired" do
+
+ pool = SystemTimer::ConcurrentTimerPool.new
+ the_thread = mock('thread')
+
+ Timeout::Error.expects(:new).with("time's up!").returns(:the_exception)
+ the_thread.expects(:raise).with(:the_exception)
+ pool.register_timer 24, the_thread
+ pool.trigger_next_expired_timer 24
+ end
+
+ test "trigger_next_expired_timer does not raise when registered timer has not expired" do
+ pool = SystemTimer::ConcurrentTimerPool.new
+ pool.register_timer 24, stub_everything
+ pool.trigger_next_expired_timer(10)
+ end
+
+ test "trigger_next_expired_timer triggers the first registered timer that expired" do
+ pool = SystemTimer::ConcurrentTimerPool.new
+ first_to_expire = pool.register_timer 24, stub_everything
+ second_to_expire = pool.register_timer 64, stub_everything
+ pool.trigger_next_expired_timer(100)
+ assert_equal [second_to_expire], pool.registered_timers
+ end
+
+ test "trigger_next_expired_timer triggers the first registered timer that " +
+ "expired whatever the timer insertion order is" do
+
+ pool = SystemTimer::ConcurrentTimerPool.new
+ second_to_expire = pool.register_timer 64, stub_everything
+ first_to_expire = pool.register_timer 24, stub_everything
+ pool.trigger_next_expired_timer(100)
+ assert_equal [second_to_expire], pool.registered_timers
+ end
+
+ test "trigger_next_expired_timer remove the expired timer from the pool" do
+ pool = SystemTimer::ConcurrentTimerPool.new
+ pool.register_timer 24, stub_everything
+ pool.trigger_next_expired_timer 24
+ end
+
+ test "trigger_next_expired_timer logs timeout a registered timer has expired" +
+ "and SystemTimer debug mode is enabled " do
+ pool = SystemTimer::ConcurrentTimerPool.new
+ the_timer = pool.register_timer 24, stub_everything
+ SystemTimer.stubs(:debug_enabled?).returns(true)
+
+ pool.expects(:log_timeout_received).with(the_timer)
+ pool.trigger_next_expired_timer 24
+ end
+
+ test "trigger_next_expired_timer does not logs timeoout when SystemTimer " +
+ "debug mode is disabled " do
+
+ pool = SystemTimer::ConcurrentTimerPool.new
+ the_timer = pool.register_timer 24, stub_everything
+ SystemTimer.stubs(:debug_enabled?).returns(false)
+
+ pool.expects(:log_timeout_received).never
+ pool.trigger_next_expired_timer 24
+ end
+
+ test "trigger_next_expired_timer does not logs timeout no registered timer " +
+ "has expired and SystemTimer debug mode is enabled " do
+
+ pool = SystemTimer::ConcurrentTimerPool.new
+ the_timer = pool.register_timer 24, stub_everything
+ SystemTimer.stubs(:debug_enabled?).returns(true)
+
+ pool.expects(:log_timeout_received).never
+ pool.trigger_next_expired_timer 23
+ end
+
+
+ test "log_timeout_received does not raise" do
+ original_stdout = $stdout
+ begin
+ stdout = StringIO.new
+ $stdout = stdout
+
+ SystemTimer::ConcurrentTimerPool.new.log_timeout_received(SystemTimer::ThreadTimer.new(:a_time, :a_thread))
+ assert_match %r{==== Triger Timer ====}, stdout.string
+ ensure
+ $stdout = original_stdout
+ end
+ end
+
+end
View
20 test/system_timer/thread_timer_test.rb
@@ -0,0 +1,20 @@
+require File.dirname(__FILE__) + '/../test_helper'
+
+unit_tests do
+
+ test "trigger_time returns the time given in the constructor" do
+ timer = SystemTimer::ThreadTimer.new(:a_tigger_time, :a_thread)
+ assert_equal :a_tigger_time, timer.trigger_time
+ end
+
+ test "thread returns the thread given in the constructor" do
+ timer = SystemTimer::ThreadTimer.new(:a_tigger_time, :a_thread)
+ assert_equal :a_thread, timer.thread
+ end
+
+ test "to_s give a human friendly description" do
+ assert_match /<ThreadTimer :time => 24, :thread => #<Thread(.*)>>/,
+ SystemTimer::ThreadTimer.new(24, Thread.current).to_s
+ end
+
+end
View
85 test/system_timer_test.rb → test/system_timer_functional_test.rb
@@ -1,27 +1,17 @@
- $: << File.dirname(__FILE__) + '/../lib'
- $: << File.dirname(__FILE__) + '/../ext/system_timer'
- $: << File.dirname(__FILE__) + "/../../../vendor/gems/dust-0.1.4/lib"
- $: << File.dirname(__FILE__) + "/../../../vendor/gems/mocha-0.5.3/lib"
-require 'test/unit'
-require 'system_timer'
-require 'dust'
-require 'mocha'
-require 'stringio'
-require "open-uri"
+require File.dirname(__FILE__) + '/test_helper'
functional_tests do
ERROR_MARGIN = 2
- # SystemTimer.enable_debug
-
test "original_ruby_sigalrm_handler is nil after reset" do
SystemTimer.send(:install_ruby_sigalrm_handler)
SystemTimer.send(:reset_original_ruby_sigalrm_handler)
assert_nil SystemTimer.send(:original_ruby_sigalrm_handler)
end
- test "original_ruby_sigalrm_handler is set to existing handler after install_ruby_sigalrm_handler" do
+ test "original_ruby_sigalrm_handler is set to existing handler after " +
+ "install_ruby_sigalrm_handler when save_previous_handler is true" do
SystemTimer.expects(:trap).with('SIGALRM').returns(:an_existing_handler)
SystemTimer.send(:install_ruby_sigalrm_handler)
assert_equal :an_existing_handler, SystemTimer.send(:original_ruby_sigalrm_handler)
@@ -88,8 +78,8 @@
begin
fake_original_ruby_handler = proc {}
initial_ruby_handler = trap "SIGALRM", fake_original_ruby_handler
- SystemTimer.install_timer(3)
- SystemTimer.cleanup_timer
+ SystemTimer.install_first_timer 3
+ SystemTimer.restore_original_configuration
assert_equal fake_original_ruby_handler, trap("SIGALRM", "IGNORE")
ensure # avoid interfering with test infrastructure
trap("SIGALRM", initial_ruby_handler) if initial_ruby_handler
@@ -122,10 +112,10 @@
end
end
- test "while exact timeouts cannot be guaranted the timeout should not exceed twiced the provided timeout" do
+ test "while exact timeouts cannot be guaranted the timeout should not exceed the provided timeout by 2 seconds" do
start = Time.now
begin
- SystemTimer.timeout_after(1) do
+ SystemTimer.timeout_after(2) do
open "http://www.invalid.domain.comz"
end
raise "should never get there"
@@ -133,17 +123,18 @@
rescue Timeout::Error => e
end
elapsed = Time.now - start
- assert elapsed < 2
+ assert elapsed < 4, "Got #{elapsed} s, expected 2, at most 4"
end
+
test "timeout are enforced on system calls" do
assert_timeout_within(3) do
SystemTimer.timeout(3) do
- sleep 60
+ sleep 30
end
end
end
-
+
test "timeout work when spawning a different thread" do
assert_timeout_within(3) do
thread = Thread.new do
@@ -155,12 +146,66 @@
end
end
+ test "can set multiple serial timers" do
+ 10.times do
+ assert_timeout_within(3) do
+ SystemTimer.timeout(3) do
+ sleep 60
+ end
+ end
+ end
+ end
+
+ test "timeout work when setting concurrent timers, the first one " +
+ "expiring before the second one" do
+
+ first_thread = Thread.new do
+ assert_timeout_within(3) do
+ SystemTimer.timeout(3) do
+ sleep 60
+ end
+ end
+ end
+ second_thread = Thread.new do
+ assert_timeout_within(5) do
+ SystemTimer.timeout(5) do
+ sleep 60
+ end
+ end
+ end
+ first_thread.join
+ second_thread.join
+ end
+
+ test "timeout work when setting concurrent timers, the second one " +
+ "expiring before the first one" do
+
+ first_thread = Thread.new do
+ assert_timeout_within(20) do
+ SystemTimer.timeout(20) do
+ sleep 60
+ end
+ end
+ end
+ second_thread = Thread.new do
+ assert_timeout_within(3) do
+ SystemTimer.timeout(3) do
+ sleep 60
+ end
+ end
+ end
+ first_thread.join
+ second_thread.join
+ end
+
def assert_timeout_within(expected_timeout_in_seconds, &block)
start = Time.now
yield
flunk "Did not timeout as expected!"
rescue Timeout::Error
elapsed = Time.now - start
+ assert elapsed >= expected_timeout_in_seconds,
+ "Timed out too early, expected #{expected_timeout_in_seconds}, got #{elapsed} s"
assert elapsed < ERROR_MARGIN * expected_timeout_in_seconds,
"Timed out after #{elapsed} seconds, expected #{expected_timeout_in_seconds}"
end
View
95 test/system_timer_unit_test.rb
@@ -0,0 +1,95 @@
+require File.dirname(__FILE__) + '/test_helper'
+
+unit_tests do
+
+ test "timeout_after registers a new timer in the timer pool" do
+ pool = stub_everything
+ Thread.stubs(:current).returns(:the_current_thread)
+ SystemTimer.stubs(:timer_pool).returns(pool)
+ SystemTimer.stubs(:install_next_timer)
+ SystemTimer.stubs(:restore_original_configuration)
+
+ pool.expects(:add_timer).with(5).returns(stub_everything)
+ SystemTimer.timeout_after(5) {}
+ end
+
+ test "timeout_after installs a system timer saving the previous " +
+ "configuration when there is only one timer" do
+
+ now = Time.now
+ Time.stubs(:now).returns(now)
+ SystemTimer.stubs(:restore_original_configuration)
+
+ SystemTimer.expects(:install_first_timer).with(24)
+ SystemTimer.timeout_after(24) {}
+ end
+
+ test "timeout_after installs a system timer without saving the previous " +
+ "configuration when there is more than one timer" do
+
+ now = Time.now
+ Time.stubs(:now).returns(now)
+ SystemTimer.timer_pool.register_timer now.to_i + 100, :a_thread
+ SystemTimer.stubs(:restore_original_configuration)
+ SystemTimer.stubs(:install_next_timer)
+
+ SystemTimer.expects(:install_next_timer).with(24)
+ SystemTimer.timeout_after(24) {}
+ end
+
+ test "timeout_after installs a system timer with the interval before " +
+ "the next timer to expire" do
+
+ now = Time.now
+ Time.stubs(:now).returns(now)
+ SystemTimer.timer_pool.register_timer now.to_i + 24, :a_thread
+ SystemTimer.stubs(:restore_original_configuration)
+ SystemTimer.stubs(:install_next_timer)
+
+ SystemTimer.expects(:install_next_timer).with(24)
+ SystemTimer.timeout_after(100) {}
+ end
+
+ test "timeout_after cancels the timer when the block completes without " +
+ "timeout" do
+
+ now = Time.now
+ the_timer = stub_everything
+ Time.stubs(:now).returns(now)
+ SystemTimer.stubs(:restore_original_configuration)
+ SystemTimer.stubs(:install_first_timer)
+ SystemTimer.timer_pool.stubs(:add_timer).returns(the_timer)
+
+ SystemTimer.timer_pool.expects(:cancel).with(the_timer)
+ SystemTimer.timeout_after(24) {}
+ end
+
+ test "debug does not output to stdout when debug is disabled" do
+ SystemTimer.stubs(:debug_enabled?).returns(false)
+ original_stdout = $stdout
+ begin
+ stdout = StringIO.new
+ $stdout = stdout
+
+ SystemTimer.send :debug, "a log message"
+ assert stdout.string.empty?
+ ensure
+ $stdout = original_stdout
+ end
+ end
+
+ test "debug logs messaget to stdout when debug is enabled" do
+ SystemTimer.stubs(:debug_enabled?).returns(true)
+ original_stdout = $stdout
+ begin
+ stdout = StringIO.new
+ $stdout = stdout
+
+ SystemTimer.send :debug, "a log message"
+ assert_match /a log message/, stdout.string
+ ensure
+ $stdout = original_stdout
+ end
+ end
+
+end
View
10 test/test_helper.rb
@@ -0,0 +1,10 @@
+ $: << File.dirname(__FILE__) + '/../lib'
+ $: << File.dirname(__FILE__) + '/../ext/system_timer'
+ $: << File.dirname(__FILE__) + "/../../../vendor/gems/dust-0.1.4/lib"
+ $: << File.dirname(__FILE__) + "/../../../vendor/gems/mocha-0.5.3/lib"
+require 'test/unit'
+require 'system_timer'
+require 'dust'
+require 'mocha'
+require 'stringio'
+require "open-uri"
Please sign in to comment.
Something went wrong with that request. Please try again.