taski 0.3.1 → 0.4.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/.gem_rbs_collection/ast/2.4/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/ast/2.4/ast.rbs +73 -0
- data/.gem_rbs_collection/minitest/5.25/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/abstract_reporter.rbs +52 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/assertion.rbs +17 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/assertions.rbs +590 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/backtrace_filter.rbs +23 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/bench_spec.rbs +102 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/benchmark.rbs +259 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/composite_reporter.rbs +25 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/compress.rbs +13 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/error_on_warning.rbs +3 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/expectation.rbs +2 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/expectations.rbs +21 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/guard.rbs +64 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/mock.rbs +64 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/parallel/executor.rbs +46 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/parallel/test/class_methods.rbs +5 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/parallel/test.rbs +3 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/parallel.rbs +2 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/pride_io.rbs +62 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/pride_lol.rbs +19 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/progress_reporter.rbs +11 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/reportable.rbs +53 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/reporter.rbs +5 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/result.rbs +28 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/runnable.rbs +163 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/skip.rbs +6 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/spec/dsl/instance_methods.rbs +48 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/spec/dsl.rbs +129 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/spec.rbs +11 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/statistics_reporter.rbs +81 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/summary_reporter.rbs +18 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/test/lifecycle_hooks.rbs +92 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/test.rbs +69 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/unexpected_error.rbs +12 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/unexpected_warning.rbs +6 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/unit/test_case.rbs +3 -0
- data/.gem_rbs_collection/minitest/5.25/minitest/unit.rbs +4 -0
- data/.gem_rbs_collection/minitest/5.25/minitest.rbs +115 -0
- data/.gem_rbs_collection/parallel/1.20/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/parallel/1.20/parallel.rbs +86 -0
- data/.gem_rbs_collection/parser/3.2/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/parser/3.2/manifest.yaml +7 -0
- data/.gem_rbs_collection/parser/3.2/parser.rbs +193 -0
- data/.gem_rbs_collection/parser/3.2/polyfill.rbs +4 -0
- data/.gem_rbs_collection/rainbow/3.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rainbow/3.0/global.rbs +7 -0
- data/.gem_rbs_collection/rainbow/3.0/presenter.rbs +209 -0
- data/.gem_rbs_collection/rainbow/3.0/rainbow.rbs +5 -0
- data/.gem_rbs_collection/rake/13.0/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rake/13.0/manifest.yaml +2 -0
- data/.gem_rbs_collection/rake/13.0/rake.rbs +39 -0
- data/.gem_rbs_collection/regexp_parser/2.8/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/regexp_parser/2.8/regexp_parser.rbs +17 -0
- data/.gem_rbs_collection/rubocop/1.57/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rubocop/1.57/rubocop.rbs +129 -0
- data/.gem_rbs_collection/rubocop-ast/1.30/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/rubocop-ast/1.30/rubocop-ast.rbs +771 -0
- data/.gem_rbs_collection/simplecov/0.22/.rbs_meta.yaml +9 -0
- data/.gem_rbs_collection/simplecov/0.22/simplecov.rbs +54 -0
- data/README.md +138 -247
- data/Steepfile +19 -0
- data/docs/advanced-features.md +625 -0
- data/docs/api-guide.md +509 -0
- data/docs/error-handling.md +684 -0
- data/examples/README.md +95 -42
- data/examples/context_demo.rb +112 -0
- data/examples/data_pipeline_demo.rb +231 -0
- data/examples/parallel_progress_demo.rb +72 -0
- data/examples/quick_start.rb +4 -4
- data/examples/reexecution_demo.rb +127 -0
- data/examples/{section_configuration.rb → section_demo.rb} +49 -60
- data/lib/taski/context.rb +52 -0
- data/lib/taski/execution/coordinator.rb +63 -0
- data/lib/taski/execution/parallel_progress_display.rb +201 -0
- data/lib/taski/execution/registry.rb +72 -0
- data/lib/taski/execution/task_wrapper.rb +255 -0
- data/lib/taski/section.rb +26 -254
- data/lib/taski/static_analysis/analyzer.rb +34 -0
- data/lib/taski/static_analysis/dependency_graph.rb +90 -0
- data/lib/taski/static_analysis/visitor.rb +114 -0
- data/lib/taski/task.rb +173 -0
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +45 -39
- data/rbs_collection.lock.yaml +116 -0
- data/rbs_collection.yaml +19 -0
- data/sig/taski.rbs +269 -62
- metadata +97 -32
- data/examples/advanced_patterns.rb +0 -119
- data/examples/progress_demo.rb +0 -166
- data/examples/tree_demo.rb +0 -205
- data/lib/taski/dependency_analyzer.rb +0 -232
- data/lib/taski/exceptions.rb +0 -17
- data/lib/taski/logger.rb +0 -158
- data/lib/taski/logging/formatter_factory.rb +0 -34
- data/lib/taski/logging/formatter_interface.rb +0 -19
- data/lib/taski/logging/json_formatter.rb +0 -26
- data/lib/taski/logging/simple_formatter.rb +0 -16
- data/lib/taski/logging/structured_formatter.rb +0 -44
- data/lib/taski/progress/display_colors.rb +0 -17
- data/lib/taski/progress/display_manager.rb +0 -117
- data/lib/taski/progress/output_capture.rb +0 -105
- data/lib/taski/progress/spinner_animation.rb +0 -49
- data/lib/taski/progress/task_formatter.rb +0 -25
- data/lib/taski/progress/task_status.rb +0 -38
- data/lib/taski/progress/terminal_controller.rb +0 -35
- data/lib/taski/progress_display.rb +0 -57
- data/lib/taski/reference.rb +0 -40
- data/lib/taski/task/base.rb +0 -91
- data/lib/taski/task/define_api.rb +0 -156
- data/lib/taski/task/dependency_resolver.rb +0 -73
- data/lib/taski/task/exports_api.rb +0 -29
- data/lib/taski/task/instance_management.rb +0 -201
- data/lib/taski/tree_colors.rb +0 -91
- data/lib/taski/utils/dependency_resolver_helper.rb +0 -85
- data/lib/taski/utils/tree_display_helper.rb +0 -68
- data/lib/taski/utils.rb +0 -107
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
# Error Handling Guide
|
|
2
|
+
|
|
3
|
+
This guide covers Taski's comprehensive error handling capabilities including circular dependency detection, task build errors, and recovery strategies.
|
|
4
|
+
|
|
5
|
+
## Error Types
|
|
6
|
+
|
|
7
|
+
Taski provides specific exception types for different error scenarios:
|
|
8
|
+
|
|
9
|
+
- `Taski::CircularDependencyError`: Circular dependency detected
|
|
10
|
+
- `Taski::TaskBuildError`: Task execution failed
|
|
11
|
+
- `Taski::TaskAnalysisError`: Static analysis failed
|
|
12
|
+
- `Taski::SectionImplementationError`: Section implementation problems
|
|
13
|
+
- `Taski::TaskInterruptedException`: Task interrupted by signal
|
|
14
|
+
|
|
15
|
+
## Circular Dependency Detection
|
|
16
|
+
|
|
17
|
+
Taski automatically detects circular dependencies and provides detailed error messages.
|
|
18
|
+
|
|
19
|
+
### Simple Circular Dependency
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
class TaskA < Taski::Task
|
|
23
|
+
exports :value_a
|
|
24
|
+
|
|
25
|
+
def run
|
|
26
|
+
@value_a = TaskB.value_b # TaskA depends on TaskB
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class TaskB < Taski::Task
|
|
31
|
+
exports :value_b
|
|
32
|
+
|
|
33
|
+
def run
|
|
34
|
+
@value_b = TaskA.value_a # TaskB depends on TaskA - CIRCULAR!
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
begin
|
|
39
|
+
TaskA.run
|
|
40
|
+
rescue Taski::CircularDependencyError => e
|
|
41
|
+
puts "Error: #{e.message}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Output:
|
|
45
|
+
# Error: Circular dependency detected!
|
|
46
|
+
# Cycle: TaskA → TaskB → TaskA
|
|
47
|
+
#
|
|
48
|
+
# The dependency chain is:
|
|
49
|
+
# 1. TaskA is trying to build → TaskB
|
|
50
|
+
# 2. TaskB is trying to build → TaskA
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Complex Circular Dependencies
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
class DatabaseConfig < Taski::Task
|
|
57
|
+
exports :connection_string
|
|
58
|
+
|
|
59
|
+
def run
|
|
60
|
+
# This creates a complex circular dependency
|
|
61
|
+
@connection_string = "postgresql://#{ServerConfig.host}/#{AppConfig.database_name}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class ServerConfig < Taski::Task
|
|
66
|
+
exports :host
|
|
67
|
+
|
|
68
|
+
def run
|
|
69
|
+
@host = AppConfig.production? ? "prod.example.com" : "localhost"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class AppConfig < Taski::Task
|
|
74
|
+
exports :database_name, :production
|
|
75
|
+
|
|
76
|
+
def run
|
|
77
|
+
@database_name = "myapp_#{DatabaseConfig.connection_string.split('/').last}"
|
|
78
|
+
@production = ENV['RAILS_ENV'] == 'production'
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
begin
|
|
83
|
+
DatabaseConfig.run
|
|
84
|
+
rescue Taski::CircularDependencyError => e
|
|
85
|
+
puts "Complex circular dependency detected:"
|
|
86
|
+
puts e.message
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Output:
|
|
90
|
+
# Complex circular dependency detected:
|
|
91
|
+
# Circular dependency detected!
|
|
92
|
+
# Cycle: DatabaseConfig → AppConfig → DatabaseConfig
|
|
93
|
+
#
|
|
94
|
+
# The dependency chain is:
|
|
95
|
+
# 1. DatabaseConfig is trying to build → AppConfig
|
|
96
|
+
# 2. AppConfig is trying to build → DatabaseConfig
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Avoiding Circular Dependencies
|
|
100
|
+
|
|
101
|
+
```ruby
|
|
102
|
+
# ❌ Bad: Circular dependency
|
|
103
|
+
class BadConfigA < Taski::Task
|
|
104
|
+
exports :value
|
|
105
|
+
def run; @value = BadConfigB.other_value; end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
class BadConfigB < Taski::Task
|
|
109
|
+
exports :other_value
|
|
110
|
+
def run; @other_value = BadConfigA.value; end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# ✅ Good: Hierarchical dependencies
|
|
114
|
+
class BaseConfig < Taski::Task
|
|
115
|
+
exports :environment, :base_url
|
|
116
|
+
|
|
117
|
+
def run
|
|
118
|
+
@environment = ENV['RAILS_ENV'] || 'development'
|
|
119
|
+
@base_url = @environment == 'production' ? 'https://api.example.com' : 'http://localhost:3000'
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
class DatabaseConfig < Taski::Task
|
|
124
|
+
exports :connection_string
|
|
125
|
+
|
|
126
|
+
def run
|
|
127
|
+
db_name = BaseConfig.environment == 'production' ? 'myapp_prod' : 'myapp_dev'
|
|
128
|
+
@connection_string = "postgresql://localhost/#{db_name}"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
class ApiConfig < Taski::Task
|
|
133
|
+
exports :endpoint
|
|
134
|
+
|
|
135
|
+
def run
|
|
136
|
+
@endpoint = "#{BaseConfig.base_url}/api/v1"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Task Build Errors
|
|
142
|
+
|
|
143
|
+
When task execution fails, Taski wraps the error in a `TaskBuildError` with detailed context.
|
|
144
|
+
|
|
145
|
+
### Basic Error Handling
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
class FailingTask < Taski::Task
|
|
149
|
+
exports :result
|
|
150
|
+
|
|
151
|
+
def run
|
|
152
|
+
raise "Something went wrong!"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
class DependentTask < Taski::Task
|
|
157
|
+
def run
|
|
158
|
+
puts "Using result: #{FailingTask.result}"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
begin
|
|
163
|
+
DependentTask.run
|
|
164
|
+
rescue Taski::TaskBuildError => e
|
|
165
|
+
puts "Task build failed: #{e.message}"
|
|
166
|
+
puts "Original error: #{e.cause.message}"
|
|
167
|
+
puts "Failed task: #{e.task_class}"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Output:
|
|
171
|
+
# Task build failed: Failed to build task FailingTask: Something went wrong!
|
|
172
|
+
# Original error: Something went wrong!
|
|
173
|
+
# Failed task: FailingTask
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Error Propagation
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
class DatabaseTask < Taski::Task
|
|
180
|
+
exports :connection
|
|
181
|
+
|
|
182
|
+
def run
|
|
183
|
+
raise "Database connection failed"
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
class CacheTask < Taski::Task
|
|
188
|
+
exports :redis_client
|
|
189
|
+
|
|
190
|
+
def run
|
|
191
|
+
@redis_client = "redis://localhost:6379"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
class ApplicationTask < Taski::Task
|
|
196
|
+
def run
|
|
197
|
+
# Both dependencies required
|
|
198
|
+
db = DatabaseTask.connection
|
|
199
|
+
cache = CacheTask.redis_client
|
|
200
|
+
puts "App started with DB: #{db}, Cache: #{cache}"
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
begin
|
|
205
|
+
ApplicationTask.run
|
|
206
|
+
rescue Taski::TaskBuildError => e
|
|
207
|
+
puts "Application failed to start:"
|
|
208
|
+
puts " Failed task: #{e.task_class}"
|
|
209
|
+
puts " Error: #{e.cause.message}"
|
|
210
|
+
|
|
211
|
+
# Chain of errors is preserved
|
|
212
|
+
current = e
|
|
213
|
+
while current.cause
|
|
214
|
+
current = current.cause
|
|
215
|
+
puts " Caused by: #{current.message}" if current.respond_to?(:message)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Output:
|
|
220
|
+
# Application failed to start:
|
|
221
|
+
# Failed task: DatabaseTask
|
|
222
|
+
# Error: Database connection failed
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## Dependency Error Recovery
|
|
226
|
+
|
|
227
|
+
Taski provides powerful error recovery mechanisms using `rescue_deps`.
|
|
228
|
+
|
|
229
|
+
### Basic Error Recovery
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
class UnreliableService < Taski::Task
|
|
233
|
+
exports :data
|
|
234
|
+
|
|
235
|
+
def run
|
|
236
|
+
if ENV['SERVICE_DOWN'] == 'true'
|
|
237
|
+
raise "Service unavailable"
|
|
238
|
+
end
|
|
239
|
+
@data = { users: 100, orders: 50 }
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
class FallbackService < Taski::Task
|
|
244
|
+
exports :cached_data
|
|
245
|
+
|
|
246
|
+
def run
|
|
247
|
+
@cached_data = { users: 90, orders: 45 } # Slightly stale data
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
class ReliableConsumer < Taski::Task
|
|
252
|
+
# Rescue any StandardError from dependencies
|
|
253
|
+
rescue_deps StandardError, -> { FallbackService.cached_data }
|
|
254
|
+
|
|
255
|
+
def run
|
|
256
|
+
data = UnreliableService.data
|
|
257
|
+
puts "Processing data: #{data}"
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Test normal operation
|
|
262
|
+
ENV['SERVICE_DOWN'] = 'false'
|
|
263
|
+
ReliableConsumer.run
|
|
264
|
+
# => Processing data: {users: 100, orders: 50}
|
|
265
|
+
|
|
266
|
+
# Test fallback
|
|
267
|
+
ENV['SERVICE_DOWN'] = 'true'
|
|
268
|
+
ReliableConsumer.reset!
|
|
269
|
+
ReliableConsumer.run
|
|
270
|
+
# => Processing data: {users: 90, orders: 45}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Multiple Rescue Strategies
|
|
274
|
+
|
|
275
|
+
```ruby
|
|
276
|
+
class PrimaryAPI < Taski::Task
|
|
277
|
+
exports :api_data
|
|
278
|
+
|
|
279
|
+
def run
|
|
280
|
+
raise "Primary API down" if ENV['PRIMARY_DOWN'] == 'true'
|
|
281
|
+
@api_data = { source: 'primary', data: 'fresh' }
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
class SecondaryAPI < Taski::Task
|
|
286
|
+
exports :backup_data
|
|
287
|
+
|
|
288
|
+
def run
|
|
289
|
+
raise "Secondary API down" if ENV['SECONDARY_DOWN'] == 'true'
|
|
290
|
+
@backup_data = { source: 'secondary', data: 'good' }
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
class LocalCache < Taski::Task
|
|
295
|
+
exports :cached_data
|
|
296
|
+
|
|
297
|
+
def run
|
|
298
|
+
raise "Cache corrupted" if ENV['CACHE_CORRUPTED'] == 'true'
|
|
299
|
+
@cached_data = { source: 'cache', data: 'stale' }
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
class ResilientDataProcessor < Taski::Task
|
|
304
|
+
# Try primary API first
|
|
305
|
+
rescue_deps StandardError, -> { SecondaryAPI.backup_data }
|
|
306
|
+
# If that fails, try local cache
|
|
307
|
+
rescue_deps StandardError, -> { LocalCache.cached_data }
|
|
308
|
+
# If all else fails, use static data
|
|
309
|
+
rescue_deps StandardError, -> { { source: 'static', data: 'default' } }
|
|
310
|
+
|
|
311
|
+
def run
|
|
312
|
+
data = PrimaryAPI.api_data
|
|
313
|
+
puts "Using data from #{data[:source]}: #{data[:data]}"
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Test various failure scenarios
|
|
318
|
+
ENV['PRIMARY_DOWN'] = 'true'
|
|
319
|
+
ResilientDataProcessor.run
|
|
320
|
+
# => Using data from secondary: good
|
|
321
|
+
|
|
322
|
+
ENV['SECONDARY_DOWN'] = 'true'
|
|
323
|
+
ResilientDataProcessor.reset!
|
|
324
|
+
ResilientDataProcessor.run
|
|
325
|
+
# => Using data from cache: stale
|
|
326
|
+
|
|
327
|
+
ENV['CACHE_CORRUPTED'] = 'true'
|
|
328
|
+
ResilientDataProcessor.reset!
|
|
329
|
+
ResilientDataProcessor.run
|
|
330
|
+
# => Using data from static: default
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Conditional Error Recovery
|
|
334
|
+
|
|
335
|
+
```ruby
|
|
336
|
+
class ExternalService < Taski::Task
|
|
337
|
+
exports :external_data
|
|
338
|
+
|
|
339
|
+
def run
|
|
340
|
+
case ENV['ERROR_TYPE']
|
|
341
|
+
when 'network'
|
|
342
|
+
raise SocketError, "Network unreachable"
|
|
343
|
+
when 'timeout'
|
|
344
|
+
raise Timeout::Error, "Request timed out"
|
|
345
|
+
when 'auth'
|
|
346
|
+
raise SecurityError, "Authentication failed"
|
|
347
|
+
else
|
|
348
|
+
@external_data = "external service data"
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
class SmartConsumer < Taski::Task
|
|
354
|
+
# Only rescue network and timeout errors, not auth errors
|
|
355
|
+
rescue_deps SocketError, Timeout::Error, -> { "fallback data" }
|
|
356
|
+
|
|
357
|
+
def run
|
|
358
|
+
data = ExternalService.external_data
|
|
359
|
+
puts "Using: #{data}"
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Network error - rescued
|
|
364
|
+
ENV['ERROR_TYPE'] = 'network'
|
|
365
|
+
SmartConsumer.run
|
|
366
|
+
# => Using: fallback data
|
|
367
|
+
|
|
368
|
+
# Auth error - not rescued, propagates up
|
|
369
|
+
ENV['ERROR_TYPE'] = 'auth'
|
|
370
|
+
begin
|
|
371
|
+
SmartConsumer.reset!
|
|
372
|
+
SmartConsumer.run
|
|
373
|
+
rescue Taski::TaskBuildError => e
|
|
374
|
+
puts "Auth error not handled: #{e.cause.message}"
|
|
375
|
+
end
|
|
376
|
+
# => Auth error not handled: Authentication failed
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
## Error Recovery Patterns
|
|
380
|
+
|
|
381
|
+
### Circuit Breaker Pattern
|
|
382
|
+
|
|
383
|
+
```ruby
|
|
384
|
+
class CircuitBreakerService < Taski::Task
|
|
385
|
+
exports :service_data
|
|
386
|
+
|
|
387
|
+
def run
|
|
388
|
+
failure_count = ENV['FAILURE_COUNT'].to_i
|
|
389
|
+
|
|
390
|
+
if failure_count >= 3
|
|
391
|
+
raise "Circuit breaker open - too many failures"
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
@service_data = "service response"
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
class CircuitBreakerConsumer < Taski::Task
|
|
399
|
+
rescue_deps StandardError, -> {
|
|
400
|
+
puts "Circuit breaker activated, using cached response"
|
|
401
|
+
"cached response"
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
def run
|
|
405
|
+
data = CircuitBreakerService.service_data
|
|
406
|
+
puts "Service response: #{data}"
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
ENV['FAILURE_COUNT'] = '5'
|
|
411
|
+
CircuitBreakerConsumer.run
|
|
412
|
+
# => Circuit breaker activated, using cached response
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
### Retry with Backoff
|
|
416
|
+
|
|
417
|
+
```ruby
|
|
418
|
+
class RetryableService < Taski::Task
|
|
419
|
+
exports :retry_data
|
|
420
|
+
|
|
421
|
+
def run
|
|
422
|
+
attempt = (ENV['ATTEMPT'] || '1').to_i
|
|
423
|
+
|
|
424
|
+
if attempt < 3
|
|
425
|
+
ENV['ATTEMPT'] = (attempt + 1).to_s
|
|
426
|
+
raise "Temporary failure (attempt #{attempt})"
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
@retry_data = "success on attempt #{attempt}"
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
class RetryingConsumer < Taski::Task
|
|
434
|
+
rescue_deps StandardError, -> {
|
|
435
|
+
puts "Retrying after failure..."
|
|
436
|
+
sleep(1) # Simple backoff
|
|
437
|
+
RetryableService.reset!
|
|
438
|
+
|
|
439
|
+
begin
|
|
440
|
+
RetryableService.retry_data
|
|
441
|
+
rescue => e
|
|
442
|
+
"final fallback after retries"
|
|
443
|
+
end
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
def run
|
|
447
|
+
data = RetryableService.retry_data
|
|
448
|
+
puts "Final result: #{data}"
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
ENV['ATTEMPT'] = '1'
|
|
453
|
+
RetryingConsumer.run
|
|
454
|
+
# => Retrying after failure...
|
|
455
|
+
# => Final result: success on attempt 3
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
## Static Analysis Errors
|
|
459
|
+
|
|
460
|
+
Taski performs static analysis to detect dependency issues early.
|
|
461
|
+
|
|
462
|
+
### Missing Dependencies
|
|
463
|
+
|
|
464
|
+
```ruby
|
|
465
|
+
class MissingDepTask < Taski::Task
|
|
466
|
+
exports :result
|
|
467
|
+
|
|
468
|
+
def run
|
|
469
|
+
# This will cause a NameError at class definition time
|
|
470
|
+
@result = UndefinedTask.some_value
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# Error occurs immediately when class is defined:
|
|
475
|
+
# NameError: uninitialized constant UndefinedTask
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### Invalid ref() Usage
|
|
479
|
+
|
|
480
|
+
```ruby
|
|
481
|
+
class InvalidRefTask < Taski::Task
|
|
482
|
+
define :invalid_ref, -> {
|
|
483
|
+
ref("NonExistentTask").value # Will fail at runtime
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
def run
|
|
487
|
+
puts invalid_ref
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
begin
|
|
492
|
+
InvalidRefTask.run
|
|
493
|
+
rescue => e
|
|
494
|
+
puts "Reference error: #{e.message}"
|
|
495
|
+
end
|
|
496
|
+
# => Reference error: uninitialized constant NonExistentTask
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
## Signal Interruption Handling
|
|
500
|
+
|
|
501
|
+
Handle task interruption gracefully with proper cleanup.
|
|
502
|
+
|
|
503
|
+
### Basic Signal Handling
|
|
504
|
+
|
|
505
|
+
```ruby
|
|
506
|
+
class InterruptibleTask < Taski::Task
|
|
507
|
+
def run
|
|
508
|
+
puts "Starting long operation..."
|
|
509
|
+
|
|
510
|
+
begin
|
|
511
|
+
long_running_operation
|
|
512
|
+
rescue Taski::TaskInterruptedException => e
|
|
513
|
+
puts "Task interrupted: #{e.message}"
|
|
514
|
+
perform_cleanup
|
|
515
|
+
raise # Re-raise to maintain proper error flow
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
puts "Operation completed"
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
private
|
|
522
|
+
|
|
523
|
+
def long_running_operation
|
|
524
|
+
50.times do |i|
|
|
525
|
+
puts "Step #{i + 1}/50"
|
|
526
|
+
sleep(0.2)
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def perform_cleanup
|
|
531
|
+
puts "Cleaning up resources..."
|
|
532
|
+
puts "Cleanup complete"
|
|
533
|
+
end
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Run with Ctrl+C to test interruption
|
|
537
|
+
InterruptibleTask.run
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### Nested Task Interruption
|
|
541
|
+
|
|
542
|
+
```ruby
|
|
543
|
+
class DatabaseMigration < Taski::Task
|
|
544
|
+
def run
|
|
545
|
+
puts "Starting migration..."
|
|
546
|
+
|
|
547
|
+
begin
|
|
548
|
+
migrate_schema
|
|
549
|
+
migrate_data
|
|
550
|
+
rescue Taski::TaskInterruptedException => e
|
|
551
|
+
puts "Migration interrupted, rolling back..."
|
|
552
|
+
rollback_changes
|
|
553
|
+
raise
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
puts "Migration completed"
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
private
|
|
560
|
+
|
|
561
|
+
def migrate_schema
|
|
562
|
+
puts "Migrating schema..."
|
|
563
|
+
sleep(2)
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def migrate_data
|
|
567
|
+
puts "Migrating data..."
|
|
568
|
+
sleep(3)
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def rollback_changes
|
|
572
|
+
puts "Rolling back migration changes..."
|
|
573
|
+
sleep(1)
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
## Debugging Strategies
|
|
579
|
+
|
|
580
|
+
### Comprehensive Error Logging
|
|
581
|
+
|
|
582
|
+
```ruby
|
|
583
|
+
class DiagnosticTask < Taski::Task
|
|
584
|
+
def run
|
|
585
|
+
begin
|
|
586
|
+
risky_operation
|
|
587
|
+
rescue => e
|
|
588
|
+
Taski.logger.error "Task failed",
|
|
589
|
+
task: self.class.name,
|
|
590
|
+
error_class: e.class.name,
|
|
591
|
+
error_message: e.message,
|
|
592
|
+
backtrace: e.backtrace.first(5)
|
|
593
|
+
raise
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
private
|
|
598
|
+
|
|
599
|
+
def risky_operation
|
|
600
|
+
raise "Diagnostic error for testing"
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
### Dependency Chain Analysis
|
|
606
|
+
|
|
607
|
+
```ruby
|
|
608
|
+
class DebugTask < Taski::Task
|
|
609
|
+
exports :debug_info
|
|
610
|
+
|
|
611
|
+
def run
|
|
612
|
+
puts "Dependency analysis:"
|
|
613
|
+
puts " Dependencies: #{self.class.dependencies.map(&:name)}"
|
|
614
|
+
puts " Dependency tree:"
|
|
615
|
+
puts self.class.tree.split("\n").map { |line| " #{line}" }
|
|
616
|
+
|
|
617
|
+
@debug_info = "debug complete"
|
|
618
|
+
end
|
|
619
|
+
end
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
## Best Practices
|
|
623
|
+
|
|
624
|
+
### 1. Fail Fast and Clearly
|
|
625
|
+
|
|
626
|
+
```ruby
|
|
627
|
+
# ✅ Good: Clear error messages
|
|
628
|
+
class ValidatingTask < Taski::Task
|
|
629
|
+
def run
|
|
630
|
+
validate_environment!
|
|
631
|
+
perform_work
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
private
|
|
635
|
+
|
|
636
|
+
def validate_environment!
|
|
637
|
+
required_vars = %w[DATABASE_URL API_KEY]
|
|
638
|
+
missing = required_vars.select { |var| ENV[var].nil? || ENV[var].empty? }
|
|
639
|
+
|
|
640
|
+
if missing.any?
|
|
641
|
+
raise "Missing required environment variables: #{missing.join(', ')}"
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
end
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### 2. Provide Meaningful Fallbacks
|
|
648
|
+
|
|
649
|
+
```ruby
|
|
650
|
+
# ✅ Good: Meaningful fallback with logging
|
|
651
|
+
class GracefulTask < Taski::Task
|
|
652
|
+
rescue_deps StandardError, -> {
|
|
653
|
+
Taski.logger.warn "Primary service failed, using fallback"
|
|
654
|
+
load_fallback_data
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
def self.load_fallback_data
|
|
658
|
+
{ status: 'degraded', message: 'Using cached data due to service outage' }
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
### 3. Test Error Scenarios
|
|
664
|
+
|
|
665
|
+
```ruby
|
|
666
|
+
# Test both success and failure paths
|
|
667
|
+
class TestableTask < Taski::Task
|
|
668
|
+
exports :result
|
|
669
|
+
|
|
670
|
+
def run
|
|
671
|
+
if ENV['SIMULATE_FAILURE'] == 'true'
|
|
672
|
+
raise "Simulated failure for testing"
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
@result = "success"
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
# In tests:
|
|
680
|
+
# ENV['SIMULATE_FAILURE'] = 'true'
|
|
681
|
+
# expect { TestableTask.run }.to raise_error(Taski::TaskBuildError)
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
This comprehensive error handling guide ensures your Taski applications are robust and maintainable in production environments.
|