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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -0
  3. data/README.md +51 -33
  4. data/Steepfile +1 -0
  5. data/docs/GUIDE.md +340 -0
  6. data/examples/README.md +68 -20
  7. data/examples/{context_demo.rb → args_demo.rb} +27 -27
  8. data/examples/clean_demo.rb +204 -0
  9. data/examples/data_pipeline_demo.rb +3 -3
  10. data/examples/group_demo.rb +113 -0
  11. data/examples/nested_section_demo.rb +161 -0
  12. data/examples/parallel_progress_demo.rb +1 -1
  13. data/examples/reexecution_demo.rb +93 -80
  14. data/examples/system_call_demo.rb +56 -0
  15. data/examples/tree_progress_demo.rb +164 -0
  16. data/lib/taski/{context.rb → args.rb} +3 -3
  17. data/lib/taski/execution/execution_context.rb +379 -0
  18. data/lib/taski/execution/executor.rb +538 -0
  19. data/lib/taski/execution/registry.rb +26 -2
  20. data/lib/taski/execution/scheduler.rb +308 -0
  21. data/lib/taski/execution/task_output_pipe.rb +42 -0
  22. data/lib/taski/execution/task_output_router.rb +216 -0
  23. data/lib/taski/execution/task_wrapper.rb +295 -146
  24. data/lib/taski/execution/tree_progress_display.rb +793 -0
  25. data/lib/taski/execution/worker_pool.rb +104 -0
  26. data/lib/taski/section.rb +23 -0
  27. data/lib/taski/static_analysis/analyzer.rb +4 -2
  28. data/lib/taski/static_analysis/visitor.rb +86 -5
  29. data/lib/taski/task.rb +223 -120
  30. data/lib/taski/version.rb +1 -1
  31. data/lib/taski.rb +147 -28
  32. data/sig/taski.rbs +310 -67
  33. metadata +17 -8
  34. data/docs/advanced-features.md +0 -625
  35. data/docs/api-guide.md +0 -509
  36. data/docs/error-handling.md +0 -684
  37. data/lib/taski/execution/coordinator.rb +0 -63
  38. 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
- @tasks[task_class] ||= yield
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
- get_task(task_class).result
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