taski 0.5.0 → 0.7.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/CHANGELOG.md +50 -0
- data/README.md +40 -21
- data/docs/GUIDE.md +340 -0
- data/examples/README.md +65 -17
- data/examples/{context_demo.rb → args_demo.rb} +27 -27
- data/examples/clean_demo.rb +204 -0
- data/examples/group_demo.rb +113 -0
- data/examples/reexecution_demo.rb +93 -80
- data/examples/system_call_demo.rb +56 -0
- data/lib/taski/{context.rb → args.rb} +3 -3
- data/lib/taski/execution/execution_context.rb +379 -0
- data/lib/taski/execution/executor.rb +408 -117
- data/lib/taski/execution/registry.rb +17 -1
- data/lib/taski/execution/scheduler.rb +308 -0
- data/lib/taski/execution/task_output_pipe.rb +42 -0
- data/lib/taski/execution/task_output_router.rb +216 -0
- data/lib/taski/execution/task_wrapper.rb +210 -40
- data/lib/taski/execution/tree_progress_display.rb +385 -98
- data/lib/taski/execution/worker_pool.rb +104 -0
- data/lib/taski/section.rb +16 -3
- data/lib/taski/task.rb +222 -36
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +138 -23
- data/sig/taski.rbs +207 -27
- metadata +13 -7
- data/docs/advanced-features.md +0 -625
- data/docs/api-guide.md +0 -509
- data/docs/error-handling.md +0 -684
- data/examples/section_progress_demo.rb +0 -78
|
@@ -1,61 +1,123 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "monitor"
|
|
4
|
-
require "etc"
|
|
5
4
|
|
|
6
5
|
module Taski
|
|
7
6
|
module Execution
|
|
8
7
|
# Producer-Consumer pattern executor for parallel task execution.
|
|
9
8
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
9
|
+
# Executor is the orchestrator that coordinates all execution components.
|
|
10
|
+
#
|
|
11
|
+
# == Architecture
|
|
12
|
+
#
|
|
13
|
+
# Executor
|
|
14
|
+
# ├── Scheduler: Dependency management and execution order
|
|
15
|
+
# ├── WorkerPool: Thread management and task distribution
|
|
16
|
+
# └── ExecutionContext: Observer notifications and output capture
|
|
17
|
+
# └── Observers (e.g., TreeProgressDisplay)
|
|
18
|
+
#
|
|
19
|
+
# == Execution Flow
|
|
13
20
|
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
21
|
+
# 1. Build dependency graph via Scheduler
|
|
22
|
+
# 2. Set up progress display via ExecutionContext
|
|
23
|
+
# 3. Start WorkerPool threads
|
|
24
|
+
# 4. Enqueue ready tasks (no dependencies) to WorkerPool
|
|
25
|
+
# 5. Run event loop:
|
|
26
|
+
# - Pop completion events from workers
|
|
27
|
+
# - Mark completed in Scheduler
|
|
28
|
+
# - Enqueue newly ready tasks to WorkerPool
|
|
29
|
+
# 6. Shutdown WorkerPool when root task completes
|
|
30
|
+
# 7. Teardown progress display
|
|
31
|
+
#
|
|
32
|
+
# == Communication Queues
|
|
33
|
+
#
|
|
34
|
+
# - Execution Queue (Main -> Worker): Tasks ready to execute (via WorkerPool)
|
|
16
35
|
# - Completion Queue (Worker -> Main): Events from workers
|
|
36
|
+
#
|
|
37
|
+
# == Thread Safety
|
|
38
|
+
#
|
|
39
|
+
# - Main Thread: Manages all state, coordinates execution, handles events
|
|
40
|
+
# - Worker Threads: Execute tasks and send completion events (via WorkerPool)
|
|
17
41
|
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
42
|
class << self
|
|
24
43
|
# Execute a task and all its dependencies
|
|
25
44
|
# @param root_task_class [Class] The root task class to execute
|
|
26
45
|
# @param registry [Registry] The task registry
|
|
27
|
-
|
|
28
|
-
|
|
46
|
+
##
|
|
47
|
+
# Create a new Executor and run execution for the specified root task class.
|
|
48
|
+
# @param root_task_class [Class] The top-level task class to execute.
|
|
49
|
+
# @param registry [Taski::Registry] Registry providing task definitions and state.
|
|
50
|
+
# @param execution_context [ExecutionContext, nil] Optional execution context to use; when nil a default context is created.
|
|
51
|
+
# @return [Object] The result returned by the execution of the root task.
|
|
52
|
+
def execute(root_task_class, registry:, execution_context: nil)
|
|
53
|
+
new(registry: registry, execution_context: execution_context).execute(root_task_class)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Execute clean for a task and all its dependencies (in reverse order)
|
|
57
|
+
# @param root_task_class [Class] The root task class to clean
|
|
58
|
+
# @param registry [Registry] The task registry
|
|
59
|
+
##
|
|
60
|
+
# Runs reverse-order clean execution beginning at the given root task class.
|
|
61
|
+
# @param [Class] root_task_class - The root task class whose dependency graph will drive the clean run.
|
|
62
|
+
# @param [Object] registry - Task registry used to resolve and track tasks during execution.
|
|
63
|
+
# @param [ExecutionContext, nil] execution_context - Optional execution context for observers and output capture; if `nil`, a default context is created.
|
|
64
|
+
def execute_clean(root_task_class, registry:, execution_context: nil)
|
|
65
|
+
new(registry: registry, execution_context: execution_context).execute_clean(root_task_class)
|
|
29
66
|
end
|
|
30
67
|
end
|
|
31
68
|
|
|
32
|
-
|
|
69
|
+
##
|
|
70
|
+
# Initialize an Executor and its internal coordination components.
|
|
71
|
+
# @param [Object] registry - Task registry used to look up task definitions and state.
|
|
72
|
+
# @param [Integer, nil] worker_count - Optional number of worker threads to use; when `nil`,
|
|
73
|
+
# uses Taski.args_worker_count which retrieves the worker count from the runtime args.
|
|
74
|
+
# @param [Taski::Execution::ExecutionContext, nil] execution_context - Optional execution context for observers and output capture; when `nil` a default context (with progress observer and execution trigger) is created.
|
|
75
|
+
def initialize(registry:, worker_count: nil, execution_context: nil)
|
|
33
76
|
@registry = registry
|
|
34
|
-
@worker_count = worker_count || default_worker_count
|
|
35
|
-
@execution_queue = Queue.new
|
|
36
77
|
@completion_queue = Queue.new
|
|
37
|
-
@workers = []
|
|
38
78
|
|
|
39
|
-
#
|
|
40
|
-
@
|
|
41
|
-
|
|
42
|
-
|
|
79
|
+
# ExecutionContext for observer pattern and output capture
|
|
80
|
+
@execution_context = execution_context || create_default_execution_context
|
|
81
|
+
|
|
82
|
+
# Scheduler for dependency management
|
|
83
|
+
@scheduler = Scheduler.new
|
|
84
|
+
|
|
85
|
+
# Determine effective worker count: explicit param > args > default
|
|
86
|
+
# Store as instance variable for consistent use in both run and clean phases
|
|
87
|
+
@effective_worker_count = worker_count || Taski.args_worker_count
|
|
88
|
+
|
|
89
|
+
# WorkerPool for thread management
|
|
90
|
+
@worker_pool = WorkerPool.new(
|
|
91
|
+
registry: @registry,
|
|
92
|
+
worker_count: @effective_worker_count
|
|
93
|
+
) { |task_class, wrapper| execute_task(task_class, wrapper) }
|
|
43
94
|
end
|
|
44
95
|
|
|
45
96
|
# Execute root task and all dependencies
|
|
46
|
-
|
|
97
|
+
##
|
|
98
|
+
# Execute the task graph rooted at the given task class.
|
|
99
|
+
#
|
|
100
|
+
# Builds the dependency graph, starts progress reporting and worker threads,
|
|
101
|
+
# enqueues tasks that are ready (no unmet dependencies), and processes worker
|
|
102
|
+
# completion events until the root task finishes. After completion or abort,
|
|
103
|
+
# shuts down workers, stops progress reporting, and restores stdout capture if
|
|
104
|
+
# this executor configured it.
|
|
105
|
+
# @param root_task_class [Class] The root task class to execute.
|
|
47
106
|
def execute(root_task_class)
|
|
48
107
|
# Build dependency graph from static analysis
|
|
49
|
-
build_dependency_graph(root_task_class)
|
|
108
|
+
@scheduler.build_dependency_graph(root_task_class)
|
|
109
|
+
|
|
110
|
+
# Set up progress display with root task
|
|
111
|
+
setup_progress_display(root_task_class)
|
|
50
112
|
|
|
51
|
-
# Set up
|
|
52
|
-
|
|
113
|
+
# Set up output capture (returns true if this executor set it up)
|
|
114
|
+
should_teardown_capture = setup_output_capture_if_needed
|
|
53
115
|
|
|
54
|
-
# Start progress display
|
|
116
|
+
# Start progress display
|
|
55
117
|
start_progress_display
|
|
56
118
|
|
|
57
119
|
# Start worker threads
|
|
58
|
-
|
|
120
|
+
@worker_pool.start
|
|
59
121
|
|
|
60
122
|
# Enqueue tasks with no dependencies
|
|
61
123
|
enqueue_ready_tasks
|
|
@@ -64,66 +126,96 @@ module Taski
|
|
|
64
126
|
run_main_loop(root_task_class)
|
|
65
127
|
|
|
66
128
|
# Shutdown workers
|
|
67
|
-
|
|
129
|
+
@worker_pool.shutdown
|
|
68
130
|
|
|
69
131
|
# Stop progress display
|
|
70
132
|
stop_progress_display
|
|
71
|
-
end
|
|
72
133
|
|
|
73
|
-
|
|
134
|
+
# Restore original stdout (only if this executor set it up)
|
|
135
|
+
teardown_output_capture if should_teardown_capture
|
|
74
136
|
|
|
75
|
-
|
|
76
|
-
|
|
137
|
+
# Raise aggregated errors if any tasks failed
|
|
138
|
+
raise_if_any_failures
|
|
77
139
|
end
|
|
78
140
|
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
141
|
+
# Execute clean for root task and all dependencies (in reverse dependency order)
|
|
142
|
+
# Clean operations run in reverse: root task cleans first, then dependencies
|
|
143
|
+
##
|
|
144
|
+
# Executes the clean workflow for the given root task in reverse dependency order.
|
|
145
|
+
# Sets up progress display and optional output capture, starts a dedicated clean worker pool,
|
|
146
|
+
# enqueues ready-to-clean tasks, processes completion events until all tasks are cleaned,
|
|
147
|
+
# then shuts down workers and tears down progress and output capture as needed.
|
|
148
|
+
# @param [Class] root_task_class - The root task class to clean
|
|
149
|
+
def execute_clean(root_task_class)
|
|
150
|
+
# Build reverse dependency graph for clean order
|
|
151
|
+
# This must happen first to ensure root task and all static dependencies are included
|
|
152
|
+
@scheduler.build_reverse_dependency_graph(root_task_class)
|
|
153
|
+
|
|
154
|
+
# Merge runtime dependencies (e.g., Section's dynamically selected implementations)
|
|
155
|
+
# This allows clean to include tasks that were selected at runtime during run phase
|
|
156
|
+
runtime_deps = @execution_context.runtime_dependencies
|
|
157
|
+
@scheduler.merge_runtime_dependencies(runtime_deps)
|
|
158
|
+
|
|
159
|
+
# Set up progress display with root task (if not already set)
|
|
160
|
+
setup_progress_display(root_task_class)
|
|
161
|
+
|
|
162
|
+
# Set up output capture (returns true if this executor set it up)
|
|
163
|
+
should_teardown_capture = setup_output_capture_if_needed
|
|
164
|
+
|
|
165
|
+
# Start progress display
|
|
166
|
+
start_progress_display
|
|
167
|
+
|
|
168
|
+
# Create a new worker pool for clean operations
|
|
169
|
+
# Uses the same worker count as the run phase
|
|
170
|
+
@clean_worker_pool = WorkerPool.new(
|
|
171
|
+
registry: @registry,
|
|
172
|
+
worker_count: @effective_worker_count
|
|
173
|
+
) { |task_class, wrapper| execute_clean_task(task_class, wrapper) }
|
|
84
174
|
|
|
85
|
-
|
|
86
|
-
|
|
175
|
+
# Start worker threads
|
|
176
|
+
@clean_worker_pool.start
|
|
87
177
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
@task_states[task_class] = STATE_PENDING
|
|
178
|
+
# Enqueue tasks ready for clean (no reverse dependencies)
|
|
179
|
+
enqueue_ready_clean_tasks
|
|
91
180
|
|
|
92
|
-
|
|
93
|
-
|
|
181
|
+
# Main event loop - continues until all tasks are cleaned
|
|
182
|
+
run_clean_main_loop(root_task_class)
|
|
183
|
+
|
|
184
|
+
# Shutdown workers
|
|
185
|
+
@clean_worker_pool.shutdown
|
|
186
|
+
|
|
187
|
+
# Stop progress display
|
|
188
|
+
stop_progress_display
|
|
189
|
+
|
|
190
|
+
# Restore original stdout (only if this executor set it up)
|
|
191
|
+
teardown_output_capture if should_teardown_capture
|
|
192
|
+
|
|
193
|
+
# Raise aggregated errors if any clean tasks failed
|
|
194
|
+
raise_if_any_clean_failures
|
|
94
195
|
end
|
|
95
196
|
|
|
96
|
-
|
|
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)
|
|
197
|
+
private
|
|
101
198
|
|
|
199
|
+
# Enqueue all tasks that are ready to execute
|
|
200
|
+
def enqueue_ready_tasks
|
|
201
|
+
@scheduler.next_ready_tasks.each do |task_class|
|
|
102
202
|
enqueue_task(task_class)
|
|
103
203
|
end
|
|
104
204
|
end
|
|
105
205
|
|
|
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
206
|
# Enqueue a single task for execution
|
|
113
207
|
def enqueue_task(task_class)
|
|
114
208
|
return if @registry.abort_requested?
|
|
115
209
|
|
|
116
|
-
@
|
|
210
|
+
@scheduler.mark_enqueued(task_class)
|
|
117
211
|
|
|
118
212
|
wrapper = get_or_create_wrapper(task_class)
|
|
119
213
|
return unless wrapper.mark_running
|
|
120
214
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
@execution_queue.push({task_class: task_class, wrapper: wrapper})
|
|
215
|
+
@execution_context.notify_task_registered(task_class)
|
|
216
|
+
@execution_context.notify_task_started(task_class)
|
|
125
217
|
|
|
126
|
-
|
|
218
|
+
@worker_pool.enqueue(task_class, wrapper)
|
|
127
219
|
end
|
|
128
220
|
|
|
129
221
|
# Get or create a task wrapper via Registry
|
|
@@ -131,40 +223,26 @@ module Taski
|
|
|
131
223
|
@registry.get_or_create(task_class) do
|
|
132
224
|
task_instance = task_class.allocate
|
|
133
225
|
task_instance.send(:initialize)
|
|
134
|
-
TaskWrapper.new(task_instance, registry: @registry)
|
|
226
|
+
TaskWrapper.new(task_instance, registry: @registry, execution_context: @execution_context)
|
|
135
227
|
end
|
|
136
228
|
end
|
|
137
229
|
|
|
138
|
-
#
|
|
139
|
-
def
|
|
140
|
-
@
|
|
141
|
-
worker = Thread.new { worker_loop }
|
|
142
|
-
@workers << worker
|
|
143
|
-
@registry.register_thread(worker)
|
|
144
|
-
end
|
|
145
|
-
end
|
|
230
|
+
# Execute a task and send completion event (called by WorkerPool)
|
|
231
|
+
def execute_task(task_class, wrapper)
|
|
232
|
+
return if @registry.abort_requested?
|
|
146
233
|
|
|
147
|
-
|
|
148
|
-
def worker_loop
|
|
149
|
-
loop do
|
|
150
|
-
work_item = @execution_queue.pop
|
|
151
|
-
break if work_item == :shutdown
|
|
234
|
+
output_capture = @execution_context.output_capture
|
|
152
235
|
|
|
153
|
-
|
|
154
|
-
|
|
236
|
+
# Start capturing output for this task
|
|
237
|
+
output_capture&.start_capture(task_class)
|
|
155
238
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# Execute a task and send completion event
|
|
163
|
-
def execute_task(task_class, wrapper)
|
|
164
|
-
return if @registry.abort_requested?
|
|
239
|
+
# Set thread-local execution context for task access (e.g., Section)
|
|
240
|
+
ExecutionContext.current = @execution_context
|
|
241
|
+
# Set thread-local registry for dependency resolution
|
|
242
|
+
Taski.set_current_registry(@registry)
|
|
165
243
|
|
|
166
244
|
begin
|
|
167
|
-
result =
|
|
245
|
+
result = wrapper.task.run
|
|
168
246
|
wrapper.mark_completed(result)
|
|
169
247
|
@completion_queue.push({task_class: task_class, wrapper: wrapper})
|
|
170
248
|
rescue Taski::TaskAbortException => e
|
|
@@ -174,74 +252,287 @@ module Taski
|
|
|
174
252
|
rescue => e
|
|
175
253
|
wrapper.mark_failed(e)
|
|
176
254
|
@completion_queue.push({task_class: task_class, wrapper: wrapper, error: e})
|
|
255
|
+
ensure
|
|
256
|
+
# Stop capturing output for this task
|
|
257
|
+
output_capture&.stop_capture
|
|
258
|
+
# Clear thread-local execution context
|
|
259
|
+
ExecutionContext.current = nil
|
|
260
|
+
# Clear thread-local registry
|
|
261
|
+
Taski.clear_current_registry
|
|
177
262
|
end
|
|
178
263
|
end
|
|
179
264
|
|
|
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
265
|
# Main thread event loop - continues until root task completes
|
|
188
266
|
def run_main_loop(root_task_class)
|
|
189
|
-
until @
|
|
190
|
-
break if @registry.abort_requested? &&
|
|
267
|
+
until @scheduler.completed?(root_task_class)
|
|
268
|
+
break if @registry.abort_requested? && !@scheduler.running_tasks?
|
|
191
269
|
|
|
192
270
|
event = @completion_queue.pop
|
|
193
271
|
handle_completion(event)
|
|
194
272
|
end
|
|
195
273
|
end
|
|
196
274
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
# Handle task completion event
|
|
275
|
+
##
|
|
276
|
+
# Marks the given task as completed in the scheduler and enqueues any tasks that become ready as a result.
|
|
277
|
+
# @param [Hash] event - Completion event containing information about the finished task.
|
|
278
|
+
# @param [Class] event[:task_class] - The task class that completed.
|
|
202
279
|
def handle_completion(event)
|
|
203
280
|
task_class = event[:task_class]
|
|
204
281
|
|
|
205
282
|
debug_log("Completed: #{task_class}")
|
|
206
283
|
|
|
207
|
-
@
|
|
208
|
-
@completed_tasks.add(task_class)
|
|
284
|
+
@scheduler.mark_completed(task_class)
|
|
209
285
|
|
|
210
286
|
# Enqueue newly ready tasks
|
|
211
287
|
enqueue_ready_tasks
|
|
212
288
|
end
|
|
213
289
|
|
|
214
|
-
#
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
290
|
+
# ========================================
|
|
291
|
+
# Clean Execution Methods
|
|
292
|
+
# ========================================
|
|
293
|
+
|
|
294
|
+
##
|
|
295
|
+
# Enqueues all tasks that are currently ready to be cleaned.
|
|
296
|
+
def enqueue_ready_clean_tasks
|
|
297
|
+
@scheduler.next_ready_clean_tasks.each do |task_class|
|
|
298
|
+
enqueue_clean_task(task_class)
|
|
299
|
+
end
|
|
218
300
|
end
|
|
219
301
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
302
|
+
##
|
|
303
|
+
# Enqueues a single task for reverse-order (clean) execution.
|
|
304
|
+
# If execution has been aborted, does nothing. Marks the task as clean-enqueued,
|
|
305
|
+
# skips if the task is not registered or not eligible to run, notifies the
|
|
306
|
+
# execution context that cleaning has started, and schedules the task on the
|
|
307
|
+
# clean worker pool.
|
|
308
|
+
# @param [Class] task_class - The task class to enqueue for clean execution.
|
|
309
|
+
def enqueue_clean_task(task_class)
|
|
310
|
+
return if @registry.abort_requested?
|
|
223
311
|
|
|
224
|
-
|
|
312
|
+
@scheduler.mark_clean_enqueued(task_class)
|
|
313
|
+
|
|
314
|
+
wrapper = get_or_create_wrapper(task_class)
|
|
315
|
+
return unless wrapper.mark_clean_running
|
|
316
|
+
|
|
317
|
+
@execution_context.notify_clean_started(task_class)
|
|
318
|
+
|
|
319
|
+
@clean_worker_pool.enqueue(task_class, wrapper)
|
|
225
320
|
end
|
|
226
321
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
322
|
+
##
|
|
323
|
+
# Executes the clean lifecycle for a task and emits a completion event.
|
|
324
|
+
#
|
|
325
|
+
# Runs the task's `clean` method, updates the provided wrapper with success or failure
|
|
326
|
+
# (which handles timing and observer notification), and pushes a completion event onto
|
|
327
|
+
# the executor's completion queue.
|
|
328
|
+
# This method respects an abort requested state from the registry (no-op if abort already requested)
|
|
329
|
+
# and triggers a registry abort when a `Taski::TaskAbortException` is raised.
|
|
330
|
+
# It also starts and stops per-task output capture when available and sets the thread-local
|
|
331
|
+
# `ExecutionContext.current` for the duration of the clean.
|
|
332
|
+
# @param [Class] task_class - The task class being cleaned.
|
|
333
|
+
# @param [Taski::Execution::TaskWrapper] wrapper - The wrapper instance for the task, used to record clean success or failure.
|
|
334
|
+
def execute_clean_task(task_class, wrapper)
|
|
335
|
+
return if @registry.abort_requested?
|
|
336
|
+
|
|
337
|
+
output_capture = @execution_context.output_capture
|
|
338
|
+
|
|
339
|
+
# Start capturing output for this task
|
|
340
|
+
output_capture&.start_capture(task_class)
|
|
341
|
+
|
|
342
|
+
# Set thread-local execution context for task access
|
|
343
|
+
ExecutionContext.current = @execution_context
|
|
344
|
+
# Set thread-local registry for dependency resolution
|
|
345
|
+
Taski.set_current_registry(@registry)
|
|
346
|
+
|
|
347
|
+
begin
|
|
348
|
+
result = wrapper.task.clean
|
|
349
|
+
wrapper.mark_clean_completed(result)
|
|
350
|
+
@completion_queue.push({task_class: task_class, wrapper: wrapper, clean: true})
|
|
351
|
+
rescue Taski::TaskAbortException => e
|
|
352
|
+
@registry.request_abort!
|
|
353
|
+
wrapper.mark_clean_failed(e)
|
|
354
|
+
@completion_queue.push({task_class: task_class, wrapper: wrapper, error: e, clean: true})
|
|
355
|
+
rescue => e
|
|
356
|
+
wrapper.mark_clean_failed(e)
|
|
357
|
+
@completion_queue.push({task_class: task_class, wrapper: wrapper, error: e, clean: true})
|
|
358
|
+
ensure
|
|
359
|
+
# Stop capturing output for this task
|
|
360
|
+
output_capture&.stop_capture
|
|
361
|
+
# Clear thread-local execution context
|
|
362
|
+
ExecutionContext.current = nil
|
|
363
|
+
# Clear thread-local registry
|
|
364
|
+
Taski.clear_current_registry
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
##
|
|
369
|
+
# Runs the main event loop that processes clean completion events until all tasks have been cleaned.
|
|
370
|
+
# Continuously pops events from the internal completion queue and delegates them to the clean completion handler,
|
|
371
|
+
# stopping early if an abort is requested and no clean tasks are running.
|
|
372
|
+
# @param [Class] root_task_class - The root task class that defines the overall clean lifecycle.
|
|
373
|
+
def run_clean_main_loop(root_task_class)
|
|
374
|
+
# Find all tasks in the dependency graph
|
|
375
|
+
# Continue until all tasks have been cleaned
|
|
376
|
+
until all_tasks_cleaned?
|
|
377
|
+
break if @registry.abort_requested? && !@scheduler.running_clean_tasks?
|
|
378
|
+
|
|
379
|
+
event = @completion_queue.pop
|
|
380
|
+
handle_clean_completion(event)
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
##
|
|
385
|
+
# Processes a clean completion event and advances the cleaning workflow.
|
|
386
|
+
# Marks the completed task in the scheduler and enqueues any tasks that become ready to clean.
|
|
387
|
+
# @param [Hash] event - A completion event hash containing the `:task_class` key for the task that finished cleaning.
|
|
388
|
+
def handle_clean_completion(event)
|
|
389
|
+
task_class = event[:task_class]
|
|
390
|
+
|
|
391
|
+
debug_log("Clean completed: #{task_class}")
|
|
392
|
+
|
|
393
|
+
@scheduler.mark_clean_completed(task_class)
|
|
394
|
+
|
|
395
|
+
# Enqueue newly ready clean tasks
|
|
396
|
+
enqueue_ready_clean_tasks
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
##
|
|
400
|
+
# Determines whether all tasks have finished their clean phase.
|
|
401
|
+
# @return [Boolean] `true` if there are no ready-to-clean tasks and no running clean tasks, `false` otherwise.
|
|
402
|
+
def all_tasks_cleaned?
|
|
403
|
+
@scheduler.next_ready_clean_tasks.empty? && !@scheduler.running_clean_tasks?
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Notify observers about the root task
|
|
407
|
+
# @param root_task_class [Class] The root task class
|
|
408
|
+
# @return [void]
|
|
409
|
+
def setup_progress_display(root_task_class)
|
|
410
|
+
@execution_context.notify_set_root_task(root_task_class)
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
# Set up output capture if progress display is active and not already set up
|
|
414
|
+
# @return [Boolean] true if this executor set up the capture
|
|
415
|
+
def setup_output_capture_if_needed
|
|
416
|
+
return false unless Taski.progress_display
|
|
417
|
+
return false if @execution_context.output_capture_active?
|
|
418
|
+
|
|
419
|
+
@execution_context.setup_output_capture($stdout)
|
|
420
|
+
true
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Tear down output capture and restore original $stdout
|
|
424
|
+
# @return [void]
|
|
425
|
+
def teardown_output_capture
|
|
426
|
+
@execution_context.teardown_output_capture
|
|
427
|
+
end
|
|
230
428
|
|
|
231
|
-
|
|
429
|
+
def start_progress_display
|
|
430
|
+
@execution_context.notify_start
|
|
232
431
|
end
|
|
233
432
|
|
|
234
433
|
def stop_progress_display
|
|
434
|
+
@execution_context.notify_stop
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def create_default_execution_context
|
|
438
|
+
context = ExecutionContext.new
|
|
235
439
|
progress = Taski.progress_display
|
|
236
|
-
|
|
440
|
+
context.add_observer(progress) if progress
|
|
441
|
+
|
|
442
|
+
# Set execution trigger to break circular dependency with TaskWrapper
|
|
443
|
+
context.execution_trigger = ->(task_class, registry) do
|
|
444
|
+
Executor.execute(task_class, registry: registry, execution_context: context)
|
|
445
|
+
end
|
|
237
446
|
|
|
238
|
-
|
|
447
|
+
context
|
|
239
448
|
end
|
|
240
449
|
|
|
241
450
|
def debug_log(message)
|
|
242
451
|
return unless ENV["TASKI_DEBUG"]
|
|
243
452
|
puts "[Executor] #{message}"
|
|
244
453
|
end
|
|
454
|
+
|
|
455
|
+
# Raise error(s) if any tasks failed during execution
|
|
456
|
+
# TaskAbortException: raised directly (abort takes priority)
|
|
457
|
+
# All other errors: raises AggregateError containing all failures
|
|
458
|
+
def raise_if_any_failures
|
|
459
|
+
failed = @registry.failed_wrappers
|
|
460
|
+
return if failed.empty?
|
|
461
|
+
|
|
462
|
+
# TaskAbortException takes priority - raise the first one directly
|
|
463
|
+
abort_wrapper = failed.find { |w| w.error.is_a?(TaskAbortException) }
|
|
464
|
+
raise abort_wrapper.error if abort_wrapper
|
|
465
|
+
|
|
466
|
+
# Flatten nested AggregateErrors and deduplicate by original error object_id
|
|
467
|
+
failures = flatten_failures(failed)
|
|
468
|
+
unique_failures = failures.uniq { |f| error_identity(f.error) }
|
|
469
|
+
|
|
470
|
+
raise AggregateError.new(unique_failures)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Flatten AggregateErrors into individual TaskFailure objects
|
|
474
|
+
# Wraps original errors with task-specific Error class for rescue matching
|
|
475
|
+
def flatten_failures(failed_wrappers)
|
|
476
|
+
failed_wrappers.flat_map do |wrapper|
|
|
477
|
+
case wrapper.error
|
|
478
|
+
when AggregateError
|
|
479
|
+
wrapper.error.errors
|
|
480
|
+
else
|
|
481
|
+
wrapped_error = wrap_with_task_error(wrapper.task.class, wrapper.error)
|
|
482
|
+
[TaskFailure.new(task_class: wrapper.task.class, error: wrapped_error)]
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# Wraps an error with the task-specific Error class
|
|
488
|
+
# @param task_class [Class] The task class
|
|
489
|
+
# @param error [Exception] The original error
|
|
490
|
+
# @return [TaskError] The wrapped error
|
|
491
|
+
def wrap_with_task_error(task_class, error)
|
|
492
|
+
# Don't double-wrap if already a TaskError
|
|
493
|
+
return error if error.is_a?(TaskError)
|
|
494
|
+
|
|
495
|
+
error_class = task_class.const_get(:Error)
|
|
496
|
+
error_class.new(error, task_class: task_class)
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Raise error(s) if any tasks failed during clean execution
|
|
500
|
+
# TaskAbortException: raised directly (abort takes priority)
|
|
501
|
+
# All other errors: raises AggregateError containing all failures
|
|
502
|
+
def raise_if_any_clean_failures
|
|
503
|
+
failed = @registry.failed_clean_wrappers
|
|
504
|
+
return if failed.empty?
|
|
505
|
+
|
|
506
|
+
# TaskAbortException takes priority - raise the first one directly
|
|
507
|
+
abort_wrapper = failed.find { |w| w.clean_error.is_a?(TaskAbortException) }
|
|
508
|
+
raise abort_wrapper.clean_error if abort_wrapper
|
|
509
|
+
|
|
510
|
+
# Flatten nested AggregateErrors and deduplicate by original error object_id
|
|
511
|
+
failures = flatten_clean_failures(failed)
|
|
512
|
+
unique_failures = failures.uniq { |f| error_identity(f.error) }
|
|
513
|
+
|
|
514
|
+
raise AggregateError.new(unique_failures)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
# Flatten AggregateErrors into individual TaskFailure objects for clean errors
|
|
518
|
+
# Wraps original errors with task-specific Error class for rescue matching
|
|
519
|
+
def flatten_clean_failures(failed_wrappers)
|
|
520
|
+
failed_wrappers.flat_map do |wrapper|
|
|
521
|
+
case wrapper.clean_error
|
|
522
|
+
when AggregateError
|
|
523
|
+
wrapper.clean_error.errors
|
|
524
|
+
else
|
|
525
|
+
wrapped_error = wrap_with_task_error(wrapper.task.class, wrapper.clean_error)
|
|
526
|
+
[TaskFailure.new(task_class: wrapper.task.class, error: wrapped_error)]
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
# Returns a unique identifier for an error, used for deduplication
|
|
532
|
+
# For TaskError, uses the wrapped cause's object_id
|
|
533
|
+
def error_identity(error)
|
|
534
|
+
error.is_a?(TaskError) ? error.cause&.object_id || error.object_id : error.object_id
|
|
535
|
+
end
|
|
245
536
|
end
|
|
246
537
|
end
|
|
247
538
|
end
|
|
@@ -16,7 +16,9 @@ module Taski
|
|
|
16
16
|
# @yield Block to create the task instance if it doesn't exist
|
|
17
17
|
# @return [Object] The task instance
|
|
18
18
|
def get_or_create(task_class)
|
|
19
|
-
@
|
|
19
|
+
@monitor.synchronize do
|
|
20
|
+
@tasks[task_class] ||= yield
|
|
21
|
+
end
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
# @param task_class [Class] The task class
|
|
@@ -61,6 +63,20 @@ module Taski
|
|
|
61
63
|
@monitor.synchronize { @abort_requested }
|
|
62
64
|
end
|
|
63
65
|
|
|
66
|
+
# @return [Array<TaskWrapper>] All wrappers that have errors
|
|
67
|
+
def failed_wrappers
|
|
68
|
+
@monitor.synchronize do
|
|
69
|
+
@tasks.values.select { |w| w.error }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @return [Array<TaskWrapper>] All wrappers that have clean errors
|
|
74
|
+
def failed_clean_wrappers
|
|
75
|
+
@monitor.synchronize do
|
|
76
|
+
@tasks.values.select { |w| w.clean_error }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
64
80
|
# @param task_class [Class] The task class to run
|
|
65
81
|
# @param exported_methods [Array<Symbol>] Methods to call to trigger execution
|
|
66
82
|
# @return [Object] The result of the task execution
|