taski 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +12 -13
- data/Steepfile +1 -0
- data/examples/README.md +3 -3
- data/examples/data_pipeline_demo.rb +3 -3
- data/examples/nested_section_demo.rb +161 -0
- data/examples/parallel_progress_demo.rb +1 -1
- data/examples/section_progress_demo.rb +78 -0
- data/examples/tree_progress_demo.rb +164 -0
- data/lib/taski/execution/executor.rb +247 -0
- data/lib/taski/execution/registry.rb +9 -1
- data/lib/taski/execution/task_wrapper.rb +126 -147
- data/lib/taski/execution/tree_progress_display.rb +506 -0
- data/lib/taski/section.rb +10 -0
- data/lib/taski/static_analysis/analyzer.rb +4 -2
- data/lib/taski/static_analysis/visitor.rb +86 -5
- data/lib/taski/task.rb +11 -94
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +10 -6
- data/sig/taski.rbs +127 -64
- metadata +6 -3
- data/lib/taski/execution/coordinator.rb +0 -63
- data/lib/taski/execution/parallel_progress_display.rb +0 -201
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
require "etc"
|
|
5
|
+
|
|
6
|
+
module Taski
|
|
7
|
+
module Execution
|
|
8
|
+
# Producer-Consumer pattern executor for parallel task execution.
|
|
9
|
+
#
|
|
10
|
+
# Architecture:
|
|
11
|
+
# - Main Thread: Manages all state, coordinates execution, handles events
|
|
12
|
+
# - Worker Threads: Execute tasks and send completion events
|
|
13
|
+
#
|
|
14
|
+
# Communication Queues:
|
|
15
|
+
# - Execution Queue (Main -> Worker): Tasks ready to execute
|
|
16
|
+
# - Completion Queue (Worker -> Main): Events from workers
|
|
17
|
+
class Executor
|
|
18
|
+
# Task execution states for the executor's internal tracking
|
|
19
|
+
STATE_PENDING = :pending
|
|
20
|
+
STATE_ENQUEUED = :enqueued
|
|
21
|
+
STATE_COMPLETED = :completed
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
# Execute a task and all its dependencies
|
|
25
|
+
# @param root_task_class [Class] The root task class to execute
|
|
26
|
+
# @param registry [Registry] The task registry
|
|
27
|
+
def execute(root_task_class, registry:)
|
|
28
|
+
new(registry: registry).execute(root_task_class)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def initialize(registry:, worker_count: nil)
|
|
33
|
+
@registry = registry
|
|
34
|
+
@worker_count = worker_count || default_worker_count
|
|
35
|
+
@execution_queue = Queue.new
|
|
36
|
+
@completion_queue = Queue.new
|
|
37
|
+
@workers = []
|
|
38
|
+
|
|
39
|
+
# State managed by main thread only
|
|
40
|
+
@dependencies = {}
|
|
41
|
+
@task_states = {}
|
|
42
|
+
@completed_tasks = Set.new
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Execute root task and all dependencies
|
|
46
|
+
# @param root_task_class [Class] The root task class to execute
|
|
47
|
+
def execute(root_task_class)
|
|
48
|
+
# Build dependency graph from static analysis
|
|
49
|
+
build_dependency_graph(root_task_class)
|
|
50
|
+
|
|
51
|
+
# Set up tree progress display with root task (before start)
|
|
52
|
+
setup_tree_progress(root_task_class)
|
|
53
|
+
|
|
54
|
+
# Start progress display automatically for tree progress
|
|
55
|
+
start_progress_display
|
|
56
|
+
|
|
57
|
+
# Start worker threads
|
|
58
|
+
start_workers
|
|
59
|
+
|
|
60
|
+
# Enqueue tasks with no dependencies
|
|
61
|
+
enqueue_ready_tasks
|
|
62
|
+
|
|
63
|
+
# Main event loop - continues until root task completes
|
|
64
|
+
run_main_loop(root_task_class)
|
|
65
|
+
|
|
66
|
+
# Shutdown workers
|
|
67
|
+
shutdown_workers
|
|
68
|
+
|
|
69
|
+
# Stop progress display
|
|
70
|
+
stop_progress_display
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def default_worker_count
|
|
76
|
+
Etc.nprocessors.clamp(2, 8)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Build dependency graph by traversing from root task
|
|
80
|
+
# Populates @dependencies and @task_states
|
|
81
|
+
def build_dependency_graph(root_task_class)
|
|
82
|
+
# @type var queue: Array[singleton(Taski::Task)]
|
|
83
|
+
queue = [root_task_class]
|
|
84
|
+
|
|
85
|
+
while (task_class = queue.shift)
|
|
86
|
+
next if @task_states.key?(task_class)
|
|
87
|
+
|
|
88
|
+
deps = task_class.cached_dependencies
|
|
89
|
+
@dependencies[task_class] = deps.dup
|
|
90
|
+
@task_states[task_class] = STATE_PENDING
|
|
91
|
+
|
|
92
|
+
deps.each { |dep| queue << dep }
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Enqueue tasks that have all dependencies completed
|
|
97
|
+
def enqueue_ready_tasks
|
|
98
|
+
@task_states.each_key do |task_class|
|
|
99
|
+
next unless @task_states[task_class] == STATE_PENDING
|
|
100
|
+
next unless ready_to_execute?(task_class)
|
|
101
|
+
|
|
102
|
+
enqueue_task(task_class)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Check if a task is ready to execute
|
|
107
|
+
def ready_to_execute?(task_class)
|
|
108
|
+
task_deps = @dependencies[task_class] || Set.new
|
|
109
|
+
task_deps.subset?(@completed_tasks)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Enqueue a single task for execution
|
|
113
|
+
def enqueue_task(task_class)
|
|
114
|
+
return if @registry.abort_requested?
|
|
115
|
+
|
|
116
|
+
@task_states[task_class] = STATE_ENQUEUED
|
|
117
|
+
|
|
118
|
+
wrapper = get_or_create_wrapper(task_class)
|
|
119
|
+
return unless wrapper.mark_running
|
|
120
|
+
|
|
121
|
+
Taski.progress_display&.register_task(task_class)
|
|
122
|
+
Taski.progress_display&.update_task(task_class, state: :running)
|
|
123
|
+
|
|
124
|
+
@execution_queue.push({task_class: task_class, wrapper: wrapper})
|
|
125
|
+
|
|
126
|
+
debug_log("Enqueued: #{task_class}")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Get or create a task wrapper via Registry
|
|
130
|
+
def get_or_create_wrapper(task_class)
|
|
131
|
+
@registry.get_or_create(task_class) do
|
|
132
|
+
task_instance = task_class.allocate
|
|
133
|
+
task_instance.send(:initialize)
|
|
134
|
+
TaskWrapper.new(task_instance, registry: @registry)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Start worker threads
|
|
139
|
+
def start_workers
|
|
140
|
+
@worker_count.times do
|
|
141
|
+
worker = Thread.new { worker_loop }
|
|
142
|
+
@workers << worker
|
|
143
|
+
@registry.register_thread(worker)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Worker thread main loop
|
|
148
|
+
def worker_loop
|
|
149
|
+
loop do
|
|
150
|
+
work_item = @execution_queue.pop
|
|
151
|
+
break if work_item == :shutdown
|
|
152
|
+
|
|
153
|
+
task_class = work_item[:task_class]
|
|
154
|
+
wrapper = work_item[:wrapper]
|
|
155
|
+
|
|
156
|
+
debug_log("Worker executing: #{task_class}")
|
|
157
|
+
|
|
158
|
+
execute_task(task_class, wrapper)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Execute a task and send completion event
|
|
163
|
+
def execute_task(task_class, wrapper)
|
|
164
|
+
return if @registry.abort_requested?
|
|
165
|
+
|
|
166
|
+
begin
|
|
167
|
+
result = execute_task_run(wrapper)
|
|
168
|
+
wrapper.mark_completed(result)
|
|
169
|
+
@completion_queue.push({task_class: task_class, wrapper: wrapper})
|
|
170
|
+
rescue Taski::TaskAbortException => e
|
|
171
|
+
@registry.request_abort!
|
|
172
|
+
wrapper.mark_failed(e)
|
|
173
|
+
@completion_queue.push({task_class: task_class, wrapper: wrapper, error: e})
|
|
174
|
+
rescue => e
|
|
175
|
+
wrapper.mark_failed(e)
|
|
176
|
+
@completion_queue.push({task_class: task_class, wrapper: wrapper, error: e})
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Execute task run method
|
|
181
|
+
# Note: Previously captured stdout for progress display, but this was removed
|
|
182
|
+
# due to thread-safety concerns with global $stdout mutation.
|
|
183
|
+
def execute_task_run(wrapper)
|
|
184
|
+
wrapper.task.run
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Main thread event loop - continues until root task completes
|
|
188
|
+
def run_main_loop(root_task_class)
|
|
189
|
+
until @completed_tasks.include?(root_task_class)
|
|
190
|
+
break if @registry.abort_requested? && no_running_tasks?
|
|
191
|
+
|
|
192
|
+
event = @completion_queue.pop
|
|
193
|
+
handle_completion(event)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def no_running_tasks?
|
|
198
|
+
@task_states.values.none? { |state| state == STATE_ENQUEUED }
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Handle task completion event
|
|
202
|
+
def handle_completion(event)
|
|
203
|
+
task_class = event[:task_class]
|
|
204
|
+
|
|
205
|
+
debug_log("Completed: #{task_class}")
|
|
206
|
+
|
|
207
|
+
@task_states[task_class] = STATE_COMPLETED
|
|
208
|
+
@completed_tasks.add(task_class)
|
|
209
|
+
|
|
210
|
+
# Enqueue newly ready tasks
|
|
211
|
+
enqueue_ready_tasks
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Shutdown worker threads
|
|
215
|
+
def shutdown_workers
|
|
216
|
+
@worker_count.times { @execution_queue.push(:shutdown) }
|
|
217
|
+
@workers.each(&:join)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def setup_tree_progress(root_task_class)
|
|
221
|
+
progress = Taski.progress_display
|
|
222
|
+
return unless progress.is_a?(TreeProgressDisplay)
|
|
223
|
+
|
|
224
|
+
progress.set_root_task(root_task_class)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def start_progress_display
|
|
228
|
+
progress = Taski.progress_display
|
|
229
|
+
return unless progress.is_a?(TreeProgressDisplay)
|
|
230
|
+
|
|
231
|
+
progress.start
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def stop_progress_display
|
|
235
|
+
progress = Taski.progress_display
|
|
236
|
+
return unless progress.is_a?(TreeProgressDisplay)
|
|
237
|
+
|
|
238
|
+
progress.stop
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def debug_log(message)
|
|
242
|
+
return unless ENV["TASKI_DEBUG"]
|
|
243
|
+
puts "[Executor] #{message}"
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
@@ -19,6 +19,12 @@ module Taski
|
|
|
19
19
|
@tasks[task_class] ||= yield
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
# @param task_class [Class] The task class
|
|
23
|
+
# @param wrapper [TaskWrapper] The wrapper instance to register
|
|
24
|
+
def register(task_class, wrapper)
|
|
25
|
+
@tasks[task_class] = wrapper
|
|
26
|
+
end
|
|
27
|
+
|
|
22
28
|
# @param task_class [Class] The task class
|
|
23
29
|
# @return [Object] The task instance
|
|
24
30
|
# @raise [RuntimeError] If the task is not registered
|
|
@@ -65,7 +71,9 @@ module Taski
|
|
|
65
71
|
|
|
66
72
|
wait_all
|
|
67
73
|
|
|
68
|
-
|
|
74
|
+
# @type var wrapper: Taski::Execution::TaskWrapper
|
|
75
|
+
wrapper = get_task(task_class)
|
|
76
|
+
wrapper.result
|
|
69
77
|
end
|
|
70
78
|
end
|
|
71
79
|
end
|
|
@@ -22,17 +22,19 @@ module Taski
|
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
# TaskWrapper manages the state and synchronization for a single task.
|
|
26
|
+
# In the Producer-Consumer pattern, TaskWrapper does NOT start threads.
|
|
27
|
+
# The Executor controls all scheduling and execution.
|
|
25
28
|
class TaskWrapper
|
|
26
|
-
attr_reader :task, :result
|
|
29
|
+
attr_reader :task, :result, :error, :timing
|
|
27
30
|
|
|
28
31
|
STATE_PENDING = :pending
|
|
29
32
|
STATE_RUNNING = :running
|
|
30
33
|
STATE_COMPLETED = :completed
|
|
31
34
|
|
|
32
|
-
def initialize(task, registry
|
|
35
|
+
def initialize(task, registry:)
|
|
33
36
|
@task = task
|
|
34
37
|
@registry = registry
|
|
35
|
-
@coordinator = coordinator
|
|
36
38
|
@result = nil
|
|
37
39
|
@clean_result = nil
|
|
38
40
|
@error = nil
|
|
@@ -42,213 +44,190 @@ module Taski
|
|
|
42
44
|
@state = STATE_PENDING
|
|
43
45
|
@clean_state = STATE_PENDING
|
|
44
46
|
@timing = nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [Symbol] Current state
|
|
50
|
+
def state
|
|
51
|
+
@monitor.synchronize { @state }
|
|
52
|
+
end
|
|
45
53
|
|
|
46
|
-
|
|
54
|
+
# @return [Boolean] true if task is pending
|
|
55
|
+
def pending?
|
|
56
|
+
state == STATE_PENDING
|
|
47
57
|
end
|
|
48
58
|
|
|
59
|
+
# @return [Boolean] true if task is completed
|
|
60
|
+
def completed?
|
|
61
|
+
state == STATE_COMPLETED
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Called by user code to get result. Triggers execution if needed.
|
|
49
65
|
# @return [Object] The result of task execution
|
|
50
66
|
def run
|
|
51
|
-
|
|
67
|
+
trigger_execution_and_wait
|
|
52
68
|
raise @error if @error # steep:ignore
|
|
53
69
|
@result
|
|
54
70
|
end
|
|
55
71
|
|
|
72
|
+
# Called by user code to clean. Triggers clean execution if needed.
|
|
56
73
|
# @return [Object] The result of cleanup
|
|
57
74
|
def clean
|
|
58
|
-
|
|
75
|
+
trigger_clean_and_wait
|
|
59
76
|
@clean_result
|
|
60
77
|
end
|
|
61
78
|
|
|
79
|
+
# Called by user code to get exported value. Triggers execution if needed.
|
|
62
80
|
# @param method_name [Symbol] The name of the exported method
|
|
63
81
|
# @return [Object] The exported value
|
|
64
82
|
def get_exported_value(method_name)
|
|
65
|
-
|
|
83
|
+
trigger_execution_and_wait
|
|
66
84
|
raise @error if @error # steep:ignore
|
|
67
85
|
@task.public_send(method_name)
|
|
68
86
|
end
|
|
69
87
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
def start_thread_with(&block)
|
|
73
|
-
thread = Thread.new(&block)
|
|
74
|
-
@registry.register_thread(thread)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# Thread-safe state machine that ensures operations are executed exactly once.
|
|
78
|
-
# Uses pattern matching for exhaustive state handling.
|
|
79
|
-
def execute_with_state_pattern(state_getter:, starter:, waiter:, pre_start_check: nil)
|
|
88
|
+
# Called by Executor to mark task as running
|
|
89
|
+
def mark_running
|
|
80
90
|
@monitor.synchronize do
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
waiter.call
|
|
86
|
-
in STATE_RUNNING
|
|
87
|
-
waiter.call
|
|
88
|
-
in STATE_COMPLETED
|
|
89
|
-
return
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def execute_task_if_needed
|
|
95
|
-
execute_with_state_pattern(
|
|
96
|
-
state_getter: -> { @state },
|
|
97
|
-
starter: -> { start_async_execution },
|
|
98
|
-
waiter: -> { wait_for_completion },
|
|
99
|
-
pre_start_check: -> {
|
|
100
|
-
if @registry.abort_requested?
|
|
101
|
-
raise Taski::TaskAbortException, "Execution aborted - no new tasks will start"
|
|
102
|
-
end
|
|
103
|
-
}
|
|
104
|
-
)
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def start_async_execution
|
|
108
|
-
@state = STATE_RUNNING
|
|
109
|
-
@timing = TaskTiming.start_now
|
|
110
|
-
update_progress(:running)
|
|
111
|
-
start_thread_with { execute_task }
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def execute_task
|
|
115
|
-
if @registry.abort_requested?
|
|
116
|
-
@error = Taski::TaskAbortException.new("Execution aborted - no new tasks will start")
|
|
117
|
-
mark_completed
|
|
118
|
-
return
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
log_start
|
|
122
|
-
@coordinator.start_dependencies(@task.class)
|
|
123
|
-
wait_for_dependencies
|
|
124
|
-
@result = @task.run
|
|
125
|
-
mark_completed
|
|
126
|
-
log_completion
|
|
127
|
-
rescue Taski::TaskAbortException => e
|
|
128
|
-
@registry.request_abort!
|
|
129
|
-
@error = e
|
|
130
|
-
mark_completed
|
|
131
|
-
rescue => e
|
|
132
|
-
@error = e
|
|
133
|
-
mark_completed
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def wait_for_dependencies
|
|
137
|
-
dependencies = @task.class.cached_dependencies
|
|
138
|
-
return if dependencies.empty?
|
|
139
|
-
|
|
140
|
-
dependencies.each do |dep_class|
|
|
141
|
-
dep_class.exported_methods.each do |method|
|
|
142
|
-
dep_class.public_send(method)
|
|
143
|
-
end
|
|
91
|
+
return false unless @state == STATE_PENDING
|
|
92
|
+
@state = STATE_RUNNING
|
|
93
|
+
@timing = TaskTiming.start_now
|
|
94
|
+
true
|
|
144
95
|
end
|
|
145
96
|
end
|
|
146
97
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def start_async_clean
|
|
156
|
-
@clean_state = STATE_RUNNING
|
|
157
|
-
start_thread_with { execute_clean }
|
|
158
|
-
end
|
|
159
|
-
|
|
160
|
-
def execute_clean
|
|
161
|
-
log_clean_start
|
|
162
|
-
@clean_result = @task.clean
|
|
163
|
-
wait_for_clean_dependencies
|
|
164
|
-
mark_clean_completed
|
|
165
|
-
log_clean_completion
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def wait_for_clean_dependencies
|
|
169
|
-
dependencies = @task.class.cached_dependencies
|
|
170
|
-
return if dependencies.empty?
|
|
171
|
-
|
|
172
|
-
wait_threads = dependencies.map do |dep_class|
|
|
173
|
-
Thread.new do
|
|
174
|
-
dep_class.public_send(:clean)
|
|
175
|
-
end
|
|
98
|
+
# Called by Executor after task.run completes successfully
|
|
99
|
+
# @param result [Object] The result of task execution
|
|
100
|
+
def mark_completed(result)
|
|
101
|
+
@timing = @timing&.with_end_now
|
|
102
|
+
@monitor.synchronize do
|
|
103
|
+
@result = result
|
|
104
|
+
@state = STATE_COMPLETED
|
|
105
|
+
@condition.broadcast
|
|
176
106
|
end
|
|
177
|
-
|
|
178
|
-
wait_threads.each(&:join)
|
|
107
|
+
update_progress(:completed, duration: @timing&.duration_ms)
|
|
179
108
|
end
|
|
180
109
|
|
|
181
|
-
|
|
110
|
+
# Called by Executor when task.run raises an error
|
|
111
|
+
# @param error [Exception] The error that occurred
|
|
112
|
+
def mark_failed(error)
|
|
182
113
|
@timing = @timing&.with_end_now
|
|
183
114
|
@monitor.synchronize do
|
|
115
|
+
@error = error
|
|
184
116
|
@state = STATE_COMPLETED
|
|
185
117
|
@condition.broadcast
|
|
186
118
|
end
|
|
187
|
-
|
|
188
|
-
if @error
|
|
189
|
-
update_progress(:failed, error: @error)
|
|
190
|
-
else
|
|
191
|
-
update_progress(:completed, duration: @timing&.duration_ms)
|
|
192
|
-
end
|
|
119
|
+
update_progress(:failed, error: error)
|
|
193
120
|
end
|
|
194
121
|
|
|
195
|
-
|
|
122
|
+
# Called by Executor after clean completes
|
|
123
|
+
# @param result [Object] The result of cleanup
|
|
124
|
+
def mark_clean_completed(result)
|
|
196
125
|
@monitor.synchronize do
|
|
126
|
+
@clean_result = result
|
|
197
127
|
@clean_state = STATE_COMPLETED
|
|
198
128
|
@clean_condition.broadcast
|
|
199
129
|
end
|
|
200
130
|
end
|
|
201
131
|
|
|
132
|
+
# Wait until task is completed
|
|
202
133
|
def wait_for_completion
|
|
203
|
-
@
|
|
134
|
+
@monitor.synchronize do
|
|
135
|
+
@condition.wait_until { @state == STATE_COMPLETED }
|
|
136
|
+
end
|
|
204
137
|
end
|
|
205
138
|
|
|
139
|
+
# Wait until clean is completed
|
|
206
140
|
def wait_for_clean_completion
|
|
207
|
-
@
|
|
141
|
+
@monitor.synchronize do
|
|
142
|
+
@clean_condition.wait_until { @clean_state == STATE_COMPLETED }
|
|
143
|
+
end
|
|
208
144
|
end
|
|
209
145
|
|
|
210
|
-
def
|
|
211
|
-
|
|
212
|
-
|
|
146
|
+
def method_missing(method_name, *args, &block)
|
|
147
|
+
if @task.class.method_defined?(method_name)
|
|
148
|
+
get_exported_value(method_name)
|
|
149
|
+
else
|
|
150
|
+
super
|
|
151
|
+
end
|
|
213
152
|
end
|
|
214
153
|
|
|
215
|
-
def
|
|
216
|
-
|
|
154
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
155
|
+
@task.class.method_defined?(method_name) || super
|
|
217
156
|
end
|
|
218
157
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
# Trigger execution via Executor and wait for completion
|
|
161
|
+
def trigger_execution_and_wait
|
|
162
|
+
should_execute = false
|
|
163
|
+
@monitor.synchronize do
|
|
164
|
+
case @state
|
|
165
|
+
when STATE_PENDING
|
|
166
|
+
check_abort!
|
|
167
|
+
should_execute = true
|
|
168
|
+
when STATE_RUNNING
|
|
169
|
+
@condition.wait_until { @state == STATE_COMPLETED }
|
|
170
|
+
when STATE_COMPLETED
|
|
171
|
+
# Already done
|
|
172
|
+
end
|
|
173
|
+
end
|
|
222
174
|
|
|
223
|
-
|
|
224
|
-
|
|
175
|
+
if should_execute
|
|
176
|
+
# Execute outside the lock to avoid deadlock
|
|
177
|
+
Executor.execute(@task.class, registry: @registry)
|
|
178
|
+
# After Executor.execute returns, the task is completed
|
|
179
|
+
end
|
|
225
180
|
end
|
|
226
181
|
|
|
227
|
-
|
|
228
|
-
|
|
182
|
+
# Trigger clean execution and wait for completion
|
|
183
|
+
def trigger_clean_and_wait
|
|
184
|
+
@monitor.synchronize do
|
|
185
|
+
case @clean_state
|
|
186
|
+
when STATE_PENDING
|
|
187
|
+
@clean_state = STATE_RUNNING
|
|
188
|
+
# Execute clean in a thread (clean doesn't use Producer-Consumer)
|
|
189
|
+
thread = Thread.new { execute_clean }
|
|
190
|
+
@registry.register_thread(thread)
|
|
191
|
+
@clean_condition.wait_until { @clean_state == STATE_COMPLETED }
|
|
192
|
+
when STATE_RUNNING
|
|
193
|
+
@clean_condition.wait_until { @clean_state == STATE_COMPLETED }
|
|
194
|
+
when STATE_COMPLETED
|
|
195
|
+
# Already done
|
|
196
|
+
end
|
|
197
|
+
end
|
|
229
198
|
end
|
|
230
199
|
|
|
231
|
-
def
|
|
232
|
-
|
|
200
|
+
def execute_clean
|
|
201
|
+
debug_log("Cleaning #{@task.class}...")
|
|
202
|
+
result = @task.clean
|
|
203
|
+
wait_for_clean_dependencies
|
|
204
|
+
mark_clean_completed(result)
|
|
205
|
+
debug_log("Clean #{@task.class} completed.")
|
|
233
206
|
end
|
|
234
207
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
208
|
+
def wait_for_clean_dependencies
|
|
209
|
+
dependencies = @task.class.cached_dependencies
|
|
210
|
+
return if dependencies.empty?
|
|
211
|
+
|
|
212
|
+
wait_threads = dependencies.map do |dep_class|
|
|
213
|
+
Thread.new { dep_class.clean }
|
|
214
|
+
end
|
|
215
|
+
wait_threads.each(&:join)
|
|
240
216
|
end
|
|
241
217
|
|
|
242
|
-
def
|
|
243
|
-
if @
|
|
244
|
-
|
|
245
|
-
else
|
|
246
|
-
super
|
|
218
|
+
def check_abort!
|
|
219
|
+
if @registry.abort_requested?
|
|
220
|
+
raise Taski::TaskAbortException, "Execution aborted - no new tasks will start"
|
|
247
221
|
end
|
|
248
222
|
end
|
|
249
223
|
|
|
250
|
-
def
|
|
251
|
-
@task.class
|
|
224
|
+
def update_progress(state, duration: nil, error: nil)
|
|
225
|
+
Taski.progress_display&.update_task(@task.class, state: state, duration: duration, error: error)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def debug_log(message)
|
|
229
|
+
return unless ENV["TASKI_DEBUG"]
|
|
230
|
+
puts "[TaskWrapper] #{message}"
|
|
252
231
|
end
|
|
253
232
|
end
|
|
254
233
|
end
|