Skip to content

Latest commit

 

History

History
432 lines (325 loc) · 11.1 KB

File metadata and controls

432 lines (325 loc) · 11.1 KB

Taski Guide

This guide provides detailed documentation beyond the basics covered in the README.

Table of Contents


Error Handling

Taski provides comprehensive error handling for parallel task execution.

Error Types

Exception Purpose
Taski::AggregateError Multiple tasks failed during parallel execution
Taski::TaskError Base class for task-specific errors
Taski::TaskAbortException Intentional abort (stops all tasks immediately)
Taski::CircularDependencyError Circular dependency detected
TaskClass::Error Auto-generated error class for each Task subclass

AggregateError

When multiple tasks fail during parallel execution, errors are collected into an AggregateError:

class DatabaseTask < Taski::Task
  exports :connection
  def run
    raise "Database connection failed"
  end
end

class CacheTask < Taski::Task
  exports :redis_client
  def run
    raise "Cache connection failed"
  end
end

class AppTask < Taski::Task
  def run
    db = DatabaseTask.connection
    cache = CacheTask.redis_client
  end
end

begin
  AppTask.run
rescue Taski::AggregateError => e
  puts "#{e.errors.size} tasks failed:"
  e.errors.each do |failure|
    puts "  - #{failure.task_class.name}: #{failure.error.message}"
  end
end
# Output:
# 2 tasks failed:
#   - DatabaseTask: Database connection failed
#   - CacheTask: Cache connection failed

Task-Specific Error Classes

Each Task subclass automatically gets an ::Error class for targeted rescue:

class DatabaseTask < Taski::Task
  exports :connection
  def run
    raise "Connection failed"
  end
end

# Rescue errors from a specific task
begin
  AppTask.run
rescue DatabaseTask::Error => e
  puts "Database task failed: #{e.message}"
  # e.task_class returns DatabaseTask
  # e.cause returns the original error
end

This works transparently with AggregateError — when you rescue DatabaseTask::Error, it matches an AggregateError that contains a DatabaseTask::Error.

How this works: Ruby's rescue uses the === operator to match exceptions. Taski's AggregateAware module (extended by TaskError and all TaskClass::Error classes) overrides === to check whether an AggregateError contains an error of that type. This means rescue Taski::TaskError will match an AggregateError wrapping TaskError instances, even though AggregateError does not inherit from TaskError.

TaskAbortException

Use TaskAbortException to immediately stop all task execution:

class CriticalTask < Taski::Task
  def run
    if critical_condition_met?
      raise Taski::TaskAbortException, "Critical error - aborting"
    end
  end
end

TaskAbortException takes priority over regular errors. Already running tasks will complete, but no new tasks will start.

Error Handling Best Practices

# 1. Handle errors within the task when recovery is possible
class ResilientTask < Taski::Task
  exports :data
  def run
    @data = fetch_from_primary
  rescue Timeout::Error
    @data = fetch_from_fallback
  end
end

# 2. Use task-specific errors for clarity
begin
  AppTask.run
rescue DatabaseTask::Error => e
  handle_database_failure(e)
rescue CacheTask::Error => e
  handle_cache_failure(e)
end

# 3. Fail fast with clear messages
class ValidatingTask < Taski::Task
  def run
    missing = %w[DATABASE_URL API_KEY].select { |v| ENV[v].nil? }
    raise "Missing: #{missing.join(', ')}" if missing.any?
  end
end

Lifecycle Management

Taski supports resource cleanup with run_and_clean, which executes the run phase followed by the clean phase in a single operation.

Basic Lifecycle

class DatabaseSetup < Taski::Task
  exports :connection

  def run
    @connection = "postgresql://localhost:5432/myapp"
    puts "Database connected"
  end

  def clean
    puts "Database disconnected"
  end
end

class WebServer < Taski::Task
  def run
    puts "Server started with #{DatabaseSetup.connection}"
  end

  def clean
    puts "Server stopped"
  end
end

# Run then clean in one call
WebServer.run_and_clean
# => Database connected
# => Server started
# => Server stopped
# => Database disconnected

run_and_clean with Block

Use a block to execute code between run and clean phases. This is useful when you need to use exported values before cleanup:

DatabaseSetup.run_and_clean do
  # Exported values are accessible here
  deploy(DatabaseSetup.connection)
end
# Clean runs automatically after the block

Idempotent Clean Methods

Clean methods should be safe to call multiple times:

class SafeFileTask < Taski::Task
  exports :data_file

  def run
    @data_file = '/tmp/data.txt'
    File.write(@data_file, 'data')
  end

  def clean
    # Check before delete
    if @data_file && File.exist?(@data_file)
      File.delete(@data_file)
    end
  end
end

Progress Display

Taski provides real-time progress visualization during task execution.

Features

  • Spinner Animation: Animated spinner during execution
  • Output Capture: Real-time display of task output (last line)
  • Status Indicators: Success/failure icons with execution time
  • Group Blocks: Organize output messages into logical phases
  • TTY Detection: Clean output when redirected to files

Group Blocks

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.

class DeployTask < Taski::Task
  def run
    group("Preparing environment") do
      puts "Checking dependencies..."
      puts "Validating config..."
    end

    group("Building application") do
      puts "Compiling source..."
      puts "Running tests..."
    end

    group("Deploying") do
      puts "Uploading files..."
      puts "Restarting server..."
    end
  end
end

Progress display output:

During execution:
⠋ DeployTask (Task) | Deploying: Uploading files...

After completion:
✓ DeployTask (Task) 520ms

The group name appears as a prefix to the output message: | GroupName: output...

Groups are useful for:

  • Logical organization: Group related operations together
  • Progress visibility: See which phase is currently executing
  • Error context: Know which phase failed when errors occur

Example Output

During execution:
  WebServer (Task)
  ├── Config (Task) ...
  │   ├── Database (Task) 45.2ms
  │   └── Cache (Task) ...
  └── Server (Task)

After completion:
  WebServer (Task) 120.5ms
  ├── Config (Task) 50.3ms
  │   ├── Database (Task) 45.2ms
  │   └── Cache (Task) 48.1ms
  └── Server (Task) 70.2ms

Display Modes

Taski supports three progress display modes:

Simple Mode (Default)

Compact single-line display showing current progress:

⠹ [3/5] DeployTask | Uploading files...
✓ [5/5] All tasks completed (1234ms)

Format: [spinner] [completed/total] TaskName | last output...

When multiple tasks run in parallel:

⠹ [2/5] DownloadLayer1, DownloadLayer2 | Downloading...

On failure:

✗ [3/5] DeployTask failed: Connection refused

Tree Mode

Full dependency tree visualization with status for each task:

WebServer (Task)
├── ⠋ Config (Task) | Reading config.yml...
│   ├── ✅ Database (Task) 45.2ms
│   └── ⠙ Cache (Task) | Connecting...
└── ◻ Server (Task)

Log Mode

Plain text output without escape codes, designed for CI/logs:

[START] DatabaseSetup
[DONE] DatabaseSetup (45.2ms)
[START] WebServer
[DONE] WebServer (120.5ms)
[TASKI] Completed: 2/2 tasks (165ms)

Configuring Progress Display

Taski.progress_display = Taski::Progress::Layout::Simple.new  # Simple display (default)
Taski.progress_display = Taski::Progress::Layout::Tree.new     # Tree display
Taski.progress_display = Taski::Progress::Layout::Log.new      # Log output (CI/logs)
Taski.progress_display = nil                                    # Disable

File Output Mode

When output is redirected, interactive spinners are automatically disabled:

ruby build.rb > build.log 2>&1

Lazy Dependency Resolution

How It Works

When a task accesses a dependency's exported value (e.g., DepTask.value), Taski may return a lightweight proxy object instead of the actual value. This proxy defers dependency resolution until you call a method on it, at which point it transparently resolves the real value and forwards the method call.

class FetchData < Taski::Task
  exports :data
  def run
    @data = expensive_api_call
  end
end

class ProcessData < Taski::Task
  exports :result
  def run
    raw = FetchData.data       # May return a proxy (no blocking yet)
    setup_environment          # Task continues while FetchData runs
    @result = raw.transform    # Proxy resolves here — blocks if needed
  end
end

From the user's perspective, the proxy is completely transparent — it behaves exactly like the real value.

Why It Matters

Proxy-based resolution enables better parallelism. A task can continue executing setup logic while its dependencies are still running, only blocking when the dependency value is actually used. This can significantly reduce total execution time when tasks have independent setup work before they need their dependencies.

Automatic Safety

Taski uses static analysis (Prism AST parsing) to determine when proxy resolution is safe. Dependencies used in positions where the proxy could cause issues — such as conditions (if dep_value), method arguments, or other contexts where truthiness or identity matters — are automatically resolved synchronously instead of returning a proxy.

You do not need to think about this in normal usage. The static analyzer examines your task's run method and only enables proxy resolution for dependency accesses that are confirmed safe (e.g., simple assignments like x = Dep.value followed by method calls on x).


Debugging

Structured Logging

require "logger"
Taski.logger = Logger.new($stdout, level: Logger::DEBUG)

Set Taski.logger to a Ruby Logger instance to enable structured logging of execution events.

Dependency Tree Visualization

puts MyTask.tree
# MyTask (Task)
# ├── DatabaseTask (Task)
# └── CacheTask (Task)
#     └── ConfigTask (Task)

Common Issues

Circular Dependencies

# Detected before execution
begin
  TaskA.run
rescue Taski::CircularDependencyError => e
  puts e.cyclic_tasks  # [[TaskA, TaskB]]
end

Static Analysis Requirements

Tasks must be defined in source files (not dynamically with Class.new) because static analysis uses Prism AST parsing which requires actual source files. Static analysis is used for dependency tree visualization, circular dependency detection, and optimizing dependency resolution (determining when lazy proxy resolution is safe vs. when synchronous resolution is required).