taski 0.5.0 → 0.7.1
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 +168 -21
- data/docs/GUIDE.md +394 -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/data_pipeline_demo.rb +1 -1
- data/examples/group_demo.rb +113 -0
- data/examples/large_tree_demo.rb +519 -0
- data/examples/reexecution_demo.rb +93 -80
- data/examples/simple_progress_demo.rb +80 -0
- data/examples/system_call_demo.rb +56 -0
- data/lib/taski/{context.rb → args.rb} +3 -3
- data/lib/taski/execution/base_progress_display.rb +348 -0
- data/lib/taski/execution/execution_context.rb +383 -0
- data/lib/taski/execution/executor.rb +405 -134
- data/lib/taski/execution/plain_progress_display.rb +76 -0
- data/lib/taski/execution/registry.rb +17 -1
- data/lib/taski/execution/scheduler.rb +308 -0
- data/lib/taski/execution/simple_progress_display.rb +173 -0
- data/lib/taski/execution/task_output_pipe.rb +42 -0
- data/lib/taski/execution/task_output_router.rb +287 -0
- data/lib/taski/execution/task_wrapper.rb +215 -52
- data/lib/taski/execution/tree_progress_display.rb +349 -212
- data/lib/taski/execution/worker_pool.rb +104 -0
- data/lib/taski/section.rb +16 -3
- data/lib/taski/static_analysis/visitor.rb +3 -0
- data/lib/taski/task.rb +218 -37
- data/lib/taski/test_helper/errors.rb +13 -0
- data/lib/taski/test_helper/minitest.rb +38 -0
- data/lib/taski/test_helper/mock_registry.rb +51 -0
- data/lib/taski/test_helper/mock_wrapper.rb +46 -0
- data/lib/taski/test_helper/rspec.rb +38 -0
- data/lib/taski/test_helper.rb +214 -0
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +211 -23
- data/sig/taski.rbs +207 -27
- metadata +25 -8
- 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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0ca85800083318a5bc4d40151f14e5c55d6474a81a89a095de6d2433a8609043
|
|
4
|
+
data.tar.gz: d68b0a3f9bf5bab689882df7cdf22fe02a169008a3d4bd64a24eaea2699dce33
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3705a29cdf6e5fbe4a244b9431c35bc326e2c383f71e00fe7223a1e87f98728e565e1faebbd081111ab8127197e39294c89d05c6e35511b8405425f21b6a8af3
|
|
7
|
+
data.tar.gz: 87d334c0e053bbd164c260e304f642a26305f14021de17bf10bb8d50180fd0160338efbca0821a3fc3af996b8212e3a10c52d5e533c54b6141b069e17f21af7a
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.7.0] - 2025-12-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Group block for organizing progress display messages (`Taski.group`) ([#105](https://github.com/ahogappa/taski/pull/105))
|
|
14
|
+
- Scope-based execution with thread-local registry for independent task execution ([#103](https://github.com/ahogappa/taski/pull/103))
|
|
15
|
+
- `TaskClass::Error` auto-generation for task-specific error handling ([#95](https://github.com/ahogappa/taski/pull/95))
|
|
16
|
+
- `AggregateAware` module for transparent rescue matching with `AggregateError` ([#95](https://github.com/ahogappa/taski/pull/95))
|
|
17
|
+
- `AggregateError#includes?` and `AggregateError#find` methods for searching aggregated errors ([#95](https://github.com/ahogappa/taski/pull/95))
|
|
18
|
+
- Aggregation of multiple errors in parallel execution ([#95](https://github.com/ahogappa/taski/pull/95))
|
|
19
|
+
- `workers` parameter to `Task.run`, `Task.clean`, and `Task.run_and_clean` for configurable parallelism ([#92](https://github.com/ahogappa/taski/pull/92))
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Renamed `context` to `args` for API clarity (BREAKING CHANGE) ([#94](https://github.com/ahogappa/taski/pull/94))
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- Thread-safety improvements in `Registry#get_or_create` ([#90](https://github.com/ahogappa/taski/pull/90))
|
|
26
|
+
|
|
27
|
+
## [0.6.0] - 2025-12-21
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- `Task#system` override for capturing subprocess output in progress display
|
|
31
|
+
- `ExecutionContext` with observer pattern for managing execution state
|
|
32
|
+
- Inline task output display in progress tree
|
|
33
|
+
- Clean execution with reverse dependency order via `run_and_clean`
|
|
34
|
+
- Comprehensive documentation for `TaskAbortException`
|
|
35
|
+
- Unit tests for `ExecutionContext`, `WorkerPool`, and `Scheduler`
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- Replaced `ThreadOutputCapture` with pipe-based `TaskOutputRouter` for more reliable output capture
|
|
39
|
+
- Split `Executor` into separate `Scheduler` and `WorkerPool` classes for better separation of concerns
|
|
40
|
+
- Centralized tree building logic in `TreeProgressDisplay`
|
|
41
|
+
- Improved `run_and_clean` implementation and display
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
- Added mutex protection to `impl_call_order` accessor for thread safety
|
|
45
|
+
- Fixed `ExecutionContext` passing to `TaskWrapper` resolving progress display garbage
|
|
46
|
+
- Improved output capture reliability for progress display
|
|
47
|
+
|
|
48
|
+
## [0.5.0] - 2025-11-30
|
|
49
|
+
|
|
50
|
+
- Initial release with core task execution functionality
|
data/README.md
CHANGED
|
@@ -113,9 +113,90 @@ end
|
|
|
113
113
|
|
|
114
114
|
> **Note**: Nested implementation classes automatically inherit Section's `interfaces` as `exports`.
|
|
115
115
|
|
|
116
|
+
## Best Practices
|
|
117
|
+
|
|
118
|
+
### Keep Tasks Small and Focused
|
|
119
|
+
|
|
120
|
+
Each task should do **one thing only**. While Taski allows you to write complex logic within a single task, keeping tasks small and focused provides significant benefits:
|
|
121
|
+
|
|
122
|
+
```ruby
|
|
123
|
+
# ✅ Good: Small, focused tasks
|
|
124
|
+
class FetchData < Taski::Task
|
|
125
|
+
exports :data
|
|
126
|
+
def run
|
|
127
|
+
@data = API.fetch
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
class TransformData < Taski::Task
|
|
132
|
+
exports :result
|
|
133
|
+
def run
|
|
134
|
+
@result = FetchData.data.transform
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
class SaveData < Taski::Task
|
|
139
|
+
def run
|
|
140
|
+
Database.save(TransformData.result)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# ❌ Avoid: Monolithic task doing everything
|
|
145
|
+
class DoEverything < Taski::Task
|
|
146
|
+
def run
|
|
147
|
+
data = API.fetch
|
|
148
|
+
result = data.transform
|
|
149
|
+
Database.save(result)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Why small tasks matter:**
|
|
155
|
+
|
|
156
|
+
- **Parallel Execution**: Independent tasks run concurrently. Large monolithic tasks can't be parallelized
|
|
157
|
+
- **Easier Cleanup**: `Task.clean` works per-task. Smaller tasks mean more granular cleanup control
|
|
158
|
+
- **Better Reusability**: Small tasks can be composed into different workflows
|
|
159
|
+
- **Clearer Dependencies**: The dependency graph becomes explicit and visible with `Task.tree`
|
|
160
|
+
|
|
161
|
+
**Note:** Complex internal logic is perfectly fine. "One thing" means one responsibility, not one line of code. Other tasks only care about the exported results, not how they were computed.
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
class RawData < Taski::Task
|
|
165
|
+
exports :data
|
|
166
|
+
def run
|
|
167
|
+
@data = API.fetch
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
class ProcessedData < Taski::Task
|
|
172
|
+
exports :result
|
|
173
|
+
|
|
174
|
+
def run
|
|
175
|
+
# Complex internal logic is OK - this task has one responsibility:
|
|
176
|
+
# producing the processed result
|
|
177
|
+
validated = validate_and_clean(RawData.data)
|
|
178
|
+
enriched = enrich_with_metadata(validated)
|
|
179
|
+
normalized = normalize_format(enriched)
|
|
180
|
+
@result = apply_business_rules(normalized)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
def validate_and_clean(data)
|
|
186
|
+
# Complex validation logic...
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def enrich_with_metadata(data)
|
|
190
|
+
# Complex enrichment logic...
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# ... other private methods
|
|
194
|
+
end
|
|
195
|
+
```
|
|
196
|
+
|
|
116
197
|
## Advanced Usage
|
|
117
198
|
|
|
118
|
-
###
|
|
199
|
+
### Args - Runtime Information and Options
|
|
119
200
|
|
|
120
201
|
Pass custom options and access execution context from any task:
|
|
121
202
|
|
|
@@ -123,43 +204,62 @@ Pass custom options and access execution context from any task:
|
|
|
123
204
|
class DeployTask < Taski::Task
|
|
124
205
|
def run
|
|
125
206
|
# User-defined options
|
|
126
|
-
env = Taski.
|
|
127
|
-
debug = Taski.
|
|
207
|
+
env = Taski.args[:env]
|
|
208
|
+
debug = Taski.args.fetch(:debug, false)
|
|
128
209
|
|
|
129
210
|
# Runtime information
|
|
130
|
-
puts "Working directory: #{Taski.
|
|
131
|
-
puts "Started at: #{Taski.
|
|
132
|
-
puts "Root task: #{Taski.
|
|
211
|
+
puts "Working directory: #{Taski.args.working_directory}"
|
|
212
|
+
puts "Started at: #{Taski.args.started_at}"
|
|
213
|
+
puts "Root task: #{Taski.args.root_task}"
|
|
133
214
|
puts "Deploying to: #{env}"
|
|
134
215
|
end
|
|
135
216
|
end
|
|
136
217
|
|
|
137
218
|
# Pass options when running
|
|
138
|
-
DeployTask.run(
|
|
219
|
+
DeployTask.run(args: { env: "production", debug: true })
|
|
139
220
|
```
|
|
140
221
|
|
|
141
|
-
|
|
142
|
-
- `Taski.
|
|
143
|
-
- `Taski.
|
|
144
|
-
- `Taski.
|
|
145
|
-
- `Taski.
|
|
146
|
-
- `Taski.
|
|
147
|
-
- `Taski.
|
|
222
|
+
Args API:
|
|
223
|
+
- `Taski.args[:key]` - Get option value (nil if not set)
|
|
224
|
+
- `Taski.args.fetch(:key, default)` - Get with default value
|
|
225
|
+
- `Taski.args.key?(:key)` - Check if option exists
|
|
226
|
+
- `Taski.args.working_directory` - Execution directory
|
|
227
|
+
- `Taski.args.started_at` - Execution start time
|
|
228
|
+
- `Taski.args.root_task` - First task class called
|
|
148
229
|
|
|
149
|
-
###
|
|
230
|
+
### Execution Model
|
|
150
231
|
|
|
151
232
|
```ruby
|
|
152
|
-
#
|
|
233
|
+
# Each class method call creates fresh execution
|
|
153
234
|
RandomTask.value # => 42
|
|
154
|
-
RandomTask.value # =>
|
|
235
|
+
RandomTask.value # => 99 (different value - fresh execution)
|
|
155
236
|
|
|
156
|
-
#
|
|
157
|
-
RandomTask.new
|
|
237
|
+
# Instance-level caching
|
|
238
|
+
instance = RandomTask.new
|
|
239
|
+
instance.run # => 42
|
|
240
|
+
instance.run # => 42 (cached within instance)
|
|
241
|
+
instance.value # => 42
|
|
158
242
|
|
|
159
|
-
#
|
|
160
|
-
RandomTask
|
|
243
|
+
# Dependencies within same execution share results
|
|
244
|
+
DoubleConsumer.run # RandomTask runs once, both accesses get same value
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Aborting Execution
|
|
248
|
+
|
|
249
|
+
Stop all pending tasks when a critical error occurs:
|
|
250
|
+
|
|
251
|
+
```ruby
|
|
252
|
+
class CriticalTask < Taski::Task
|
|
253
|
+
def run
|
|
254
|
+
if fatal_error?
|
|
255
|
+
raise Taski::TaskAbortException, "Cannot continue"
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
161
259
|
```
|
|
162
260
|
|
|
261
|
+
When `TaskAbortException` is raised, no new tasks will start. Already running tasks will complete, then execution stops.
|
|
262
|
+
|
|
163
263
|
### Progress Display
|
|
164
264
|
|
|
165
265
|
Tree-based progress visualization is enabled by default:
|
|
@@ -172,6 +272,23 @@ WebServer (Task)
|
|
|
172
272
|
└── ◻ Server (Task)
|
|
173
273
|
```
|
|
174
274
|
|
|
275
|
+
**Simple mode** provides a compact single-line display:
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
⠹ [3/5] DeployTask | Uploading files...
|
|
279
|
+
✓ [5/5] All tasks completed (1234ms)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Configuration:**
|
|
283
|
+
|
|
284
|
+
```ruby
|
|
285
|
+
# Via API
|
|
286
|
+
Taski.progress_mode = :simple # or :tree (default)
|
|
287
|
+
|
|
288
|
+
# Via environment variable
|
|
289
|
+
TASKI_PROGRESS_MODE=simple ruby your_script.rb
|
|
290
|
+
```
|
|
291
|
+
|
|
175
292
|
To disable: `TASKI_PROGRESS_DISABLE=1 ruby your_script.rb`
|
|
176
293
|
|
|
177
294
|
### Tree Visualization
|
|
@@ -184,6 +301,36 @@ puts WebServer.tree
|
|
|
184
301
|
# └── Cache (Task)
|
|
185
302
|
```
|
|
186
303
|
|
|
304
|
+
## Testing
|
|
305
|
+
|
|
306
|
+
### Test Helper for Mocking Dependencies
|
|
307
|
+
|
|
308
|
+
Taski provides a test helper to mock task dependencies in your unit tests:
|
|
309
|
+
|
|
310
|
+
```ruby
|
|
311
|
+
require 'taski/test_helper/minitest'
|
|
312
|
+
|
|
313
|
+
class BuildReportTest < Minitest::Test
|
|
314
|
+
include Taski::TestHelper::Minitest
|
|
315
|
+
|
|
316
|
+
def test_builds_report
|
|
317
|
+
# Mock direct dependencies - their run methods won't execute
|
|
318
|
+
mock_task(FetchData, users: [1, 2, 3])
|
|
319
|
+
|
|
320
|
+
# Task under test uses mocked values
|
|
321
|
+
assert_equal 3, BuildReport.user_count
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Key features:**
|
|
327
|
+
- Mock only direct dependencies; indirect dependencies are automatically isolated
|
|
328
|
+
- Verify which dependencies were accessed with `assert_task_accessed` / `refute_task_accessed`
|
|
329
|
+
- Automatic cleanup after each test
|
|
330
|
+
- Supports both Minitest and RSpec
|
|
331
|
+
|
|
332
|
+
For RSpec, use `include Taski::TestHelper::RSpec` instead.
|
|
333
|
+
|
|
187
334
|
## Development
|
|
188
335
|
|
|
189
336
|
```bash
|
data/docs/GUIDE.md
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# Taski Guide
|
|
2
|
+
|
|
3
|
+
This guide provides detailed documentation beyond the basics covered in the README.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Error Handling](#error-handling)
|
|
8
|
+
- [Lifecycle Management](#lifecycle-management)
|
|
9
|
+
- [Progress Display](#progress-display)
|
|
10
|
+
- [Debugging](#debugging)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Error Handling
|
|
15
|
+
|
|
16
|
+
Taski provides comprehensive error handling for parallel task execution.
|
|
17
|
+
|
|
18
|
+
### Error Types
|
|
19
|
+
|
|
20
|
+
| Exception | Purpose |
|
|
21
|
+
|-----------|---------|
|
|
22
|
+
| `Taski::AggregateError` | Multiple tasks failed during parallel execution |
|
|
23
|
+
| `Taski::TaskError` | Base class for task-specific errors |
|
|
24
|
+
| `Taski::TaskAbortException` | Intentional abort (stops all tasks immediately) |
|
|
25
|
+
| `Taski::CircularDependencyError` | Circular dependency detected |
|
|
26
|
+
| `TaskClass::Error` | Auto-generated error class for each Task subclass |
|
|
27
|
+
|
|
28
|
+
### AggregateError
|
|
29
|
+
|
|
30
|
+
When multiple tasks fail during parallel execution, errors are collected into an `AggregateError`:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
class DatabaseTask < Taski::Task
|
|
34
|
+
exports :connection
|
|
35
|
+
def run
|
|
36
|
+
raise "Database connection failed"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class CacheTask < Taski::Task
|
|
41
|
+
exports :redis_client
|
|
42
|
+
def run
|
|
43
|
+
raise "Cache connection failed"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class AppTask < Taski::Task
|
|
48
|
+
def run
|
|
49
|
+
db = DatabaseTask.connection
|
|
50
|
+
cache = CacheTask.redis_client
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
begin
|
|
55
|
+
AppTask.run
|
|
56
|
+
rescue Taski::AggregateError => e
|
|
57
|
+
puts "#{e.errors.size} tasks failed:"
|
|
58
|
+
e.errors.each do |failure|
|
|
59
|
+
puts " - #{failure.task_class.name}: #{failure.error.message}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
# Output:
|
|
63
|
+
# 2 tasks failed:
|
|
64
|
+
# - DatabaseTask: Database connection failed
|
|
65
|
+
# - CacheTask: Cache connection failed
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Task-Specific Error Classes
|
|
69
|
+
|
|
70
|
+
Each Task subclass automatically gets an `::Error` class for targeted rescue:
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
class DatabaseTask < Taski::Task
|
|
74
|
+
exports :connection
|
|
75
|
+
def run
|
|
76
|
+
raise "Connection failed"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Rescue errors from a specific task
|
|
81
|
+
begin
|
|
82
|
+
AppTask.run
|
|
83
|
+
rescue DatabaseTask::Error => e
|
|
84
|
+
puts "Database task failed: #{e.message}"
|
|
85
|
+
# e.task_class returns DatabaseTask
|
|
86
|
+
# e.cause returns the original error
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
This works transparently with `AggregateError` - when you rescue `DatabaseTask::Error`, it matches an `AggregateError` that contains a `DatabaseTask::Error`.
|
|
91
|
+
|
|
92
|
+
### TaskAbortException
|
|
93
|
+
|
|
94
|
+
Use `TaskAbortException` to immediately stop all task execution:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
class CriticalTask < Taski::Task
|
|
98
|
+
def run
|
|
99
|
+
if critical_condition_met?
|
|
100
|
+
raise Taski::TaskAbortException, "Critical error - aborting"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`TaskAbortException` takes priority over regular errors. Already running tasks will complete, but no new tasks will start.
|
|
107
|
+
|
|
108
|
+
### Error Handling Best Practices
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
# 1. Handle errors within the task when recovery is possible
|
|
112
|
+
class ResilientTask < Taski::Task
|
|
113
|
+
exports :data
|
|
114
|
+
def run
|
|
115
|
+
@data = fetch_from_primary
|
|
116
|
+
rescue Timeout::Error
|
|
117
|
+
@data = fetch_from_fallback
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# 2. Use task-specific errors for clarity
|
|
122
|
+
begin
|
|
123
|
+
AppTask.run
|
|
124
|
+
rescue DatabaseTask::Error => e
|
|
125
|
+
handle_database_failure(e)
|
|
126
|
+
rescue CacheTask::Error => e
|
|
127
|
+
handle_cache_failure(e)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# 3. Fail fast with clear messages
|
|
131
|
+
class ValidatingTask < Taski::Task
|
|
132
|
+
def run
|
|
133
|
+
missing = %w[DATABASE_URL API_KEY].select { |v| ENV[v].nil? }
|
|
134
|
+
raise "Missing: #{missing.join(', ')}" if missing.any?
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Lifecycle Management
|
|
142
|
+
|
|
143
|
+
Taski supports resource cleanup with `run`, `clean`, and `run_and_clean` methods.
|
|
144
|
+
|
|
145
|
+
### Basic Lifecycle
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
class DatabaseSetup < Taski::Task
|
|
149
|
+
exports :connection
|
|
150
|
+
|
|
151
|
+
def run
|
|
152
|
+
@connection = "postgresql://localhost:5432/myapp"
|
|
153
|
+
puts "Database connected"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def clean
|
|
157
|
+
puts "Database disconnected"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
class WebServer < Taski::Task
|
|
162
|
+
def run
|
|
163
|
+
puts "Server started with #{DatabaseSetup.connection}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def clean
|
|
167
|
+
puts "Server stopped"
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Start
|
|
172
|
+
WebServer.run
|
|
173
|
+
# => Database connected
|
|
174
|
+
# => Server started with postgresql://localhost:5432/myapp
|
|
175
|
+
|
|
176
|
+
# Clean (reverse dependency order)
|
|
177
|
+
WebServer.clean
|
|
178
|
+
# => Server stopped
|
|
179
|
+
# => Database disconnected
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### run_and_clean
|
|
183
|
+
|
|
184
|
+
Execute run followed by clean in a single operation:
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
WebServer.run_and_clean
|
|
188
|
+
# => Database connected
|
|
189
|
+
# => Server started
|
|
190
|
+
# => Server stopped
|
|
191
|
+
# => Database disconnected
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Idempotent Clean Methods
|
|
195
|
+
|
|
196
|
+
Clean methods should be safe to call multiple times:
|
|
197
|
+
|
|
198
|
+
```ruby
|
|
199
|
+
class SafeFileTask < Taski::Task
|
|
200
|
+
exports :data_file
|
|
201
|
+
|
|
202
|
+
def run
|
|
203
|
+
@data_file = '/tmp/data.txt'
|
|
204
|
+
File.write(@data_file, 'data')
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def clean
|
|
208
|
+
# Check before delete
|
|
209
|
+
if @data_file && File.exist?(@data_file)
|
|
210
|
+
File.delete(@data_file)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Progress Display
|
|
219
|
+
|
|
220
|
+
Taski provides real-time progress visualization during task execution.
|
|
221
|
+
|
|
222
|
+
### Features
|
|
223
|
+
|
|
224
|
+
- **Spinner Animation**: Animated spinner during execution
|
|
225
|
+
- **Output Capture**: Real-time display of task output (last line)
|
|
226
|
+
- **Status Indicators**: Success/failure icons with execution time
|
|
227
|
+
- **Group Blocks**: Organize output messages into logical phases
|
|
228
|
+
- **TTY Detection**: Clean output when redirected to files
|
|
229
|
+
|
|
230
|
+
### Group Blocks
|
|
231
|
+
|
|
232
|
+
Use `group` blocks to organize output within a task into logical phases. The current group name is displayed alongside the task's output in the progress display.
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
class DeployTask < Taski::Task
|
|
236
|
+
def run
|
|
237
|
+
group("Preparing environment") do
|
|
238
|
+
puts "Checking dependencies..."
|
|
239
|
+
puts "Validating config..."
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
group("Building application") do
|
|
243
|
+
puts "Compiling source..."
|
|
244
|
+
puts "Running tests..."
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
group("Deploying") do
|
|
248
|
+
puts "Uploading files..."
|
|
249
|
+
puts "Restarting server..."
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Progress display output:
|
|
256
|
+
|
|
257
|
+
```text
|
|
258
|
+
During execution:
|
|
259
|
+
⠋ DeployTask (Task) | Deploying: Uploading files...
|
|
260
|
+
|
|
261
|
+
After completion:
|
|
262
|
+
✓ DeployTask (Task) 520ms
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The group name appears as a prefix to the output message: `| GroupName: output...`
|
|
266
|
+
|
|
267
|
+
Groups are useful for:
|
|
268
|
+
- **Logical organization**: Group related operations together
|
|
269
|
+
- **Progress visibility**: See which phase is currently executing
|
|
270
|
+
- **Error context**: Know which phase failed when errors occur
|
|
271
|
+
|
|
272
|
+
### Example Output
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
During execution:
|
|
276
|
+
WebServer (Task)
|
|
277
|
+
├── Config (Task) ...
|
|
278
|
+
│ ├── Database (Task) 45.2ms
|
|
279
|
+
│ └── Cache (Task) ...
|
|
280
|
+
└── Server (Task)
|
|
281
|
+
|
|
282
|
+
After completion:
|
|
283
|
+
WebServer (Task) 120.5ms
|
|
284
|
+
├── Config (Task) 50.3ms
|
|
285
|
+
│ ├── Database (Task) 45.2ms
|
|
286
|
+
│ └── Cache (Task) 48.1ms
|
|
287
|
+
└── Server (Task) 70.2ms
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Display Modes
|
|
291
|
+
|
|
292
|
+
Taski supports two progress display modes:
|
|
293
|
+
|
|
294
|
+
#### Tree Mode (Default)
|
|
295
|
+
|
|
296
|
+
Full dependency tree visualization with status for each task:
|
|
297
|
+
|
|
298
|
+
```
|
|
299
|
+
WebServer (Task)
|
|
300
|
+
├── ⠋ Config (Task) | Reading config.yml...
|
|
301
|
+
│ ├── ✅ Database (Task) 45.2ms
|
|
302
|
+
│ └── ⠙ Cache (Task) | Connecting...
|
|
303
|
+
└── ◻ Server (Task)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
#### Simple Mode
|
|
307
|
+
|
|
308
|
+
Compact single-line display showing current progress:
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
⠹ [3/5] DeployTask | Uploading files...
|
|
312
|
+
✓ [5/5] All tasks completed (1234ms)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Format: `[spinner] [completed/total] TaskName | last output...`
|
|
316
|
+
|
|
317
|
+
When multiple tasks run in parallel:
|
|
318
|
+
```
|
|
319
|
+
⠹ [2/5] DownloadLayer1, DownloadLayer2 | Downloading...
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
On failure:
|
|
323
|
+
```
|
|
324
|
+
✗ [3/5] DeployTask failed: Connection refused
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Configuring Progress Mode
|
|
328
|
+
|
|
329
|
+
**Via API:**
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
Taski.progress_mode = :simple # Use simple mode
|
|
333
|
+
Taski.progress_mode = :tree # Use tree mode (default)
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**Via environment variable:**
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
TASKI_PROGRESS_MODE=simple ruby your_script.rb
|
|
340
|
+
TASKI_PROGRESS_MODE=tree ruby your_script.rb
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Disabling Progress Display
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
TASKI_PROGRESS_DISABLE=1 ruby your_script.rb
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### File Output Mode
|
|
350
|
+
|
|
351
|
+
When output is redirected, interactive spinners are automatically disabled:
|
|
352
|
+
|
|
353
|
+
```bash
|
|
354
|
+
ruby build.rb > build.log 2>&1
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Debugging
|
|
360
|
+
|
|
361
|
+
### Environment Variables
|
|
362
|
+
|
|
363
|
+
| Variable | Purpose |
|
|
364
|
+
|----------|---------|
|
|
365
|
+
| `TASKI_PROGRESS_DISABLE=1` | Disable progress display |
|
|
366
|
+
| `TASKI_PROGRESS_MODE=simple\|tree` | Set progress display mode (default: tree) |
|
|
367
|
+
| `TASKI_DEBUG=1` | Enable debug output |
|
|
368
|
+
|
|
369
|
+
### Dependency Tree Visualization
|
|
370
|
+
|
|
371
|
+
```ruby
|
|
372
|
+
puts MyTask.tree
|
|
373
|
+
# MyTask (Task)
|
|
374
|
+
# ├── DatabaseTask (Task)
|
|
375
|
+
# └── CacheTask (Task)
|
|
376
|
+
# └── ConfigTask (Task)
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Common Issues
|
|
380
|
+
|
|
381
|
+
**Circular Dependencies**
|
|
382
|
+
|
|
383
|
+
```ruby
|
|
384
|
+
# Detected before execution
|
|
385
|
+
begin
|
|
386
|
+
TaskA.run
|
|
387
|
+
rescue Taski::CircularDependencyError => e
|
|
388
|
+
puts e.cyclic_tasks # [[TaskA, TaskB]]
|
|
389
|
+
end
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
**Static Analysis Requirements**
|
|
393
|
+
|
|
394
|
+
Tasks must be defined in source files (not dynamically with `Class.new`) because static analysis uses Prism AST parsing which requires actual source files.
|