Feature #14859 ยป 0001-implement-Timeout-in-VM.patch
| benchmark/bm_timeout_mt_nested.rb | ||
|---|---|---|
| require 'timeout' | ||
| total = 10000 | ||
| nthr = 8 | ||
| nr = total / nthr | ||
| def nest_timeout(n, nthr) | ||
|   n -= 1 | ||
|   if n > 0 | ||
|     Timeout.timeout(n) { nest_timeout(n, nthr) } | ||
|   else | ||
|     nthr.times { Thread.pass } | ||
|   end | ||
| end | ||
| nthr.times.map do | ||
|   Thread.new do | ||
|     nr.times { nest_timeout(10, nthr) } | ||
|   end | ||
| end.map(&:join) | ||
| benchmark/bm_timeout_mt_same.rb | ||
|---|---|---|
| require 'timeout' | ||
| total = 100000 | ||
| nthr = 8 | ||
| nr = total / nthr | ||
| nthr.times.map do | ||
|   Thread.new do | ||
|     nr.times { Timeout.timeout(5) { Thread.pass } } | ||
|   end | ||
| end.map(&:join) | ||
| benchmark/bm_timeout_mt_ugly.rb | ||
|---|---|---|
| # unrealistic: this is the worst-case of insertion-sort-based timeout | ||
| require 'timeout' | ||
| total = 100000 | ||
| nthr = 8 | ||
| nr = total / nthr | ||
| nthr.times.map do | ||
|   Thread.new do | ||
|     i = nr | ||
|     while (i -= 1) >= 0 | ||
|       Timeout.timeout(i + 1) { nthr.times { Thread.pass } } | ||
|     end | ||
|   end | ||
| end.map(&:join) | ||
| benchmark/bm_timeout_nested.rb | ||
|---|---|---|
| require 'timeout' | ||
| def nest_timeout(n) | ||
|   n -= 1 | ||
|   if n > 0 | ||
|     Timeout.timeout(n) { nest_timeout(n) } | ||
|   end | ||
| end | ||
| 100000.times do | ||
|   nest_timeout(10) | ||
| end | ||
| benchmark/bm_timeout_same.rb | ||
|---|---|---|
| require 'timeout' | ||
| 100000.times { Timeout.timeout(5) {} } | ||
| benchmark/bm_timeout_zero.rb | ||
|---|---|---|
| require 'timeout' | ||
| 100000.times { Timeout.timeout(0) {} } | ||
| common.mk | ||
|---|---|---|
| 		symbol.$(OBJEXT) \ | ||
| 		thread.$(OBJEXT) \ | ||
| 		time.$(OBJEXT) \ | ||
| 		timeout.$(OBJEXT) \ | ||
| 		transcode.$(OBJEXT) \ | ||
| 		util.$(OBJEXT) \ | ||
| 		variable.$(OBJEXT) \ | ||
| ... | ... | |
| time.$(OBJEXT): {$(VPATH)}subst.h | ||
| time.$(OBJEXT): {$(VPATH)}time.c | ||
| time.$(OBJEXT): {$(VPATH)}timev.h | ||
| timeout.$(OBJEXT): $(CCAN_DIR)/check_type/check_type.h | ||
| timeout.$(OBJEXT): $(CCAN_DIR)/container_of/container_of.h | ||
| timeout.$(OBJEXT): $(CCAN_DIR)/list/list.h | ||
| timeout.$(OBJEXT): $(CCAN_DIR)/str/str.h | ||
| timeout.$(OBJEXT): $(hdrdir)/ruby/ruby.h | ||
| timeout.$(OBJEXT): $(top_srcdir)/include/ruby.h | ||
| timeout.$(OBJEXT): {$(VPATH)}config.h | ||
| timeout.$(OBJEXT): {$(VPATH)}defines.h | ||
| timeout.$(OBJEXT): {$(VPATH)}id.h | ||
| timeout.$(OBJEXT): {$(VPATH)}intern.h | ||
| timeout.$(OBJEXT): {$(VPATH)}internal.h | ||
| timeout.$(OBJEXT): {$(VPATH)}method.h | ||
| timeout.$(OBJEXT): {$(VPATH)}missing.h | ||
| timeout.$(OBJEXT): {$(VPATH)}node.h | ||
| timeout.$(OBJEXT): {$(VPATH)}ruby_assert.h | ||
| timeout.$(OBJEXT): {$(VPATH)}ruby_atomic.h | ||
| timeout.$(OBJEXT): {$(VPATH)}st.h | ||
| timeout.$(OBJEXT): {$(VPATH)}subst.h | ||
| timeout.$(OBJEXT): {$(VPATH)}thread_$(THREAD_MODEL).h | ||
| timeout.$(OBJEXT): {$(VPATH)}thread_native.h | ||
| timeout.$(OBJEXT): {$(VPATH)}timeout.c | ||
| timeout.$(OBJEXT): {$(VPATH)}vm_core.h | ||
| timeout.$(OBJEXT): {$(VPATH)}vm_opts.h | ||
| transcode.$(OBJEXT): $(hdrdir)/ruby/ruby.h | ||
| transcode.$(OBJEXT): $(top_srcdir)/include/ruby.h | ||
| transcode.$(OBJEXT): {$(VPATH)}config.h | ||
| inits.c | ||
|---|---|---|
|     CALL(version); | ||
|     CALL(vm_trace); | ||
|     CALL(ast); | ||
|     CALL(timeout); | ||
| } | ||
| #undef CALL | ||
| internal.h | ||
|---|---|---|
| /* time.c */ | ||
| struct timeval rb_time_timeval(VALUE); | ||
| /* timeout.c */ | ||
| typedef struct rb_vm_struct rb_vm_t; | ||
| typedef struct rb_execution_context_struct rb_execution_context_t; | ||
| struct timespec *rb_timeout_sleep_interval(rb_vm_t *, struct timespec *); | ||
| void rb_timeout_expire(const rb_execution_context_t *); | ||
| /* thread.c */ | ||
| #define COVERAGE_INDEX_LINES    0 | ||
| #define COVERAGE_INDEX_BRANCHES 1 | ||
| ... | ... | |
| void rb_mutex_allow_trap(VALUE self, int val); | ||
| VALUE rb_uninterruptible(VALUE (*b_proc)(ANYARGS), VALUE data); | ||
| VALUE rb_mutex_owned_p(VALUE self); | ||
| void rb_getclockofday(struct timespec *); | ||
| /* thread_pthread.c, thread_win32.c */ | ||
| int rb_divert_reserved_fd(int fd); | ||
| test/test_timeout.rb | ||
|---|---|---|
| # frozen_string_literal: false | ||
| require 'test/unit' | ||
| require 'timeout' | ||
| begin | ||
|   require 'io/wait' | ||
| rescue LoadError | ||
| end | ||
| class TestTimeout < Test::Unit::TestCase | ||
|   def test_queue | ||
| ... | ... | |
|     } | ||
|     assert(ok, bug11344) | ||
|   end | ||
|   def test_io | ||
|     t = 0.001 | ||
|     IO.pipe do |r, w| | ||
|       assert_raise(Timeout::Error) { Timeout.timeout(t) { r.read } } | ||
|       if r.respond_to?(:wait) | ||
|         assert_raise(Timeout::Error) { Timeout.timeout(t) { r.wait } } | ||
|         assert_raise(Timeout::Error) { Timeout.timeout(t) { r.wait(9) } } | ||
|       end | ||
|       rset = [r, r.dup] | ||
|       assert_raise(Timeout::Error) do | ||
|         Timeout.timeout(t) { IO.select(rset, nil, nil, 9) } | ||
|       end | ||
|       assert_raise(Timeout::Error) { Timeout.timeout(t) { IO.select(rset) } } | ||
|       rset.each(&:close) | ||
|     end | ||
|   end | ||
|   def test_thread_join | ||
|     th = Thread.new { sleep } | ||
|     assert_raise(Timeout::Error) { Timeout.timeout(0.001) { th.join } } | ||
|   ensure | ||
|     th.kill | ||
|     th.join | ||
|   end | ||
|   def test_mutex_lock | ||
|     m = Mutex.new | ||
|     m.lock | ||
|     th = Thread.new { m.synchronize { :ok} } | ||
|     assert_raise(Timeout::Error) { Timeout.timeout(0.001) { th.join } } | ||
|     m.unlock | ||
|     assert_equal :ok, th.value | ||
|   end | ||
|   def test_yield_and_return_value | ||
|     r = Timeout.timeout(0) do |sec| | ||
|       assert_equal 0, sec | ||
|       sec | ||
|     end | ||
|     assert_equal 0, r | ||
|     t = 123 | ||
|     r = Timeout.timeout(t) do |sec| | ||
|       assert_same t, sec | ||
|       sec | ||
|     end | ||
|     assert_same r, t | ||
|     r = Timeout.timeout(t, RuntimeError) do |sec| | ||
|       assert_same t, sec | ||
|       sec | ||
|     end | ||
|     assert_same r, t | ||
|   end | ||
|   def test_timeout_thread | ||
|     in_thread { sleep } | ||
|   end | ||
|   def test_timeout_loop | ||
|     in_thread { loop {} } | ||
|   end | ||
|   def test_timeout_io_read | ||
|     IO.pipe { |r, w| in_thread { r.read } } | ||
|   end | ||
|   def test_timeout_mutex | ||
|     m = Mutex.new | ||
|     m.synchronize { in_thread { m.synchronize {} } } | ||
|     in_thread { m.synchronize { m.sleep } } | ||
|   end | ||
|   def in_thread(&blk) | ||
|     th = Thread.new do | ||
|       begin | ||
|         Timeout.timeout(0.001) { blk.call } | ||
|       rescue => e | ||
|         e | ||
|       end | ||
|     end | ||
|     assert_same th, th.join(0.3) | ||
|     assert_kind_of Timeout::Error, th.value | ||
|   end | ||
| end | ||
| thread.c | ||
|---|---|---|
| } | ||
| static void | ||
| rb_threadptr_interrupt_common(rb_thread_t *th, int trap) | ||
| rb_threadptr_interrupt_set(rb_thread_t *th, rb_atomic_t flag) | ||
| { | ||
|     rb_native_mutex_lock(&th->interrupt_lock); | ||
|     if (trap) { | ||
| 	RUBY_VM_SET_TRAP_INTERRUPT(th->ec); | ||
|     } | ||
|     else { | ||
| 	RUBY_VM_SET_INTERRUPT(th->ec); | ||
|     } | ||
|     ATOMIC_OR(th->ec->interrupt_flag, flag); | ||
|     if (th->unblock.func != NULL) { | ||
| 	(th->unblock.func)(th->unblock.arg); | ||
|     } | ||
|     else { | ||
| 	/* none */ | ||
|         (th->unblock.func)(th->unblock.arg); | ||
|     } | ||
|     rb_native_mutex_unlock(&th->interrupt_lock); | ||
| } | ||
| ... | ... | |
| void | ||
| rb_threadptr_interrupt(rb_thread_t *th) | ||
| { | ||
|     rb_threadptr_interrupt_common(th, 0); | ||
|     rb_threadptr_interrupt_set(th, PENDING_INTERRUPT_MASK); | ||
| } | ||
| static void | ||
| threadptr_trap_interrupt(rb_thread_t *th) | ||
| { | ||
|     rb_threadptr_interrupt_common(th, 1); | ||
|     rb_threadptr_interrupt_set(th, TRAP_INTERRUPT_MASK); | ||
| } | ||
| static void | ||
| ... | ... | |
|     rb_timespec_now(ts); | ||
| } | ||
| void | ||
| rb_getclockofday(struct timespec *ts) | ||
| { | ||
|     getclockofday(ts); | ||
| } | ||
| static void | ||
| timespec_add(struct timespec *dst, const struct timespec *ts) | ||
| { | ||
| ... | ... | |
| 	int timer_interrupt; | ||
| 	int pending_interrupt; | ||
| 	int trap_interrupt; | ||
| 	int timeout_interrupt; | ||
| 	timer_interrupt = interrupt & TIMER_INTERRUPT_MASK; | ||
| 	pending_interrupt = interrupt & PENDING_INTERRUPT_MASK; | ||
| 	postponed_job_interrupt = interrupt & POSTPONED_JOB_INTERRUPT_MASK; | ||
| 	trap_interrupt = interrupt & TRAP_INTERRUPT_MASK; | ||
| 	timeout_interrupt = interrupt & TIMEOUT_INTERRUPT_MASK; | ||
| 	if (postponed_job_interrupt) { | ||
| 	    rb_postponed_job_flush(th->vm); | ||
| ... | ... | |
| 	    } | ||
| 	} | ||
| 	if (timeout_interrupt) { | ||
| 	    rb_timeout_expire(th->ec); | ||
| 	} | ||
| 	if (timer_interrupt) { | ||
| 	    uint32_t limits_us = TIME_QUANTUM_USEC; | ||
| ... | ... | |
|     } | ||
|     rb_native_mutex_unlock(&vm->thread_destruct_lock); | ||
|     if (vm->timer_thread_timeout >= 0) { | ||
|         rb_threadptr_interrupt_set(vm->main_thread, TIMEOUT_INTERRUPT_MASK); | ||
|     } | ||
|     /* check signal */ | ||
|     rb_threadptr_check_signal(vm->main_thread); | ||
|     vm->timer_thread_timeout = ATOMIC_EXCHANGE(vm->next_timeout, -1); | ||
| #if 0 | ||
|     /* prove profiler */ | ||
|     if (vm->prove_profile.enable) { | ||
| ... | ... | |
|     if (vm_living_thread_num(vm) > vm->sleeper) return; | ||
|     if (vm_living_thread_num(vm) < vm->sleeper) rb_bug("sleeper must not be more than vm_living_thread_num(vm)"); | ||
|     if (patrol_thread && patrol_thread != GET_THREAD()) return; | ||
|     if (rb_timeout_sleep_interval(vm, 0)) return; | ||
|     list_for_each(&vm->living_threads, th, vmlt_node) { | ||
| 	if (th->status != THREAD_STOPPED_FOREVER || RUBY_VM_INTERRUPTED(th->ec)) { | ||
| thread_pthread.c | ||
|---|---|---|
| void rb_native_cond_wait(rb_nativethread_cond_t *cond, rb_nativethread_lock_t *mutex); | ||
| void rb_native_cond_initialize(rb_nativethread_cond_t *cond); | ||
| void rb_native_cond_destroy(rb_nativethread_cond_t *cond); | ||
| static void rb_thread_wakeup_timer_thread_low(void); | ||
| void rb_thread_wakeup_timer_thread_low(void); | ||
| static struct { | ||
|     pthread_t id; | ||
|     int created; | ||
| ... | ... | |
|     } | ||
| } | ||
| static void | ||
| void | ||
| rb_thread_wakeup_timer_thread_low(void) | ||
| { | ||
|     if (timer_thread_pipe.owner_process == getpid()) { | ||
| ... | ... | |
|  * @pre the calling context is in the timer thread. | ||
|  */ | ||
| static inline void | ||
| timer_thread_sleep(rb_global_vm_lock_t* gvl) | ||
| timer_thread_sleep(rb_vm_t *vm) | ||
| { | ||
|     int result; | ||
|     int need_polling; | ||
| ... | ... | |
|     need_polling = !ubf_threads_empty(); | ||
|     if (gvl->waiting > 0 || need_polling) { | ||
|     if (vm->gvl.waiting > 0 || need_polling) { | ||
| 	/* polling (TIME_QUANTUM_USEC usec) */ | ||
| 	result = poll(pollfds, 1, TIME_QUANTUM_USEC/1000); | ||
|     } | ||
|     else { | ||
| 	/* wait (infinite) */ | ||
| 	result = poll(pollfds, numberof(pollfds), -1); | ||
| 	/* wait (infinite, or whatever timeout.c sets) */ | ||
| 	result = poll(pollfds, numberof(pollfds), vm->timer_thread_timeout); | ||
|     } | ||
|     if (result == 0) { | ||
| ... | ... | |
| #else /* USE_SLEEPY_TIMER_THREAD */ | ||
| # define PER_NANO 1000000000 | ||
| void rb_thread_wakeup_timer_thread(void) {} | ||
| static void rb_thread_wakeup_timer_thread_low(void) {} | ||
| void rb_thread_wakeup_timer_thread_low(void) {} | ||
| static rb_nativethread_lock_t timer_thread_lock; | ||
| static rb_nativethread_cond_t timer_thread_cond; | ||
| static inline void | ||
| timer_thread_sleep(rb_global_vm_lock_t* unused) | ||
| timer_thread_sleep(rb_vm_t *unused) | ||
| { | ||
|     struct timespec ts; | ||
|     ts.tv_sec = 0; | ||
| ... | ... | |
| static void * | ||
| thread_timer(void *p) | ||
| { | ||
|     rb_global_vm_lock_t *gvl = (rb_global_vm_lock_t *)p; | ||
|     rb_vm_t *vm = p; | ||
|     if (TT_DEBUG) WRITE_CONST(2, "start timer thread\n"); | ||
| ... | ... | |
| 	if (TT_DEBUG) WRITE_CONST(2, "tick\n"); | ||
|         /* wait */ | ||
| 	timer_thread_sleep(gvl); | ||
| 	timer_thread_sleep(vm); | ||
|     } | ||
| #if USE_SLEEPY_TIMER_THREAD | ||
|     CLOSE_INVALIDATE(normal[0]); | ||
| ... | ... | |
| 	if (timer_thread.created) { | ||
| 	    rb_bug("rb_thread_create_timer_thread: Timer thread was already created\n"); | ||
| 	} | ||
| 	err = pthread_create(&timer_thread.id, &attr, thread_timer, &vm->gvl); | ||
| 	err = pthread_create(&timer_thread.id, &attr, thread_timer, vm); | ||
| 	pthread_attr_destroy(&attr); | ||
| 	if (err == EINVAL) { | ||
| ... | ... | |
| 	     * default stack size is enough for them: | ||
| 	     */ | ||
| 	    stack_size = 0; | ||
| 	    err = pthread_create(&timer_thread.id, NULL, thread_timer, &vm->gvl); | ||
| 	    err = pthread_create(&timer_thread.id, NULL, thread_timer, vm); | ||
| 	} | ||
| 	if (err != 0) { | ||
| 	    rb_warn("pthread_create failed for timer: %s, scheduling broken", | ||
| thread_win32.c | ||
|---|---|---|
|     /* do nothing */ | ||
| } | ||
| void | ||
| rb_thread_wakeup_timer_thread_low(void) | ||
| { | ||
|     /* do nothing */ | ||
| } | ||
| static void | ||
| rb_thread_create_timer_thread(void) | ||
| { | ||
| timeout.c | ||
|---|---|---|
| #include "internal.h" | ||
| #include "vm_core.h" | ||
| /* match ccan/timer/timer.h, which we may support in the future: */ | ||
| struct timer { | ||
|     struct list_node list; | ||
|     uint64_t time; /* usec */ | ||
| }; | ||
| struct timeout { | ||
|     rb_execution_context_t *ec; | ||
|     VALUE sec; | ||
|     VALUE klass; | ||
|     VALUE message; | ||
|     struct timer t; | ||
| }; | ||
| static VALUE eTimeoutError, mTimeout, eUncaughtThrow; | ||
| static ID id_thread; | ||
| static uint64_t | ||
| timespec2usec(const struct timespec *ts) | ||
| { | ||
|     return (uint64_t)ts->tv_sec * 1000000 + (uint64_t)ts->tv_nsec / 1000; | ||
| } | ||
| static void | ||
| timers_ll_add(struct list_head *timers, struct timer *t, | ||
|               uint64_t rel_usec, uint64_t now_usec) | ||
| { | ||
|     struct timer *i = 0; | ||
|     t->time = rel_usec + now_usec; | ||
|     /* | ||
|      * search backwards: assume typical projects have multiple objects | ||
|      * sharing the same timeout values, so new timers will expire later | ||
|      * than existing timers | ||
|      */ | ||
|     list_for_each_rev(timers, i, list) { | ||
|         if (t->time >= i->time) { | ||
|             list_add_after(timers, &i->list, &t->list); | ||
|             return; | ||
|         } | ||
|     } | ||
|     list_add(timers, &t->list); | ||
| } | ||
| static struct timer * | ||
| timers_ll_expire(struct list_head *timers, uint64_t now_usec) | ||
| { | ||
|     struct timer *t = list_top(timers, struct timer, list); | ||
|     if (t && now_usec >= t->time) { | ||
|         list_del_init(&t->list); | ||
|         return t; | ||
|     } | ||
|     return 0; | ||
| } | ||
| static struct timer * | ||
| timers_ll_earliest(const struct list_head *timers) | ||
| { | ||
|     return list_top(timers, struct timer, list); | ||
| } | ||
| static VALUE | ||
| timeout_yield(VALUE tag, VALUE sec) | ||
| { | ||
|     return rb_yield(sec); | ||
| } | ||
| static VALUE | ||
| timeout_run(VALUE x) | ||
| { | ||
|     struct timeout *a = (struct timeout *)x; | ||
|     if (RTEST(a->klass)) { | ||
|       return rb_yield(a->sec); | ||
|     } | ||
|     /* for Timeout::Error#exception to throw */ | ||
|     a->message = rb_exc_new_str(eTimeoutError, a->message); | ||
|     /* hide for rb_gc_force_recycle */ | ||
|     RBASIC_CLEAR_CLASS(a->message); | ||
|     x = rb_catch_obj(a->message, timeout_yield, a->sec); | ||
|     if (x == a->message) { | ||
|         rb_attr_delete(x, id_thread); | ||
|         rb_exc_raise(x); | ||
|     } | ||
|     /* common case, no timeout, so exc is still hidden and safe to recycle */ | ||
|     VM_ASSERT(!RBASIC_CLASS(a->message) && RB_TYPE_P(a->message, T_OBJECT)); | ||
|     if (FL_TEST(a->message, FL_EXIVAR)) { | ||
|         rb_free_generic_ivar(a->message); | ||
| 	FL_UNSET(a->message, FL_EXIVAR); | ||
|     } | ||
|     rb_gc_force_recycle(a->message); | ||
|     return x; | ||
| } | ||
| static VALUE | ||
| timeout_ensure(VALUE x) | ||
| { | ||
|     struct timeout *a = (struct timeout *)x; | ||
|     list_del_init(&a->t.list); /* inlined timer_del */ | ||
|     return Qfalse; | ||
| } | ||
| static struct timeout * | ||
| rb_timers_expire_one(rb_vm_t *vm, uint64_t now_usec) | ||
| { | ||
|     struct timer *t = timers_ll_expire(&vm->timers, now_usec); | ||
|     return t ? container_of(t, struct timeout, t) : 0; | ||
| } | ||
| static void | ||
| arm_timer(rb_vm_t *vm, uint64_t rel_usec) | ||
| { | ||
|     int msec = rel_usec / 1000; | ||
|     ATOMIC_EXCHANGE(vm->next_timeout, (rb_atomic_t)msec); | ||
|     /* _low makes a difference in benchmark/bm_timeout_mt_nested.rb */ | ||
|     rb_thread_wakeup_timer_thread_low(); | ||
| } | ||
| struct expire_args { | ||
|     uint64_t now_usec; | ||
|     rb_thread_t *current_th; | ||
|     enum rb_thread_status prev_status; | ||
| }; | ||
| static VALUE | ||
| do_expire(VALUE x) | ||
| { | ||
|     struct expire_args *ea = (struct expire_args *)x; | ||
|     rb_vm_t *vm = ea->current_th->vm; | ||
|     struct timeout *a; | ||
|     while ((a = rb_timers_expire_one(vm, ea->now_usec))) { | ||
|         rb_thread_t *target_th = rb_ec_thread_ptr(a->ec); | ||
|         VALUE exc; | ||
|         if (RTEST(a->klass)) { | ||
|             exc = rb_exc_new_str(a->klass, a->message); | ||
|         } | ||
|         else { /* default, pre-made Timeout::Error */ | ||
|             exc = a->message; | ||
|             RBASIC_SET_CLASS_RAW(exc, eTimeoutError); /* reveal */ | ||
|             /* for Timeout::Error#exception to call `throw' */ | ||
|             rb_ivar_set(exc, id_thread, target_th->self); | ||
|         } | ||
|         if (ea->current_th == target_th) { | ||
|             rb_threadptr_pending_interrupt_enque(target_th, exc); | ||
|             rb_threadptr_interrupt(target_th); | ||
|         } | ||
|         else { | ||
|             rb_funcall(target_th->self, rb_intern("raise"), 1, exc); | ||
|         } | ||
|     } | ||
|     return Qfalse; | ||
| } | ||
| static VALUE | ||
| expire_ensure(VALUE p) | ||
| { | ||
|     struct expire_args *ea = (struct expire_args *)p; | ||
|     rb_vm_t *vm = ea->current_th->vm; | ||
|     struct timer *t = timers_ll_earliest(&vm->timers); | ||
|     if (t) { | ||
|         arm_timer(vm, t->time > ea->now_usec ? t->time - ea->now_usec : 0); | ||
|     } | ||
|     ea->current_th->status = ea->prev_status; | ||
|     return Qfalse; | ||
| } | ||
| void | ||
| rb_timeout_expire(const rb_execution_context_t *ec) | ||
| { | ||
|     struct expire_args ea; | ||
|     struct timespec ts; | ||
|     rb_getclockofday(&ts); | ||
|     ea.now_usec = timespec2usec(&ts); | ||
|     ea.current_th = rb_ec_thread_ptr(ec); | ||
|     ea.prev_status = ea.current_th->status; | ||
|     ea.current_th->status = THREAD_RUNNABLE; | ||
|     rb_ensure(do_expire, (VALUE)&ea, expire_ensure, (VALUE)&ea); | ||
| } | ||
| struct timespec * | ||
| rb_timeout_sleep_interval(rb_vm_t *vm, struct timespec *ts) | ||
| { | ||
|     struct timer *t = timers_ll_earliest(&vm->timers); | ||
|     if (t && !ts) { | ||
|         return (struct timespec *)-1; | ||
|     } | ||
|     if (t) { | ||
|         uint64_t now_usec; | ||
|         rb_getclockofday(ts); | ||
|         now_usec = timespec2usec(ts); | ||
|         if (t->time >= now_usec) { | ||
|             uint64_t rel_usec = t->time - now_usec; | ||
|             ts->tv_sec = rel_usec / 1000000; | ||
|             ts->tv_nsec = rel_usec % 1000000 * 1000; | ||
|         } | ||
|         else { | ||
|             ts->tv_sec = 0; | ||
|             ts->tv_nsec = 0; | ||
|         } | ||
|         return ts; | ||
|     } | ||
|     return 0; | ||
| } | ||
| static void | ||
| timeout_add(struct timeout *a) | ||
| { | ||
|     rb_vm_t *vm = rb_ec_vm_ptr(a->ec); | ||
|     struct timer *cur = timers_ll_earliest(&vm->timers); | ||
|     uint64_t now_usec, rel_usec; | ||
|     struct timeval tv = rb_time_interval(a->sec); | ||
|     struct timespec ts; | ||
|     ts.tv_sec = tv.tv_sec; | ||
|     ts.tv_nsec = tv.tv_usec * 1000; | ||
|     rel_usec = timespec2usec(&ts); | ||
|     rb_getclockofday(&ts); | ||
|     now_usec = timespec2usec(&ts); | ||
|     timers_ll_add(&vm->timers, &a->t, rel_usec, now_usec); | ||
|     if (!cur || timers_ll_earliest(&vm->timers) == &a->t) { | ||
|         arm_timer(vm, rel_usec); | ||
|     } | ||
| } | ||
| static VALUE | ||
| s_timeout(int argc, VALUE *argv, VALUE mod) | ||
| { | ||
|     struct timeout a; | ||
|     rb_scan_args(argc, argv, "12", &a.sec, &a.klass, &a.message); | ||
|     if (NIL_P(a.sec) || rb_equal(a.sec, INT2FIX(0))) { | ||
|         return rb_yield(a.sec); | ||
|     } | ||
|     if (!RTEST(a.message)) { | ||
|         a.message = rb_fstring_cstr("execution expired"); | ||
|     } | ||
|     a.ec = GET_EC(); | ||
|     timeout_add(&a); | ||
|     return rb_ensure(timeout_run, (VALUE)&a, timeout_ensure, (VALUE)&a); | ||
| } | ||
| static VALUE | ||
| begin_throw(VALUE self) | ||
| { | ||
|     rb_throw_obj(self, self); | ||
|     return self; | ||
| } | ||
| static VALUE | ||
| rescue_throw(VALUE ignore, VALUE err) | ||
| { | ||
|     return Qnil; | ||
| } | ||
| /* | ||
|  * We don't want to generate a backtrace like the version | ||
|  * in timeout.rb does.  We also want to raise the same | ||
|  * exception object so s_timeout (in core) can match | ||
|  * against it without relying on an extra proc for: | ||
|  * | ||
|  *      proc { |exception| return yield(sec) } | ||
|  */ | ||
| static VALUE | ||
| timeout_error_exception(int argc, VALUE *argv, VALUE self) | ||
| { | ||
|     if (rb_attr_get(self, id_thread) == rb_thread_current()) { | ||
|         rb_rescue2(begin_throw, self, rescue_throw, Qfalse, eUncaughtThrow, 0); | ||
|     } | ||
|     return self; | ||
| } | ||
| static VALUE | ||
| timeout_compat(int argc, VALUE *argv, VALUE mod) | ||
| { | ||
|     VALUE w[2]; | ||
|     w[0] = rb_funcall(mod, rb_intern("__method__"), 0); | ||
|     w[0] = rb_sprintf("Object#%"PRIsVALUE | ||
|                       " is deprecated, use Timeout.timeout instead.", w[0]); | ||
|     w[1] = rb_hash_new(); | ||
|     rb_hash_aset(w[1], ID2SYM(rb_intern("uplevel")), INT2FIX(1)); | ||
|     rb_funcallv(mod, rb_intern("warn"), 2, w); | ||
|     return s_timeout(argc, argv, mTimeout); | ||
| } | ||
| void | ||
| Init_timeout(void) | ||
| { | ||
| #undef rb_intern | ||
|     mTimeout = rb_define_module("Timeout"); | ||
|     eTimeoutError = rb_define_class_under(mTimeout, "Error", rb_eRuntimeError); | ||
|     eUncaughtThrow = rb_const_get(rb_cObject, rb_intern("UncaughtThrowError")); | ||
|     rb_define_method(mTimeout, "timeout", s_timeout, -1); | ||
|     rb_define_singleton_method(mTimeout, "timeout", s_timeout, -1); | ||
|     rb_define_method(eTimeoutError, "exception", timeout_error_exception, -1); | ||
|     id_thread = rb_intern("@thread"); | ||
|     /* backwards compatibility */ | ||
|     rb_define_method(rb_mKernel, "timeout", timeout_compat, -1); | ||
|     rb_const_set(rb_cObject, rb_intern("TimeoutError"), eTimeoutError); | ||
|     rb_deprecate_constant(rb_cObject, "TimeoutError"); | ||
|     rb_provide("timeout.rb"); | ||
| } | ||
| vm.c | ||
|---|---|---|
| { | ||
|     MEMZERO(vm, rb_vm_t, 1); | ||
|     rb_vm_living_threads_init(vm); | ||
|     list_head_init(&vm->timers); | ||
|     vm->thread_report_on_exception = 1; | ||
|     vm->src_encoding_index = -1; | ||
|     vm->next_timeout = (rb_atomic_t)-1; | ||
|     vm->timer_thread_timeout = -1; | ||
|     vm_default_params_setup(vm); | ||
| } | ||
| vm_core.h | ||
|---|---|---|
|     rb_global_vm_lock_t gvl; | ||
|     rb_nativethread_lock_t    thread_destruct_lock; | ||
|     struct list_head timers; /* TODO: consider moving to rb_thread_t */ | ||
|     rb_atomic_t next_timeout; | ||
|     int timer_thread_timeout; | ||
|     struct rb_thread_struct *main_thread; | ||
|     struct rb_thread_struct *running_thread; | ||
| ... | ... | |
| void rb_thread_stop_timer_thread(void); | ||
| void rb_thread_reset_timer_thread(void); | ||
| void rb_thread_wakeup_timer_thread(void); | ||
| void rb_thread_wakeup_timer_thread_low(void); | ||
| static inline void | ||
| rb_vm_living_threads_init(rb_vm_t *vm) | ||
| ... | ... | |
|     TIMER_INTERRUPT_MASK         = 0x01, | ||
|     PENDING_INTERRUPT_MASK       = 0x02, | ||
|     POSTPONED_JOB_INTERRUPT_MASK = 0x04, | ||
|     TRAP_INTERRUPT_MASK	         = 0x08 | ||
|     TRAP_INTERRUPT_MASK	         = 0x08, | ||
|     TIMEOUT_INTERRUPT_MASK       = 0x10 | ||
| }; | ||
| #define RUBY_VM_SET_TIMER_INTERRUPT(ec)		ATOMIC_OR((ec)->interrupt_flag, TIMER_INTERRUPT_MASK) | ||
| #define RUBY_VM_SET_INTERRUPT(ec)		ATOMIC_OR((ec)->interrupt_flag, PENDING_INTERRUPT_MASK) | ||
| #define RUBY_VM_SET_POSTPONED_JOB_INTERRUPT(ec)	ATOMIC_OR((ec)->interrupt_flag, POSTPONED_JOB_INTERRUPT_MASK) | ||
| #define RUBY_VM_SET_TRAP_INTERRUPT(ec)		ATOMIC_OR((ec)->interrupt_flag, TRAP_INTERRUPT_MASK) | ||
| #define RUBY_VM_INTERRUPTED(ec)			((ec)->interrupt_flag & ~(ec)->interrupt_mask & \ | ||
| 						 (PENDING_INTERRUPT_MASK|TRAP_INTERRUPT_MASK)) | ||
| #define RUBY_VM_INTERRUPTED(ec)                 ((ec)->interrupt_flag & \ | ||
|                                                   ~(ec)->interrupt_mask & \ | ||
|                                                   (PENDING_INTERRUPT_MASK|\ | ||
|                                                    TRAP_INTERRUPT_MASK|\ | ||
|                                                    TIMEOUT_INTERRUPT_MASK)) | ||
| #define RUBY_VM_INTERRUPTED_ANY(ec)		((ec)->interrupt_flag & ~(ec)->interrupt_mask) | ||
| VALUE rb_exc_set_backtrace(VALUE exc, VALUE bt); | ||
| -  | ||