taski 0.4.2 → 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 +51 -33
- data/Steepfile +1 -0
- data/docs/GUIDE.md +340 -0
- data/examples/README.md +68 -20
- data/examples/{context_demo.rb → args_demo.rb} +27 -27
- data/examples/clean_demo.rb +204 -0
- data/examples/data_pipeline_demo.rb +3 -3
- data/examples/group_demo.rb +113 -0
- data/examples/nested_section_demo.rb +161 -0
- data/examples/parallel_progress_demo.rb +1 -1
- data/examples/reexecution_demo.rb +93 -80
- data/examples/system_call_demo.rb +56 -0
- data/examples/tree_progress_demo.rb +164 -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 +538 -0
- data/lib/taski/execution/registry.rb +26 -2
- 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 +295 -146
- data/lib/taski/execution/tree_progress_display.rb +793 -0
- data/lib/taski/execution/worker_pool.rb +104 -0
- data/lib/taski/section.rb +23 -0
- data/lib/taski/static_analysis/analyzer.rb +4 -2
- data/lib/taski/static_analysis/visitor.rb +86 -5
- data/lib/taski/task.rb +223 -120
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +147 -28
- data/sig/taski.rbs +310 -67
- metadata +17 -8
- data/docs/advanced-features.md +0 -625
- data/docs/api-guide.md +0 -509
- data/docs/error-handling.md +0 -684
- data/lib/taski/execution/coordinator.rb +0 -63
- data/lib/taski/execution/parallel_progress_display.rb +0 -201
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
module Taski
|
|
6
|
+
module Execution
|
|
7
|
+
# Producer-Consumer pattern executor for parallel task execution.
|
|
8
|
+
#
|
|
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
|
|
20
|
+
#
|
|
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)
|
|
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)
|
|
41
|
+
class Executor
|
|
42
|
+
class << self
|
|
43
|
+
# Execute a task and all its dependencies
|
|
44
|
+
# @param root_task_class [Class] The root task class to execute
|
|
45
|
+
# @param registry [Registry] The task registry
|
|
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)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
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)
|
|
76
|
+
@registry = registry
|
|
77
|
+
@completion_queue = Queue.new
|
|
78
|
+
|
|
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) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Execute root task and all dependencies
|
|
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.
|
|
106
|
+
def execute(root_task_class)
|
|
107
|
+
# Build dependency graph from static analysis
|
|
108
|
+
@scheduler.build_dependency_graph(root_task_class)
|
|
109
|
+
|
|
110
|
+
# Set up progress display with root task
|
|
111
|
+
setup_progress_display(root_task_class)
|
|
112
|
+
|
|
113
|
+
# Set up output capture (returns true if this executor set it up)
|
|
114
|
+
should_teardown_capture = setup_output_capture_if_needed
|
|
115
|
+
|
|
116
|
+
# Start progress display
|
|
117
|
+
start_progress_display
|
|
118
|
+
|
|
119
|
+
# Start worker threads
|
|
120
|
+
@worker_pool.start
|
|
121
|
+
|
|
122
|
+
# Enqueue tasks with no dependencies
|
|
123
|
+
enqueue_ready_tasks
|
|
124
|
+
|
|
125
|
+
# Main event loop - continues until root task completes
|
|
126
|
+
run_main_loop(root_task_class)
|
|
127
|
+
|
|
128
|
+
# Shutdown workers
|
|
129
|
+
@worker_pool.shutdown
|
|
130
|
+
|
|
131
|
+
# Stop progress display
|
|
132
|
+
stop_progress_display
|
|
133
|
+
|
|
134
|
+
# Restore original stdout (only if this executor set it up)
|
|
135
|
+
teardown_output_capture if should_teardown_capture
|
|
136
|
+
|
|
137
|
+
# Raise aggregated errors if any tasks failed
|
|
138
|
+
raise_if_any_failures
|
|
139
|
+
end
|
|
140
|
+
|
|
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) }
|
|
174
|
+
|
|
175
|
+
# Start worker threads
|
|
176
|
+
@clean_worker_pool.start
|
|
177
|
+
|
|
178
|
+
# Enqueue tasks ready for clean (no reverse dependencies)
|
|
179
|
+
enqueue_ready_clean_tasks
|
|
180
|
+
|
|
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
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
private
|
|
198
|
+
|
|
199
|
+
# Enqueue all tasks that are ready to execute
|
|
200
|
+
def enqueue_ready_tasks
|
|
201
|
+
@scheduler.next_ready_tasks.each do |task_class|
|
|
202
|
+
enqueue_task(task_class)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Enqueue a single task for execution
|
|
207
|
+
def enqueue_task(task_class)
|
|
208
|
+
return if @registry.abort_requested?
|
|
209
|
+
|
|
210
|
+
@scheduler.mark_enqueued(task_class)
|
|
211
|
+
|
|
212
|
+
wrapper = get_or_create_wrapper(task_class)
|
|
213
|
+
return unless wrapper.mark_running
|
|
214
|
+
|
|
215
|
+
@execution_context.notify_task_registered(task_class)
|
|
216
|
+
@execution_context.notify_task_started(task_class)
|
|
217
|
+
|
|
218
|
+
@worker_pool.enqueue(task_class, wrapper)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Get or create a task wrapper via Registry
|
|
222
|
+
def get_or_create_wrapper(task_class)
|
|
223
|
+
@registry.get_or_create(task_class) do
|
|
224
|
+
task_instance = task_class.allocate
|
|
225
|
+
task_instance.send(:initialize)
|
|
226
|
+
TaskWrapper.new(task_instance, registry: @registry, execution_context: @execution_context)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Execute a task and send completion event (called by WorkerPool)
|
|
231
|
+
def execute_task(task_class, wrapper)
|
|
232
|
+
return if @registry.abort_requested?
|
|
233
|
+
|
|
234
|
+
output_capture = @execution_context.output_capture
|
|
235
|
+
|
|
236
|
+
# Start capturing output for this task
|
|
237
|
+
output_capture&.start_capture(task_class)
|
|
238
|
+
|
|
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)
|
|
243
|
+
|
|
244
|
+
begin
|
|
245
|
+
result = wrapper.task.run
|
|
246
|
+
wrapper.mark_completed(result)
|
|
247
|
+
@completion_queue.push({task_class: task_class, wrapper: wrapper})
|
|
248
|
+
rescue Taski::TaskAbortException => e
|
|
249
|
+
@registry.request_abort!
|
|
250
|
+
wrapper.mark_failed(e)
|
|
251
|
+
@completion_queue.push({task_class: task_class, wrapper: wrapper, error: e})
|
|
252
|
+
rescue => e
|
|
253
|
+
wrapper.mark_failed(e)
|
|
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
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Main thread event loop - continues until root task completes
|
|
266
|
+
def run_main_loop(root_task_class)
|
|
267
|
+
until @scheduler.completed?(root_task_class)
|
|
268
|
+
break if @registry.abort_requested? && !@scheduler.running_tasks?
|
|
269
|
+
|
|
270
|
+
event = @completion_queue.pop
|
|
271
|
+
handle_completion(event)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
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.
|
|
279
|
+
def handle_completion(event)
|
|
280
|
+
task_class = event[:task_class]
|
|
281
|
+
|
|
282
|
+
debug_log("Completed: #{task_class}")
|
|
283
|
+
|
|
284
|
+
@scheduler.mark_completed(task_class)
|
|
285
|
+
|
|
286
|
+
# Enqueue newly ready tasks
|
|
287
|
+
enqueue_ready_tasks
|
|
288
|
+
end
|
|
289
|
+
|
|
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
|
|
300
|
+
end
|
|
301
|
+
|
|
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?
|
|
311
|
+
|
|
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)
|
|
320
|
+
end
|
|
321
|
+
|
|
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
|
|
428
|
+
|
|
429
|
+
def start_progress_display
|
|
430
|
+
@execution_context.notify_start
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def stop_progress_display
|
|
434
|
+
@execution_context.notify_stop
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def create_default_execution_context
|
|
438
|
+
context = ExecutionContext.new
|
|
439
|
+
progress = Taski.progress_display
|
|
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
|
|
446
|
+
|
|
447
|
+
context
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def debug_log(message)
|
|
451
|
+
return unless ENV["TASKI_DEBUG"]
|
|
452
|
+
puts "[Executor] #{message}"
|
|
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
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
end
|
|
@@ -16,7 +16,15 @@ 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
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param task_class [Class] The task class
|
|
25
|
+
# @param wrapper [TaskWrapper] The wrapper instance to register
|
|
26
|
+
def register(task_class, wrapper)
|
|
27
|
+
@tasks[task_class] = wrapper
|
|
20
28
|
end
|
|
21
29
|
|
|
22
30
|
# @param task_class [Class] The task class
|
|
@@ -55,6 +63,20 @@ module Taski
|
|
|
55
63
|
@monitor.synchronize { @abort_requested }
|
|
56
64
|
end
|
|
57
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
|
+
|
|
58
80
|
# @param task_class [Class] The task class to run
|
|
59
81
|
# @param exported_methods [Array<Symbol>] Methods to call to trigger execution
|
|
60
82
|
# @return [Object] The result of the task execution
|
|
@@ -65,7 +87,9 @@ module Taski
|
|
|
65
87
|
|
|
66
88
|
wait_all
|
|
67
89
|
|
|
68
|
-
|
|
90
|
+
# @type var wrapper: Taski::Execution::TaskWrapper
|
|
91
|
+
wrapper = get_task(task_class)
|
|
92
|
+
wrapper.result
|
|
69
93
|
end
|
|
70
94
|
end
|
|
71
95
|
end
|