335 lines
12 KiB
Ruby
335 lines
12 KiB
Ruby
|
require 'concurrent/collection/copy_on_notify_observer_set'
|
||
|
require 'concurrent/concern/dereferenceable'
|
||
|
require 'concurrent/concern/observable'
|
||
|
require 'concurrent/atomic/atomic_boolean'
|
||
|
require 'concurrent/executor/executor_service'
|
||
|
require 'concurrent/executor/ruby_executor_service'
|
||
|
require 'concurrent/executor/safe_task_executor'
|
||
|
require 'concurrent/scheduled_task'
|
||
|
|
||
|
module Concurrent
|
||
|
|
||
|
# A very common concurrency pattern is to run a thread that performs a task at
|
||
|
# regular intervals. The thread that performs the task sleeps for the given
|
||
|
# interval then wakes up and performs the task. Lather, rinse, repeat... This
|
||
|
# pattern causes two problems. First, it is difficult to test the business
|
||
|
# logic of the task because the task itself is tightly coupled with the
|
||
|
# concurrency logic. Second, an exception raised while performing the task can
|
||
|
# cause the entire thread to abend. In a long-running application where the
|
||
|
# task thread is intended to run for days/weeks/years a crashed task thread
|
||
|
# can pose a significant problem. `TimerTask` alleviates both problems.
|
||
|
#
|
||
|
# When a `TimerTask` is launched it starts a thread for monitoring the
|
||
|
# execution interval. The `TimerTask` thread does not perform the task,
|
||
|
# however. Instead, the TimerTask launches the task on a separate thread.
|
||
|
# Should the task experience an unrecoverable crash only the task thread will
|
||
|
# crash. This makes the `TimerTask` very fault tolerant. Additionally, the
|
||
|
# `TimerTask` thread can respond to the success or failure of the task,
|
||
|
# performing logging or ancillary operations. `TimerTask` can also be
|
||
|
# configured with a timeout value allowing it to kill a task that runs too
|
||
|
# long.
|
||
|
#
|
||
|
# One other advantage of `TimerTask` is that it forces the business logic to
|
||
|
# be completely decoupled from the concurrency logic. The business logic can
|
||
|
# be tested separately then passed to the `TimerTask` for scheduling and
|
||
|
# running.
|
||
|
#
|
||
|
# In some cases it may be necessary for a `TimerTask` to affect its own
|
||
|
# execution cycle. To facilitate this, a reference to the TimerTask instance
|
||
|
# is passed as an argument to the provided block every time the task is
|
||
|
# executed.
|
||
|
#
|
||
|
# The `TimerTask` class includes the `Dereferenceable` mixin module so the
|
||
|
# result of the last execution is always available via the `#value` method.
|
||
|
# Dereferencing options can be passed to the `TimerTask` during construction or
|
||
|
# at any later time using the `#set_deref_options` method.
|
||
|
#
|
||
|
# `TimerTask` supports notification through the Ruby standard library
|
||
|
# {http://ruby-doc.org/stdlib-2.0/libdoc/observer/rdoc/Observable.html
|
||
|
# Observable} module. On execution the `TimerTask` will notify the observers
|
||
|
# with three arguments: time of execution, the result of the block (or nil on
|
||
|
# failure), and any raised exceptions (or nil on success). If the timeout
|
||
|
# interval is exceeded the observer will receive a `Concurrent::TimeoutError`
|
||
|
# object as the third argument.
|
||
|
#
|
||
|
# @!macro copy_options
|
||
|
#
|
||
|
# @example Basic usage
|
||
|
# task = Concurrent::TimerTask.new{ puts 'Boom!' }
|
||
|
# task.execute
|
||
|
#
|
||
|
# task.execution_interval #=> 60 (default)
|
||
|
# task.timeout_interval #=> 30 (default)
|
||
|
#
|
||
|
# # wait 60 seconds...
|
||
|
# #=> 'Boom!'
|
||
|
#
|
||
|
# task.shutdown #=> true
|
||
|
#
|
||
|
# @example Configuring `:execution_interval` and `:timeout_interval`
|
||
|
# task = Concurrent::TimerTask.new(execution_interval: 5, timeout_interval: 5) do
|
||
|
# puts 'Boom!'
|
||
|
# end
|
||
|
#
|
||
|
# task.execution_interval #=> 5
|
||
|
# task.timeout_interval #=> 5
|
||
|
#
|
||
|
# @example Immediate execution with `:run_now`
|
||
|
# task = Concurrent::TimerTask.new(run_now: true){ puts 'Boom!' }
|
||
|
# task.execute
|
||
|
#
|
||
|
# #=> 'Boom!'
|
||
|
#
|
||
|
# @example Last `#value` and `Dereferenceable` mixin
|
||
|
# task = Concurrent::TimerTask.new(
|
||
|
# dup_on_deref: true,
|
||
|
# execution_interval: 5
|
||
|
# ){ Time.now }
|
||
|
#
|
||
|
# task.execute
|
||
|
# Time.now #=> 2013-11-07 18:06:50 -0500
|
||
|
# sleep(10)
|
||
|
# task.value #=> 2013-11-07 18:06:55 -0500
|
||
|
#
|
||
|
# @example Controlling execution from within the block
|
||
|
# timer_task = Concurrent::TimerTask.new(execution_interval: 1) do |task|
|
||
|
# task.execution_interval.times{ print 'Boom! ' }
|
||
|
# print "\n"
|
||
|
# task.execution_interval += 1
|
||
|
# if task.execution_interval > 5
|
||
|
# puts 'Stopping...'
|
||
|
# task.shutdown
|
||
|
# end
|
||
|
# end
|
||
|
#
|
||
|
# timer_task.execute # blocking call - this task will stop itself
|
||
|
# #=> Boom!
|
||
|
# #=> Boom! Boom!
|
||
|
# #=> Boom! Boom! Boom!
|
||
|
# #=> Boom! Boom! Boom! Boom!
|
||
|
# #=> Boom! Boom! Boom! Boom! Boom!
|
||
|
# #=> Stopping...
|
||
|
#
|
||
|
# @example Observation
|
||
|
# class TaskObserver
|
||
|
# def update(time, result, ex)
|
||
|
# if result
|
||
|
# print "(#{time}) Execution successfully returned #{result}\n"
|
||
|
# elsif ex.is_a?(Concurrent::TimeoutError)
|
||
|
# print "(#{time}) Execution timed out\n"
|
||
|
# else
|
||
|
# print "(#{time}) Execution failed with error #{ex}\n"
|
||
|
# end
|
||
|
# end
|
||
|
# end
|
||
|
#
|
||
|
# task = Concurrent::TimerTask.new(execution_interval: 1, timeout_interval: 1){ 42 }
|
||
|
# task.add_observer(TaskObserver.new)
|
||
|
# task.execute
|
||
|
# sleep 4
|
||
|
#
|
||
|
# #=> (2013-10-13 19:08:58 -0400) Execution successfully returned 42
|
||
|
# #=> (2013-10-13 19:08:59 -0400) Execution successfully returned 42
|
||
|
# #=> (2013-10-13 19:09:00 -0400) Execution successfully returned 42
|
||
|
# task.shutdown
|
||
|
#
|
||
|
# task = Concurrent::TimerTask.new(execution_interval: 1, timeout_interval: 1){ sleep }
|
||
|
# task.add_observer(TaskObserver.new)
|
||
|
# task.execute
|
||
|
#
|
||
|
# #=> (2013-10-13 19:07:25 -0400) Execution timed out
|
||
|
# #=> (2013-10-13 19:07:27 -0400) Execution timed out
|
||
|
# #=> (2013-10-13 19:07:29 -0400) Execution timed out
|
||
|
# task.shutdown
|
||
|
#
|
||
|
# task = Concurrent::TimerTask.new(execution_interval: 1){ raise StandardError }
|
||
|
# task.add_observer(TaskObserver.new)
|
||
|
# task.execute
|
||
|
#
|
||
|
# #=> (2013-10-13 19:09:37 -0400) Execution failed with error StandardError
|
||
|
# #=> (2013-10-13 19:09:38 -0400) Execution failed with error StandardError
|
||
|
# #=> (2013-10-13 19:09:39 -0400) Execution failed with error StandardError
|
||
|
# task.shutdown
|
||
|
#
|
||
|
# @see http://ruby-doc.org/stdlib-2.0/libdoc/observer/rdoc/Observable.html
|
||
|
# @see http://docs.oracle.com/javase/7/docs/api/java/util/TimerTask.html
|
||
|
class TimerTask < RubyExecutorService
|
||
|
include Concern::Dereferenceable
|
||
|
include Concern::Observable
|
||
|
|
||
|
# Default `:execution_interval` in seconds.
|
||
|
EXECUTION_INTERVAL = 60
|
||
|
|
||
|
# Default `:timeout_interval` in seconds.
|
||
|
TIMEOUT_INTERVAL = 30
|
||
|
|
||
|
# Create a new TimerTask with the given task and configuration.
|
||
|
#
|
||
|
# @!macro timer_task_initialize
|
||
|
# @param [Hash] opts the options defining task execution.
|
||
|
# @option opts [Integer] :execution_interval number of seconds between
|
||
|
# task executions (default: EXECUTION_INTERVAL)
|
||
|
# @option opts [Integer] :timeout_interval number of seconds a task can
|
||
|
# run before it is considered to have failed (default: TIMEOUT_INTERVAL)
|
||
|
# @option opts [Boolean] :run_now Whether to run the task immediately
|
||
|
# upon instantiation or to wait until the first # execution_interval
|
||
|
# has passed (default: false)
|
||
|
#
|
||
|
# @!macro deref_options
|
||
|
#
|
||
|
# @raise ArgumentError when no block is given.
|
||
|
#
|
||
|
# @yield to the block after :execution_interval seconds have passed since
|
||
|
# the last yield
|
||
|
# @yieldparam task a reference to the `TimerTask` instance so that the
|
||
|
# block can control its own lifecycle. Necessary since `self` will
|
||
|
# refer to the execution context of the block rather than the running
|
||
|
# `TimerTask`.
|
||
|
#
|
||
|
# @return [TimerTask] the new `TimerTask`
|
||
|
def initialize(opts = {}, &task)
|
||
|
raise ArgumentError.new('no block given') unless block_given?
|
||
|
super
|
||
|
set_deref_options opts
|
||
|
end
|
||
|
|
||
|
# Is the executor running?
|
||
|
#
|
||
|
# @return [Boolean] `true` when running, `false` when shutting down or shutdown
|
||
|
def running?
|
||
|
@running.true?
|
||
|
end
|
||
|
|
||
|
# Execute a previously created `TimerTask`.
|
||
|
#
|
||
|
# @return [TimerTask] a reference to `self`
|
||
|
#
|
||
|
# @example Instance and execute in separate steps
|
||
|
# task = Concurrent::TimerTask.new(execution_interval: 10){ print "Hello World\n" }
|
||
|
# task.running? #=> false
|
||
|
# task.execute
|
||
|
# task.running? #=> true
|
||
|
#
|
||
|
# @example Instance and execute in one line
|
||
|
# task = Concurrent::TimerTask.new(execution_interval: 10){ print "Hello World\n" }.execute
|
||
|
# task.running? #=> true
|
||
|
def execute
|
||
|
synchronize do
|
||
|
if @running.false?
|
||
|
@running.make_true
|
||
|
schedule_next_task(@run_now ? 0 : @execution_interval)
|
||
|
end
|
||
|
end
|
||
|
self
|
||
|
end
|
||
|
|
||
|
# Create and execute a new `TimerTask`.
|
||
|
#
|
||
|
# @!macro timer_task_initialize
|
||
|
#
|
||
|
# @example
|
||
|
# task = Concurrent::TimerTask.execute(execution_interval: 10){ print "Hello World\n" }
|
||
|
# task.running? #=> true
|
||
|
def self.execute(opts = {}, &task)
|
||
|
TimerTask.new(opts, &task).execute
|
||
|
end
|
||
|
|
||
|
# @!attribute [rw] execution_interval
|
||
|
# @return [Fixnum] Number of seconds after the task completes before the
|
||
|
# task is performed again.
|
||
|
def execution_interval
|
||
|
synchronize { @execution_interval }
|
||
|
end
|
||
|
|
||
|
# @!attribute [rw] execution_interval
|
||
|
# @return [Fixnum] Number of seconds after the task completes before the
|
||
|
# task is performed again.
|
||
|
def execution_interval=(value)
|
||
|
if (value = value.to_f) <= 0.0
|
||
|
raise ArgumentError.new('must be greater than zero')
|
||
|
else
|
||
|
synchronize { @execution_interval = value }
|
||
|
end
|
||
|
end
|
||
|
|
||
|
# @!attribute [rw] timeout_interval
|
||
|
# @return [Fixnum] Number of seconds the task can run before it is
|
||
|
# considered to have failed.
|
||
|
def timeout_interval
|
||
|
synchronize { @timeout_interval }
|
||
|
end
|
||
|
|
||
|
# @!attribute [rw] timeout_interval
|
||
|
# @return [Fixnum] Number of seconds the task can run before it is
|
||
|
# considered to have failed.
|
||
|
def timeout_interval=(value)
|
||
|
if (value = value.to_f) <= 0.0
|
||
|
raise ArgumentError.new('must be greater than zero')
|
||
|
else
|
||
|
synchronize { @timeout_interval = value }
|
||
|
end
|
||
|
end
|
||
|
|
||
|
private :post, :<<
|
||
|
|
||
|
private
|
||
|
|
||
|
def ns_initialize(opts, &task)
|
||
|
set_deref_options(opts)
|
||
|
|
||
|
self.execution_interval = opts[:execution] || opts[:execution_interval] || EXECUTION_INTERVAL
|
||
|
self.timeout_interval = opts[:timeout] || opts[:timeout_interval] || TIMEOUT_INTERVAL
|
||
|
@run_now = opts[:now] || opts[:run_now]
|
||
|
@executor = Concurrent::SafeTaskExecutor.new(task)
|
||
|
@running = Concurrent::AtomicBoolean.new(false)
|
||
|
@value = nil
|
||
|
|
||
|
self.observers = Collection::CopyOnNotifyObserverSet.new
|
||
|
end
|
||
|
|
||
|
# @!visibility private
|
||
|
def ns_shutdown_execution
|
||
|
@running.make_false
|
||
|
super
|
||
|
end
|
||
|
|
||
|
# @!visibility private
|
||
|
def ns_kill_execution
|
||
|
@running.make_false
|
||
|
super
|
||
|
end
|
||
|
|
||
|
# @!visibility private
|
||
|
def schedule_next_task(interval = execution_interval)
|
||
|
ScheduledTask.execute(interval, args: [Concurrent::Event.new], &method(:execute_task))
|
||
|
nil
|
||
|
end
|
||
|
|
||
|
# @!visibility private
|
||
|
def execute_task(completion)
|
||
|
return nil unless @running.true?
|
||
|
ScheduledTask.execute(timeout_interval, args: [completion], &method(:timeout_task))
|
||
|
_success, value, reason = @executor.execute(self)
|
||
|
if completion.try?
|
||
|
self.value = value
|
||
|
schedule_next_task
|
||
|
time = Time.now
|
||
|
observers.notify_observers do
|
||
|
[time, self.value, reason]
|
||
|
end
|
||
|
end
|
||
|
nil
|
||
|
end
|
||
|
|
||
|
# @!visibility private
|
||
|
def timeout_task(completion)
|
||
|
return unless @running.true?
|
||
|
if completion.try?
|
||
|
self.value = value
|
||
|
schedule_next_task
|
||
|
observers.notify_observers(Time.now, nil, Concurrent::TimeoutError.new)
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|