taski 0.3.1 → 0.4.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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -271
  3. data/Steepfile +19 -0
  4. data/docs/advanced-features.md +625 -0
  5. data/docs/api-guide.md +509 -0
  6. data/docs/error-handling.md +684 -0
  7. data/examples/README.md +98 -42
  8. data/examples/context_demo.rb +118 -0
  9. data/examples/data_pipeline_demo.rb +231 -0
  10. data/examples/parallel_progress_demo.rb +72 -0
  11. data/examples/quick_start.rb +4 -4
  12. data/examples/reexecution_demo.rb +127 -0
  13. data/examples/{section_configuration.rb → section_demo.rb} +49 -60
  14. data/lib/taski/context.rb +50 -0
  15. data/lib/taski/execution/coordinator.rb +63 -0
  16. data/lib/taski/execution/parallel_progress_display.rb +201 -0
  17. data/lib/taski/execution/registry.rb +72 -0
  18. data/lib/taski/execution/task_wrapper.rb +255 -0
  19. data/lib/taski/section.rb +26 -254
  20. data/lib/taski/static_analysis/analyzer.rb +46 -0
  21. data/lib/taski/static_analysis/dependency_graph.rb +90 -0
  22. data/lib/taski/static_analysis/visitor.rb +130 -0
  23. data/lib/taski/task.rb +199 -0
  24. data/lib/taski/version.rb +1 -1
  25. data/lib/taski.rb +68 -39
  26. data/rbs_collection.lock.yaml +116 -0
  27. data/rbs_collection.yaml +19 -0
  28. data/sig/taski.rbs +269 -62
  29. metadata +36 -32
  30. data/examples/advanced_patterns.rb +0 -119
  31. data/examples/progress_demo.rb +0 -166
  32. data/examples/tree_demo.rb +0 -205
  33. data/lib/taski/dependency_analyzer.rb +0 -232
  34. data/lib/taski/exceptions.rb +0 -17
  35. data/lib/taski/logger.rb +0 -158
  36. data/lib/taski/logging/formatter_factory.rb +0 -34
  37. data/lib/taski/logging/formatter_interface.rb +0 -19
  38. data/lib/taski/logging/json_formatter.rb +0 -26
  39. data/lib/taski/logging/simple_formatter.rb +0 -16
  40. data/lib/taski/logging/structured_formatter.rb +0 -44
  41. data/lib/taski/progress/display_colors.rb +0 -17
  42. data/lib/taski/progress/display_manager.rb +0 -117
  43. data/lib/taski/progress/output_capture.rb +0 -105
  44. data/lib/taski/progress/spinner_animation.rb +0 -49
  45. data/lib/taski/progress/task_formatter.rb +0 -25
  46. data/lib/taski/progress/task_status.rb +0 -38
  47. data/lib/taski/progress/terminal_controller.rb +0 -35
  48. data/lib/taski/progress_display.rb +0 -57
  49. data/lib/taski/reference.rb +0 -40
  50. data/lib/taski/task/base.rb +0 -91
  51. data/lib/taski/task/define_api.rb +0 -156
  52. data/lib/taski/task/dependency_resolver.rb +0 -73
  53. data/lib/taski/task/exports_api.rb +0 -29
  54. data/lib/taski/task/instance_management.rb +0 -201
  55. data/lib/taski/tree_colors.rb +0 -91
  56. data/lib/taski/utils/dependency_resolver_helper.rb +0 -85
  57. data/lib/taski/utils/tree_display_helper.rb +0 -68
  58. 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.