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
|
@@ -26,24 +26,33 @@ module Taski
|
|
|
26
26
|
# In the Producer-Consumer pattern, TaskWrapper does NOT start threads.
|
|
27
27
|
# The Executor controls all scheduling and execution.
|
|
28
28
|
class TaskWrapper
|
|
29
|
-
attr_reader :task, :result, :error, :timing
|
|
29
|
+
attr_reader :task, :result, :error, :timing, :clean_error
|
|
30
30
|
|
|
31
31
|
STATE_PENDING = :pending
|
|
32
32
|
STATE_RUNNING = :running
|
|
33
33
|
STATE_COMPLETED = :completed
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
##
|
|
36
|
+
# Create a new TaskWrapper for the given task and registry.
|
|
37
|
+
# Initializes synchronization primitives, state tracking for execution and cleanup, and timing/result/error holders.
|
|
38
|
+
# @param [Object] task - The task instance being wrapped.
|
|
39
|
+
# @param [Object] registry - The registry used to query abort status and coordinate execution.
|
|
40
|
+
# @param [Object, nil] execution_context - Optional execution context used to trigger and report execution and cleanup.
|
|
41
|
+
def initialize(task, registry:, execution_context: nil)
|
|
36
42
|
@task = task
|
|
37
43
|
@registry = registry
|
|
44
|
+
@execution_context = execution_context
|
|
38
45
|
@result = nil
|
|
39
46
|
@clean_result = nil
|
|
40
47
|
@error = nil
|
|
48
|
+
@clean_error = nil
|
|
41
49
|
@monitor = Monitor.new
|
|
42
50
|
@condition = @monitor.new_cond
|
|
43
51
|
@clean_condition = @monitor.new_cond
|
|
44
52
|
@state = STATE_PENDING
|
|
45
53
|
@clean_state = STATE_PENDING
|
|
46
54
|
@timing = nil
|
|
55
|
+
@clean_timing = nil
|
|
47
56
|
end
|
|
48
57
|
|
|
49
58
|
# @return [Symbol] Current state
|
|
@@ -61,28 +70,67 @@ module Taski
|
|
|
61
70
|
state == STATE_COMPLETED
|
|
62
71
|
end
|
|
63
72
|
|
|
73
|
+
# Resets the wrapper state to allow re-execution.
|
|
74
|
+
# Clears all cached results and returns state to pending.
|
|
75
|
+
def reset!
|
|
76
|
+
@monitor.synchronize do
|
|
77
|
+
@state = STATE_PENDING
|
|
78
|
+
@clean_state = STATE_PENDING
|
|
79
|
+
@result = nil
|
|
80
|
+
@clean_result = nil
|
|
81
|
+
@error = nil
|
|
82
|
+
@clean_error = nil
|
|
83
|
+
@timing = nil
|
|
84
|
+
@clean_timing = nil
|
|
85
|
+
end
|
|
86
|
+
@task.reset! if @task.respond_to?(:reset!)
|
|
87
|
+
@registry.reset!
|
|
88
|
+
end
|
|
89
|
+
|
|
64
90
|
# Called by user code to get result. Triggers execution if needed.
|
|
91
|
+
# Sets up args if not already set (for Task.new.run usage).
|
|
65
92
|
# @return [Object] The result of task execution
|
|
66
93
|
def run
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
94
|
+
with_args_lifecycle do
|
|
95
|
+
trigger_execution_and_wait
|
|
96
|
+
raise @error if @error # steep:ignore
|
|
97
|
+
@result
|
|
98
|
+
end
|
|
70
99
|
end
|
|
71
100
|
|
|
72
101
|
# Called by user code to clean. Triggers clean execution if needed.
|
|
102
|
+
# Sets up args if not already set (for Task.new.clean usage).
|
|
73
103
|
# @return [Object] The result of cleanup
|
|
74
104
|
def clean
|
|
75
|
-
|
|
76
|
-
|
|
105
|
+
with_args_lifecycle do
|
|
106
|
+
trigger_clean_and_wait
|
|
107
|
+
@clean_result
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Called by user code to run and clean. Runs execution followed by cleanup.
|
|
112
|
+
# If run fails, clean is still executed for resource release.
|
|
113
|
+
# Pre-increments progress display nest_level to prevent double rendering.
|
|
114
|
+
# @return [Object] The result of task execution
|
|
115
|
+
def run_and_clean
|
|
116
|
+
context = ensure_execution_context
|
|
117
|
+
context.notify_start # Pre-increment nest_level to prevent double rendering
|
|
118
|
+
run
|
|
119
|
+
ensure
|
|
120
|
+
clean
|
|
121
|
+
context&.notify_stop # Final decrement and render
|
|
77
122
|
end
|
|
78
123
|
|
|
79
124
|
# Called by user code to get exported value. Triggers execution if needed.
|
|
125
|
+
# Sets up args if not already set (for Task.new usage).
|
|
80
126
|
# @param method_name [Symbol] The name of the exported method
|
|
81
127
|
# @return [Object] The exported value
|
|
82
128
|
def get_exported_value(method_name)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
129
|
+
with_args_lifecycle do
|
|
130
|
+
trigger_execution_and_wait
|
|
131
|
+
raise @error if @error # steep:ignore
|
|
132
|
+
@task.public_send(method_name)
|
|
133
|
+
end
|
|
86
134
|
end
|
|
87
135
|
|
|
88
136
|
# Called by Executor to mark task as running
|
|
@@ -108,7 +156,10 @@ module Taski
|
|
|
108
156
|
end
|
|
109
157
|
|
|
110
158
|
# Called by Executor when task.run raises an error
|
|
111
|
-
|
|
159
|
+
##
|
|
160
|
+
# Marks the task as failed and records the error.
|
|
161
|
+
# Records the provided error, sets the task state to completed, updates the timing end time, notifies threads waiting for completion, and reports the failure to the execution context.
|
|
162
|
+
# @param [Exception] error - The exception raised during task execution.
|
|
112
163
|
def mark_failed(error)
|
|
113
164
|
@timing = @timing&.with_end_now
|
|
114
165
|
@monitor.synchronize do
|
|
@@ -119,30 +170,75 @@ module Taski
|
|
|
119
170
|
update_progress(:failed, error: error)
|
|
120
171
|
end
|
|
121
172
|
|
|
173
|
+
# Called by Executor to mark clean as running
|
|
174
|
+
##
|
|
175
|
+
# Mark the task's cleanup state as running and start timing.
|
|
176
|
+
# @return [Boolean] `true` if the clean state was changed from pending to running, `false` otherwise.
|
|
177
|
+
def mark_clean_running
|
|
178
|
+
@monitor.synchronize do
|
|
179
|
+
return false unless @clean_state == STATE_PENDING
|
|
180
|
+
@clean_state = STATE_RUNNING
|
|
181
|
+
@clean_timing = TaskTiming.start_now
|
|
182
|
+
true
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
122
186
|
# Called by Executor after clean completes
|
|
123
|
-
|
|
187
|
+
##
|
|
188
|
+
# Marks the cleanup run as completed, stores the cleanup result, sets the clean state to COMPLETED,
|
|
189
|
+
# notifies any waiters, and reports completion to observers.
|
|
190
|
+
# @param [Object] result - The result of the cleanup operation.
|
|
124
191
|
def mark_clean_completed(result)
|
|
192
|
+
@clean_timing = @clean_timing&.with_end_now
|
|
125
193
|
@monitor.synchronize do
|
|
126
194
|
@clean_result = result
|
|
127
195
|
@clean_state = STATE_COMPLETED
|
|
128
196
|
@clean_condition.broadcast
|
|
129
197
|
end
|
|
198
|
+
update_clean_progress(:clean_completed, duration: @clean_timing&.duration_ms)
|
|
130
199
|
end
|
|
131
200
|
|
|
132
|
-
#
|
|
201
|
+
# Called by Executor when clean raises an error
|
|
202
|
+
##
|
|
203
|
+
# Marks the cleanup as failed by storing the cleanup error, transitioning the cleanup state to completed,
|
|
204
|
+
# notifying any waiters, and reports failure to observers.
|
|
205
|
+
# @param [Exception] error - The exception raised during the cleanup run.
|
|
206
|
+
def mark_clean_failed(error)
|
|
207
|
+
@clean_timing = @clean_timing&.with_end_now
|
|
208
|
+
@monitor.synchronize do
|
|
209
|
+
@clean_error = error
|
|
210
|
+
@clean_state = STATE_COMPLETED
|
|
211
|
+
@clean_condition.broadcast
|
|
212
|
+
end
|
|
213
|
+
update_clean_progress(:clean_failed, duration: @clean_timing&.duration_ms, error: error)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
##
|
|
217
|
+
# Blocks the current thread until the task reaches the completed state.
|
|
218
|
+
#
|
|
219
|
+
# The caller will be suspended until the wrapper's state becomes STATE_COMPLETED.
|
|
220
|
+
# This method does not raise on its own; any errors from task execution are surfaced elsewhere.
|
|
133
221
|
def wait_for_completion
|
|
134
222
|
@monitor.synchronize do
|
|
135
223
|
@condition.wait_until { @state == STATE_COMPLETED }
|
|
136
224
|
end
|
|
137
225
|
end
|
|
138
226
|
|
|
139
|
-
|
|
227
|
+
##
|
|
228
|
+
# Blocks the current thread until the task's clean phase reaches the completed state.
|
|
229
|
+
# The caller will be suspended until the wrapper's clean_state becomes STATE_COMPLETED.
|
|
140
230
|
def wait_for_clean_completion
|
|
141
231
|
@monitor.synchronize do
|
|
142
232
|
@clean_condition.wait_until { @clean_state == STATE_COMPLETED }
|
|
143
233
|
end
|
|
144
234
|
end
|
|
145
235
|
|
|
236
|
+
##
|
|
237
|
+
# Delegates method calls to get_exported_value for exported task methods.
|
|
238
|
+
# @param method_name [Symbol] The method name being called.
|
|
239
|
+
# @param args [Array] Arguments passed to the method.
|
|
240
|
+
# @param block [Proc] Block passed to the method.
|
|
241
|
+
# @return [Object] The exported value for the method.
|
|
146
242
|
def method_missing(method_name, *args, &block)
|
|
147
243
|
if @task.class.method_defined?(method_name)
|
|
148
244
|
get_exported_value(method_name)
|
|
@@ -151,13 +247,34 @@ module Taski
|
|
|
151
247
|
end
|
|
152
248
|
end
|
|
153
249
|
|
|
250
|
+
##
|
|
251
|
+
# Returns true if the task class defines the given method.
|
|
252
|
+
# @param method_name [Symbol] The method name to check.
|
|
253
|
+
# @param include_private [Boolean] Whether to include private methods.
|
|
254
|
+
# @return [Boolean] true if the task responds to the method.
|
|
154
255
|
def respond_to_missing?(method_name, include_private = false)
|
|
155
256
|
@task.class.method_defined?(method_name) || super
|
|
156
257
|
end
|
|
157
258
|
|
|
158
259
|
private
|
|
159
260
|
|
|
160
|
-
|
|
261
|
+
##
|
|
262
|
+
# Ensures args are set during block execution, then resets if they weren't set before.
|
|
263
|
+
# This allows Task.new.run usage without requiring explicit args setup.
|
|
264
|
+
# @yield The block to execute with args lifecycle management
|
|
265
|
+
# @return [Object] The result of the block
|
|
266
|
+
def with_args_lifecycle
|
|
267
|
+
args_was_nil = Taski.args.nil?
|
|
268
|
+
Taski.start_args(options: {}, root_task: @task.class) if args_was_nil
|
|
269
|
+
yield
|
|
270
|
+
ensure
|
|
271
|
+
Taski.reset_args! if args_was_nil
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
##
|
|
275
|
+
# Ensures the task is executed if still pending and waits for completion.
|
|
276
|
+
# If the task is pending, triggers execution (via the configured ExecutionContext when present, otherwise via Executor) outside the monitor; if the task is running, waits until it becomes completed; if already completed, returns immediately.
|
|
277
|
+
# @raise [Taski::TaskAbortException] If the registry requested an abort before execution begins.
|
|
161
278
|
def trigger_execution_and_wait
|
|
162
279
|
should_execute = false
|
|
163
280
|
@monitor.synchronize do
|
|
@@ -174,57 +291,110 @@ module Taski
|
|
|
174
291
|
|
|
175
292
|
if should_execute
|
|
176
293
|
# Execute outside the lock to avoid deadlock
|
|
177
|
-
|
|
178
|
-
|
|
294
|
+
# Use ensure_execution_context to create a shared context if not set
|
|
295
|
+
context = ensure_execution_context
|
|
296
|
+
context.trigger_execution(@task.class, registry: @registry)
|
|
297
|
+
# After execution returns, the task is completed
|
|
179
298
|
end
|
|
180
299
|
end
|
|
181
300
|
|
|
182
|
-
|
|
301
|
+
##
|
|
302
|
+
# Triggers task cleanup through the configured execution mechanism and waits until the cleanup completes.
|
|
303
|
+
#
|
|
304
|
+
# If an ExecutionContext is configured the cleanup is invoked through it; otherwise a fallback executor is used.
|
|
305
|
+
# @raise [Taski::TaskAbortException] if the registry has requested an abort.
|
|
183
306
|
def trigger_clean_and_wait
|
|
307
|
+
should_execute = false
|
|
184
308
|
@monitor.synchronize do
|
|
185
309
|
case @clean_state
|
|
186
310
|
when STATE_PENDING
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
thread = Thread.new { execute_clean }
|
|
190
|
-
@registry.register_thread(thread)
|
|
191
|
-
@clean_condition.wait_until { @clean_state == STATE_COMPLETED }
|
|
311
|
+
check_abort!
|
|
312
|
+
should_execute = true
|
|
192
313
|
when STATE_RUNNING
|
|
193
314
|
@clean_condition.wait_until { @clean_state == STATE_COMPLETED }
|
|
194
315
|
when STATE_COMPLETED
|
|
195
316
|
# Already done
|
|
196
317
|
end
|
|
197
318
|
end
|
|
198
|
-
end
|
|
199
319
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
end
|
|
207
|
-
|
|
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 }
|
|
320
|
+
if should_execute
|
|
321
|
+
# Execute outside the lock to avoid deadlock
|
|
322
|
+
# Use ensure_execution_context to reuse the context from run phase
|
|
323
|
+
context = ensure_execution_context
|
|
324
|
+
context.trigger_clean(@task.class, registry: @registry)
|
|
325
|
+
# After execution returns, the task is completed
|
|
214
326
|
end
|
|
215
|
-
wait_threads.each(&:join)
|
|
216
327
|
end
|
|
217
328
|
|
|
329
|
+
##
|
|
330
|
+
# Checks whether the registry has requested an abort and raises an exception to stop starting new tasks.
|
|
331
|
+
# @raise [Taski::TaskAbortException] if `@registry.abort_requested?` is true — raised with the message "Execution aborted - no new tasks will start".
|
|
218
332
|
def check_abort!
|
|
219
333
|
if @registry.abort_requested?
|
|
220
334
|
raise Taski::TaskAbortException, "Execution aborted - no new tasks will start"
|
|
221
335
|
end
|
|
222
336
|
end
|
|
223
337
|
|
|
338
|
+
##
|
|
339
|
+
# Ensures an execution context exists for this wrapper.
|
|
340
|
+
# Returns the existing context if set, otherwise creates a shared context.
|
|
341
|
+
# This enables run and clean phases to share state like runtime dependencies.
|
|
342
|
+
# @return [ExecutionContext] The execution context for this wrapper
|
|
343
|
+
def ensure_execution_context
|
|
344
|
+
@execution_context ||= create_shared_context
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
##
|
|
348
|
+
# Creates a shared execution context with proper triggers for run and clean.
|
|
349
|
+
# The context is configured to reuse itself when triggering nested executions.
|
|
350
|
+
# @return [ExecutionContext] A new execution context
|
|
351
|
+
def create_shared_context
|
|
352
|
+
context = ExecutionContext.new
|
|
353
|
+
progress = Taski.progress_display
|
|
354
|
+
context.add_observer(progress) if progress
|
|
355
|
+
|
|
356
|
+
# Set triggers to reuse this context for nested executions
|
|
357
|
+
context.execution_trigger = ->(task_class, registry) do
|
|
358
|
+
Executor.execute(task_class, registry: registry, execution_context: context)
|
|
359
|
+
end
|
|
360
|
+
context.clean_trigger = ->(task_class, registry) do
|
|
361
|
+
Executor.execute_clean(task_class, registry: registry, execution_context: context)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
context
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
##
|
|
368
|
+
# Notifies the execution context of task completion or failure.
|
|
369
|
+
# Falls back to getting the current context if not set during initialization.
|
|
370
|
+
# @param state [Symbol] The completion state (unused, kept for API consistency).
|
|
371
|
+
# @param duration [Numeric, nil] The execution duration in milliseconds.
|
|
372
|
+
# @param error [Exception, nil] The error if the task failed.
|
|
224
373
|
def update_progress(state, duration: nil, error: nil)
|
|
225
|
-
|
|
374
|
+
# Defensive fallback: try to get current context if not set during initialization
|
|
375
|
+
@execution_context ||= ExecutionContext.current
|
|
376
|
+
return unless @execution_context
|
|
377
|
+
|
|
378
|
+
@execution_context.notify_task_completed(@task.class, duration: duration, error: error)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
##
|
|
382
|
+
# Notifies the execution context of clean completion or failure.
|
|
383
|
+
# Falls back to getting the current context if not set during initialization.
|
|
384
|
+
# @param state [Symbol] The clean state (unused, kept for API consistency).
|
|
385
|
+
# @param duration [Numeric, nil] The clean duration in milliseconds.
|
|
386
|
+
# @param error [Exception, nil] The error if the clean failed.
|
|
387
|
+
def update_clean_progress(state, duration: nil, error: nil)
|
|
388
|
+
# Defensive fallback: try to get current context if not set during initialization
|
|
389
|
+
@execution_context ||= ExecutionContext.current
|
|
390
|
+
return unless @execution_context
|
|
391
|
+
|
|
392
|
+
@execution_context.notify_clean_completed(@task.class, duration: duration, error: error)
|
|
226
393
|
end
|
|
227
394
|
|
|
395
|
+
##
|
|
396
|
+
# Outputs a debug message if TASKI_DEBUG environment variable is set.
|
|
397
|
+
# @param message [String] The debug message to output.
|
|
228
398
|
def debug_log(message)
|
|
229
399
|
return unless ENV["TASKI_DEBUG"]
|
|
230
400
|
puts "[TaskWrapper] #{message}"
|