abid 0.3.0.pre.alpha.2 → 0.3.0.pre.alpha.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,137 @@
1
+ require 'concurrent/ivar'
2
+ require 'forwardable'
3
+ require 'monitor'
4
+
5
+ module Abid
6
+ module Engine
7
+ # @!visibility private
8
+
9
+ # Process object manages the task execution status.
10
+ #
11
+ # You should retrive a process object via Job#process.
12
+ # Do not create a process object by Process.new constructor.
13
+ #
14
+ # A process object has an internal status of the task execution and
15
+ # the task result (Process#result).
16
+ #
17
+ # An initial status is :unscheduled.
18
+ # When Process#prepare is called, the status gets :pending.
19
+ # When Process#execute is called and the task is posted to a thread pool,
20
+ # the status gets :running. When the task is finished, the status gets
21
+ # :complete and the result is assigned to :successed or :failed.
22
+ #
23
+ # process = Job['task_name'].process
24
+ # process.prepare
25
+ # process.start
26
+ # process.wait
27
+ # process.result #=> :successed or :failed
28
+ #
29
+ # Possible status are:
30
+ #
31
+ # <dl>
32
+ # <dt>:unscheduled</dt>
33
+ # <dd>The task is not invoked yet.</dd>
34
+ # <dt>:pending</dt>
35
+ # <dd>The task is waiting for prerequisites complete.</dd>
36
+ # <dt>:running</dt>
37
+ # <dd>The task is running.</dd>
38
+ # <dt>:complete</dt>
39
+ # <dd>The task is finished.</dd>
40
+ # </dl>
41
+ #
42
+ # Possible results are:
43
+ #
44
+ # <dl>
45
+ # <dt>:successed</dt>
46
+ # <dd>The task is successed.</dd>
47
+ # <dt>:failed</dt>
48
+ # <dd>The task is failed.</dd>
49
+ # <dt>:cancelled</dt>
50
+ # <dd>The task is not executed because of some problems.</dd>
51
+ # <dt>:skipped</dt>
52
+ # <dd>The task is not executed because already successed.</dd>
53
+ # </dl>
54
+ class Process
55
+ extend Forwardable
56
+
57
+ attr_reader :status, :error
58
+
59
+ def_delegators :@result_ivar, :add_observer, :wait, :complete?
60
+
61
+ def initialize(process_manager)
62
+ @process_manager = process_manager
63
+ @result_ivar = Concurrent::IVar.new
64
+ @status = :unscheduled
65
+ @error = nil
66
+ @mon = Monitor.new
67
+ end
68
+
69
+ %w(successed failed cancelled skipped).each do |meth|
70
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
71
+ def #{meth}?
72
+ result == :#{meth}
73
+ end
74
+ RUBY
75
+ end
76
+
77
+ def result
78
+ @result_ivar.value if @result_ivar.complete?
79
+ end
80
+
81
+ def prepare
82
+ compare_and_set_status(:pending, :unscheduled)
83
+ end
84
+
85
+ def start
86
+ compare_and_set_status(:running, :pending)
87
+ end
88
+
89
+ def finish(error = nil)
90
+ return unless compare_and_set_status(:complete, :running)
91
+ @error = error if error
92
+ @result_ivar.set(error.nil? ? :successed : :failed)
93
+ true
94
+ end
95
+
96
+ def cancel(error = nil)
97
+ return false unless compare_and_set_status(:complete, :pending)
98
+ @error = error if error
99
+ @result_ivar.set :cancelled
100
+ true
101
+ end
102
+
103
+ def skip
104
+ return false unless compare_and_set_status(:complete, :pending)
105
+ @result_ivar.set :skipped
106
+ true
107
+ end
108
+
109
+ # Force fail the task.
110
+ # @return [void]
111
+ def quit(error)
112
+ @status = :complete
113
+ @error = error
114
+ @result_ivar.try_set(:failed)
115
+ end
116
+
117
+ private
118
+
119
+ # Atomic compare and set operation.
120
+ # State is set to `next_state` only if
121
+ # `current state == expected_current`.
122
+ #
123
+ # @param [Symbol] next_state
124
+ # @param [Symbol] expected_current
125
+ #
126
+ # @return [Boolean] true if state is changed, false otherwise
127
+ def compare_and_set_status(next_state, *expected_current)
128
+ @mon.synchronize do
129
+ return unless expected_current.include? @status
130
+ @status = next_state
131
+ @process_manager.update(self)
132
+ true
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,56 @@
1
+ require 'monitor'
2
+
3
+ module Abid
4
+ module Engine
5
+ class ProcessManager
6
+ attr_reader :active_processes
7
+
8
+ # @param env [Environment] abid environment
9
+ def initialize(env)
10
+ @env = env
11
+ @active_processes = {}.compare_by_identity
12
+ @mon = Monitor.new
13
+ end
14
+
15
+ # @return [Process] new process
16
+ def create
17
+ Process.new(self)
18
+ end
19
+
20
+ # Update active process set.
21
+ # @param process [Process]
22
+ def update(process)
23
+ case process.status
24
+ when :pending, :running
25
+ add_active(process)
26
+ when :complete
27
+ delete_active(process)
28
+ end
29
+ end
30
+
31
+ # Kill all active processes
32
+ # @param error [Exception] error reason
33
+ def kill(error)
34
+ each_active { |p| p.quit(error) }
35
+ end
36
+
37
+ def active?(process)
38
+ @mon.synchronize { @active_processes.include? process }
39
+ end
40
+
41
+ private
42
+
43
+ def add_active(process)
44
+ @mon.synchronize { @active_processes[process] ||= process }
45
+ end
46
+
47
+ def delete_active(process)
48
+ @mon.synchronize { @active_processes.delete(process) }
49
+ end
50
+
51
+ def each_active(&block)
52
+ @mon.synchronize { @active_processes.values.each(&block) }
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,96 @@
1
+ require 'concurrent/atomic/atomic_fixnum'
2
+
3
+ module Abid
4
+ module Engine
5
+ # Scheduler operates whole job flow execution.
6
+ class Scheduler
7
+ # @return [void]
8
+ def self.invoke(job, *args, invocation_chain: nil)
9
+ task_args = Rake::TaskArguments.new(job.task.arg_names, args)
10
+ invocation_chain ||= Rake::InvocationChain::EMPTY
11
+
12
+ detect_circular_dependency(job, invocation_chain)
13
+ new(job, task_args, invocation_chain).invoke
14
+ end
15
+
16
+ # @!visibility private
17
+
18
+ # Execute given block when DependencyCounter#update is called `count`
19
+ # times.
20
+ class DependencyCounter
21
+ def initialize(count, &block)
22
+ @counter = Concurrent::AtomicFixnum.new(count)
23
+ @block = block
24
+ yield if count <= 0
25
+ end
26
+
27
+ def update(*_)
28
+ @block.call if @counter.decrement.zero?
29
+ end
30
+ end
31
+
32
+ def self.detect_circular_dependency(job, chain)
33
+ # raise error if job.task is a member of the chain
34
+ new_chain = Rake::InvocationChain.append(job.task, chain)
35
+
36
+ job.prerequisites.each do |preq_job|
37
+ detect_circular_dependency(preq_job, new_chain)
38
+ end
39
+ end
40
+
41
+ def initialize(job, args, invocation_chain)
42
+ @job = job
43
+ @args = args
44
+ @chain = invocation_chain.conj(@job.task)
45
+ @executor = Executor.new(job, args)
46
+ end
47
+
48
+ def invoke
49
+ return unless @executor.prepare
50
+
51
+ trace_invoke
52
+ attach_chain
53
+ invoke_prerequisites
54
+ after_prerequisites do
55
+ @executor.capture_exception do
56
+ @executor.start
57
+ end
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def trace_invoke
64
+ return unless @job.env.options.trace
65
+ @job.env.application.trace \
66
+ "** Invoke #{@job.task.name} #{@job.task.format_trace_flags}"
67
+ end
68
+
69
+ def attach_chain
70
+ @job.process.add_observer do
71
+ error = @job.process.error
72
+ next if error.nil?
73
+ next if @chain.nil?
74
+
75
+ error.extend(Rake::InvocationExceptionMixin) unless
76
+ error.respond_to?(:chain)
77
+ error.chain ||= @chain
78
+ end
79
+ end
80
+
81
+ def invoke_prerequisites
82
+ @job.prerequisites.each do |preq_job|
83
+ preq_args = @args.new_scope(@job.task.arg_names)
84
+ Scheduler.new(preq_job, preq_args, @chain).invoke
85
+ end
86
+ end
87
+
88
+ def after_prerequisites(&block)
89
+ counter = DependencyCounter.new(@job.prerequisites.size, &block)
90
+ @job.prerequisites.each do |preq_job|
91
+ preq_job.process.add_observer counter
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,83 @@
1
+ module Abid
2
+ module Engine
3
+ # Waits for a job to be finished which is running in external application,
4
+ # and completes the job in its own application.
5
+ #
6
+ # Waiter.new(job).wait
7
+ #
8
+ # The `job` result gets :successed or :failed when external application
9
+ # finished the job execution.
10
+ class Waiter
11
+ DEFAULT_WAIT_INTERVAL = 10
12
+ DEFAULT_WAIT_TIMEOUT = 3600
13
+
14
+ def initialize(job)
15
+ @job = job
16
+ @process = job.process
17
+ @wait_limit = Concurrent.monotonic_time + wait_timeout
18
+ end
19
+
20
+ def wait
21
+ unless @job.env.options.wait_external_task
22
+ @process.finish(AlreadyRunningError.new('job already running'))
23
+ return
24
+ end
25
+
26
+ wait_iter
27
+ end
28
+
29
+ private
30
+
31
+ def wait_interval
32
+ @job.env.options.wait_external_task_interval ||
33
+ DEFAULT_WAIT_INTERVAL
34
+ end
35
+
36
+ def wait_timeout
37
+ @job.env.options.wait_external_task_timeout ||
38
+ DEFAULT_WAIT_TIMEOUT
39
+ end
40
+
41
+ def wait_iter
42
+ @job.env.worker_manager[:timer_set].post(wait_interval) do
43
+ capture_exception do
44
+ state = @job.state.find
45
+
46
+ check_finished(state) ||
47
+ check_timeout ||
48
+ wait_iter
49
+ end
50
+ end
51
+ end
52
+
53
+ def check_finished(state)
54
+ return false if state.running?
55
+
56
+ if state.successed?
57
+ @process.finish
58
+ elsif state.failed?
59
+ @process.finish RuntimeError.new('task failed while waiting')
60
+ else
61
+ @process.finish RuntimeError.new('unexpected task state')
62
+ end
63
+ true
64
+ end
65
+
66
+ def check_timeout
67
+ return false if Concurrent.monotonic_time < @wait_limit
68
+
69
+ @process.finish RuntimeError.new('timeout')
70
+ true
71
+ end
72
+
73
+ def capture_exception
74
+ yield
75
+ rescue StandardError, ScriptError => error
76
+ @process.quit(error)
77
+ rescue Exception => exception
78
+ # TODO: exit immediately when fatal error occurs.
79
+ @process.quit(exception)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,123 @@
1
+ require 'forwardable'
2
+ require 'monitor'
3
+
4
+ require 'concurrent/delay'
5
+
6
+ module Abid
7
+ module Engine
8
+ # WorkerManager manges thread pools definition, creation and termination.
9
+ #
10
+ # worker_manager = Abid.global.worker_manager
11
+ #
12
+ # worker_manager.define(:main, 2)
13
+ #
14
+ # worker_manager[:main].post { :do_something }
15
+ #
16
+ # worker_manager.shutdown
17
+ #
18
+ class WorkerManager
19
+ def initialize(env)
20
+ @env = env
21
+ @workers = {}
22
+ @alive = true
23
+ @mon = Monitor.new
24
+
25
+ initialize_builtin_workers
26
+ end
27
+
28
+ # Define new worker.
29
+ #
30
+ # An actual worker is created when needed.
31
+ def define(name, num_threads)
32
+ @mon.synchronize do
33
+ check_alive!
34
+
35
+ if @workers.include?(name)
36
+ raise Error, "worker #{name} is already defined"
37
+ end
38
+
39
+ @workers[name] = Concurrent::Delay.new do
40
+ create_worker(num_threads: num_threads)
41
+ end
42
+ end
43
+ end
44
+
45
+ # Find or create worker
46
+ #
47
+ # @param name [String, Symbol] worker name
48
+ # @return [Concurrent::ExecutorService]
49
+ def [](name)
50
+ @mon.synchronize do
51
+ check_alive!
52
+
53
+ unless @workers.include?(name)
54
+ raise Error, "worker #{name} is not defined"
55
+ end
56
+
57
+ @workers[name].value!
58
+ end
59
+ end
60
+
61
+ def shutdown(timeout = nil)
62
+ @mon.synchronize do
63
+ check_alive!
64
+ each_active(&:shutdown)
65
+ each_active { |worker| worker.wait_for_termination(timeout) }
66
+
67
+ result = each_active.all?(&:shutdown?)
68
+ @alive = false if result
69
+ result
70
+ end
71
+ end
72
+
73
+ def kill
74
+ @mon.synchronize do
75
+ check_alive!
76
+ @alive = false
77
+ each_active(&:kill)
78
+ end
79
+ true
80
+ end
81
+
82
+ private
83
+
84
+ def initialize_builtin_workers
85
+ @workers[:default] = Concurrent::Delay.new { create_default_worker }
86
+ @workers[:waiter] = Concurrent::Delay.new { Concurrent.new_io_executor }
87
+ @workers[:timer_set] = Concurrent::Delay.new do
88
+ Concurrent::TimerSet.new(executor: self[:waiter])
89
+ end
90
+ end
91
+
92
+ def create_worker(definition)
93
+ if definition[:num_threads] > 0
94
+ Concurrent::FixedThreadPool.new(definition[:num_threads])
95
+ else
96
+ Concurrent::CachedThreadPool.new
97
+ end
98
+ end
99
+
100
+ def create_default_worker
101
+ create_worker(num_threads: default_num_threads)
102
+ end
103
+
104
+ def default_num_threads
105
+ if @env.options.always_multitask
106
+ @env.options.thread_pool_size ||
107
+ Rake.suggested_num_threads - 1
108
+ else
109
+ 1
110
+ end
111
+ end
112
+
113
+ def check_alive!
114
+ raise Error, 'already terminated' unless @alive
115
+ end
116
+
117
+ # Iterate on active workers
118
+ def each_active(&block)
119
+ @workers.values.select(&:fulfilled?).map(&:value).each(&block)
120
+ end
121
+ end
122
+ end
123
+ end