Azoth Scheduler
A cron-like task scheduling system for Azoth projects, built on event-sourcing principles.
Features
-
Flexible Scheduling
- Cron expressions for complex recurring tasks
- Simple interval-based scheduling
- One-time execution at specific timestamps
- Immediate execution (job queue functionality)
-
Robust Execution
- Configurable concurrency limits
- Automatic retries with exponential backoff
- Timeout support per task
- Task cancellation
-
Event-Sourced
- All schedule operations are events in the canonical log
- Task executions produce events for EventHandlers
- Full audit trail of all task activity
- Durable state across restarts
Architecture
┌─────────────────────┐
│ Schedule Events │ TaskScheduled, TaskExecuted, TaskCancelled
│ (Canonical Store) │ Written via Transaction API
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Scheduler Loop │ Continuously checks for due tasks
│ (like Projector) │ - Polls schedule projection
└──────────┬──────────┘ - Executes via TaskHandler
│ - Writes result events
▼
┌─────────────────────┐
│ Schedule Projection │ SQLite table with next_run_time index
│ (SQLite) │ Tracks task state, retries, history
└─────────────────────┘
│
▼
┌─────────────────────┐
│ TaskHandler │ Produces event for EventHandler
│ (User Code) │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ EventHandler │ Processes task result event
│ (User Code) │ Updates projections, side effects
└─────────────────────┘
Usage
Basic Example
use AzothDb;
use *;
use Arc;
use Duration;
// Define a task handler
;
async
Schedule Types
Cron Expression
scheduler.schedule_task?;
Cron format: second minute hour day_of_month month day_of_week (6-field with seconds)
0 0 0 * * *- Daily at midnight0 0 */6 * * *- Every 6 hours0 0 9 * * 1-5- Weekdays at 9 AM*/30 * * * * *- Every 30 seconds
Interval
scheduler.schedule_task?;
One-Time
let run_at = now.timestamp + 3600; // 1 hour from now
scheduler.schedule_task?;
Immediate
scheduler.schedule_task?;
Task Handlers
Task handlers execute the actual work and produce events:
;
Configuration
let scheduler = builder
.with_task_handler
.with_poll_interval // How often to check for due tasks
.with_max_concurrent_tasks // Max parallel executions
.with_default_max_retries // Retries on failure
.with_default_timeout_secs // 5 minute timeout
.build?;
Task Management
// Cancel a task
scheduler.cancel_task?;
// Get task info
let task = scheduler.get_task?;
// List all tasks
let tasks = scheduler.list_tasks?;
// List only enabled tasks
let tasks = scheduler.list_tasks?;
Retry Behavior
When a task fails:
- The failure is recorded with a
TaskExecutedevent (success: false) - The task's retry count is incremented
- If retry count < max retries:
- Task is rescheduled with exponential backoff (1min, 2min, 4min, etc.)
- If retry count >= max retries:
- Task is disabled
- Check execution history to diagnose
Examples
Run the included examples:
# Basic scheduler with immediate and interval tasks
# Cron-based scheduling
Testing
Design Rationale
Why event-triggered tasks?
- Fully event-sourced (all executions in event log)
- Testable and debuggable (inspect events)
- Distributed processing (handlers can run anywhere)
- Consistent with Azoth's architecture
Why projection-based state?
- Fast queries by next_run_time
- SQL queries for task management
- Durable across restarts
Why poll-based timing?
- Simple implementation
- Works with multiple schedulers (future feature)
- Minimum latency = poll interval (acceptable at 1 second)