|
| 1 | +# Cron Job Extension |
| 2 | + |
| 3 | +A production-grade cron job scheduler for Forge with distributed support, execution history, metrics, and web UI. |
| 4 | + |
| 5 | +## Features |
| 6 | + |
| 7 | +- ✅ **Flexible Scheduling**: Use standard cron expressions with seconds precision |
| 8 | +- ✅ **Multiple Job Types**: Code-based handlers or shell commands |
| 9 | +- ✅ **Execution History**: Track all job executions with detailed status |
| 10 | +- ✅ **Retry Logic**: Automatic retries with exponential backoff |
| 11 | +- ✅ **Concurrency Control**: Configurable worker pool for job execution |
| 12 | +- ✅ **Storage Backends**: In-memory, database, or Redis |
| 13 | +- ✅ **Distributed Mode**: Leader election and distributed locking (requires consensus extension) |
| 14 | +- ✅ **REST API**: Full HTTP API for job management |
| 15 | +- ✅ **Metrics**: Prometheus metrics for observability |
| 16 | +- ✅ **Config-Based Jobs**: Define jobs in YAML/JSON files |
| 17 | +- ✅ **Web UI**: Dashboard for job management and monitoring |
| 18 | + |
| 19 | +## Quick Start |
| 20 | + |
| 21 | +### Simple Mode (Single Instance) |
| 22 | + |
| 23 | +```go |
| 24 | +package main |
| 25 | + |
| 26 | +import ( |
| 27 | + "context" |
| 28 | + "github.com/xraph/forge" |
| 29 | + "github.com/xraph/forge/extensions/cron" |
| 30 | +) |
| 31 | + |
| 32 | +func main() { |
| 33 | + app := forge.New() |
| 34 | + |
| 35 | + // Register cron extension |
| 36 | + cronExt := cron.NewExtension( |
| 37 | + cron.WithMode("simple"), |
| 38 | + cron.WithStorage("memory"), |
| 39 | + cron.WithMaxConcurrentJobs(5), |
| 40 | + ) |
| 41 | + app.RegisterExtension(cronExt) |
| 42 | + |
| 43 | + // Register job handlers |
| 44 | + app.AfterRegister(func(ctx context.Context) error { |
| 45 | + registry := forge.MustResolve[*cron.JobRegistry](app.Container(), "cron.registry") |
| 46 | + |
| 47 | + // Register a handler |
| 48 | + registry.Register("sendReport", func(ctx context.Context, job *cron.Job) error { |
| 49 | + // Your job logic here |
| 50 | + return nil |
| 51 | + }) |
| 52 | + |
| 53 | + // Create a job programmatically |
| 54 | + scheduler := forge.MustResolve[cron.Scheduler](app.Container(), "cron.scheduler") |
| 55 | + scheduler.AddJob(&cron.Job{ |
| 56 | + ID: "daily-report", |
| 57 | + Name: "Daily Report", |
| 58 | + Schedule: "0 9 * * *", // Every day at 9 AM |
| 59 | + HandlerName: "sendReport", |
| 60 | + Enabled: true, |
| 61 | + }) |
| 62 | + |
| 63 | + return nil |
| 64 | + }) |
| 65 | + |
| 66 | + app.Run(context.Background()) |
| 67 | +} |
| 68 | +``` |
| 69 | + |
| 70 | +### Configuration File |
| 71 | + |
| 72 | +Create `jobs.yaml`: |
| 73 | + |
| 74 | +```yaml |
| 75 | +jobs: |
| 76 | + - id: cleanup-temp |
| 77 | + name: Cleanup Temporary Files |
| 78 | + schedule: "0 2 * * *" # Daily at 2 AM |
| 79 | + command: /usr/local/bin/cleanup.sh |
| 80 | + timeout: 10m |
| 81 | + maxRetries: 3 |
| 82 | + enabled: true |
| 83 | + |
| 84 | + - id: backup-database |
| 85 | + name: Database Backup |
| 86 | + schedule: "0 0 * * *" # Daily at midnight |
| 87 | + command: /usr/local/bin/backup-db.sh |
| 88 | + args: |
| 89 | + - --compress |
| 90 | + - --output=/backups |
| 91 | + timeout: 30m |
| 92 | + enabled: true |
| 93 | +``` |
| 94 | +
|
| 95 | +Load jobs from config: |
| 96 | +
|
| 97 | +```go |
| 98 | +app.AfterRegister(func(ctx context.Context) error { |
| 99 | + loader := cron.NewJobLoader(app.Logger(), registry) |
| 100 | + jobs, err := loader.LoadFromFile(ctx, "jobs.yaml") |
| 101 | + if err != nil { |
| 102 | + return err |
| 103 | + } |
| 104 | + |
| 105 | + for _, job := range jobs { |
| 106 | + scheduler.AddJob(job) |
| 107 | + } |
| 108 | + |
| 109 | + return nil |
| 110 | +}) |
| 111 | +``` |
| 112 | + |
| 113 | +## Configuration |
| 114 | + |
| 115 | +```yaml |
| 116 | +extensions: |
| 117 | + cron: |
| 118 | + mode: simple # "simple" or "distributed" |
| 119 | + storage: memory # "memory", "database", or "redis" |
| 120 | + max_concurrent_jobs: 10 |
| 121 | + default_timeout: 5m |
| 122 | + default_timezone: UTC |
| 123 | + max_retries: 3 |
| 124 | + retry_backoff: 1s |
| 125 | + retry_multiplier: 2.0 |
| 126 | + max_retry_backoff: 30s |
| 127 | + history_retention_days: 30 |
| 128 | + enable_api: true |
| 129 | + api_prefix: /api/cron |
| 130 | + enable_web_ui: true |
| 131 | + enable_metrics: true |
| 132 | +``` |
| 133 | +
|
| 134 | +## Cron Schedule Format |
| 135 | +
|
| 136 | +The scheduler supports standard cron expressions with optional seconds: |
| 137 | +
|
| 138 | +``` |
| 139 | +┌────────────── second (0-59) [optional] |
| 140 | +│ ┌──────────── minute (0-59) |
| 141 | +│ │ ┌────────── hour (0-23) |
| 142 | +│ │ │ ┌──────── day of month (1-31) |
| 143 | +│ │ │ │ ┌────── month (1-12 or JAN-DEC) |
| 144 | +│ │ │ │ │ ┌──── day of week (0-6 or SUN-SAT) |
| 145 | +│ │ │ │ │ │ |
| 146 | +│ │ │ │ │ │ |
| 147 | +* * * * * * |
| 148 | +``` |
| 149 | + |
| 150 | +Examples: |
| 151 | +- `0 9 * * *` - Every day at 9 AM |
| 152 | +- `*/15 * * * *` - Every 15 minutes |
| 153 | +- `0 0 * * 0` - Every Sunday at midnight |
| 154 | +- `0 9 * * 1-5` - Weekdays at 9 AM |
| 155 | +- `30 2 1 * *` - 2:30 AM on the first of every month |
| 156 | + |
| 157 | +## REST API |
| 158 | + |
| 159 | +### Job Management |
| 160 | + |
| 161 | +- `GET /api/cron/jobs` - List all jobs |
| 162 | +- `POST /api/cron/jobs` - Create a job |
| 163 | +- `GET /api/cron/jobs/:id` - Get job details |
| 164 | +- `PUT /api/cron/jobs/:id` - Update a job |
| 165 | +- `DELETE /api/cron/jobs/:id` - Delete a job |
| 166 | +- `POST /api/cron/jobs/:id/trigger` - Manually trigger a job |
| 167 | +- `POST /api/cron/jobs/:id/enable` - Enable a job |
| 168 | +- `POST /api/cron/jobs/:id/disable` - Disable a job |
| 169 | + |
| 170 | +### Execution History |
| 171 | + |
| 172 | +- `GET /api/cron/executions` - List all executions |
| 173 | +- `GET /api/cron/jobs/:id/executions` - Get job execution history |
| 174 | +- `GET /api/cron/executions/:id` - Get execution details |
| 175 | + |
| 176 | +### Statistics |
| 177 | + |
| 178 | +- `GET /api/cron/stats` - Get scheduler statistics |
| 179 | +- `GET /api/cron/jobs/:id/stats` - Get job statistics |
| 180 | + |
| 181 | +### Health |
| 182 | + |
| 183 | +- `GET /api/cron/health` - Health check |
| 184 | + |
| 185 | +## Distributed Mode |
| 186 | + |
| 187 | +For multi-instance deployments with leader election: |
| 188 | + |
| 189 | +```yaml |
| 190 | +extensions: |
| 191 | + cron: |
| 192 | + mode: distributed |
| 193 | + storage: redis |
| 194 | + redis_connection: default |
| 195 | + leader_election: true |
| 196 | + consensus_extension: consensus |
| 197 | + heartbeat_interval: 5s |
| 198 | + lock_ttl: 30s |
| 199 | +``` |
| 200 | +
|
| 201 | +Distributed mode ensures only one instance schedules jobs, with automatic failover. |
| 202 | +
|
| 203 | +## Metrics |
| 204 | +
|
| 205 | +Prometheus metrics exposed: |
| 206 | +
|
| 207 | +- `cron_jobs_total` - Total registered jobs |
| 208 | +- `cron_executions_total` - Total executions by status |
| 209 | +- `cron_execution_duration_seconds` - Execution duration histogram |
| 210 | +- `cron_scheduler_lag_seconds` - Lag between scheduled and actual time |
| 211 | +- `cron_executor_queue_size` - Current executor queue size |
| 212 | +- `cron_leader_status` - Leader status (0=follower, 1=leader) |
| 213 | + |
| 214 | +## Web UI |
| 215 | + |
| 216 | +Access the web UI at `/cron/ui` (configurable) to: |
| 217 | + |
| 218 | +- View all scheduled jobs |
| 219 | +- Monitor execution history |
| 220 | +- Manually trigger jobs |
| 221 | +- Enable/disable jobs |
| 222 | +- View real-time statistics |
| 223 | + |
| 224 | +## Advanced Usage |
| 225 | + |
| 226 | +### Retry Configuration |
| 227 | + |
| 228 | +Configure retries per job: |
| 229 | + |
| 230 | +```go |
| 231 | +job := &cron.Job{ |
| 232 | + ID: "retry-example", |
| 233 | + Name: "Job with Custom Retry", |
| 234 | + Schedule: "*/5 * * * *", |
| 235 | + HandlerName: "unreliableTask", |
| 236 | + MaxRetries: 5, |
| 237 | + Timeout: 2 * time.Minute, |
| 238 | + Enabled: true, |
| 239 | +} |
| 240 | +``` |
| 241 | + |
| 242 | +### Job Middleware |
| 243 | + |
| 244 | +Add middleware to job handlers: |
| 245 | + |
| 246 | +```go |
| 247 | +// Logging middleware |
| 248 | +loggingMiddleware := cron.CreateLoggingMiddleware(func(ctx context.Context, job *cron.Job, err error) { |
| 249 | + logger.Info("Job executed", |
| 250 | + "job_id", job.ID, |
| 251 | + "duration", time.Since(start), |
| 252 | + "error", err, |
| 253 | + ) |
| 254 | +}) |
| 255 | +
|
| 256 | +// Panic recovery middleware |
| 257 | +panicMiddleware := cron.CreatePanicRecoveryMiddleware(func(ctx context.Context, job *cron.Job, recovered interface{}) { |
| 258 | + logger.Error("Job panicked", "panic", recovered) |
| 259 | +}) |
| 260 | +
|
| 261 | +// Register with middleware |
| 262 | +registry.RegisterWithMiddleware("myJob", handler, loggingMiddleware, panicMiddleware) |
| 263 | +``` |
| 264 | + |
| 265 | +### Programmatic Job Management |
| 266 | + |
| 267 | +```go |
| 268 | +// Get extension instance |
| 269 | +cronExt := app.Extension("cron").(*cron.Extension) |
| 270 | +
|
| 271 | +// Create job |
| 272 | +job := &cron.Job{ |
| 273 | + ID: "dynamic-job", |
| 274 | + Name: "Dynamically Created Job", |
| 275 | + Schedule: "0 * * * *", |
| 276 | + Command: "/usr/local/bin/script.sh", |
| 277 | + Enabled: true, |
| 278 | +} |
| 279 | +cronExt.CreateJob(ctx, job) |
| 280 | +
|
| 281 | +// Update job |
| 282 | +update := &cron.JobUpdate{ |
| 283 | + Enabled: forge.Ptr(false), |
| 284 | +} |
| 285 | +cronExt.UpdateJob(ctx, "dynamic-job", update) |
| 286 | +
|
| 287 | +// Delete job |
| 288 | +cronExt.DeleteJob(ctx, "dynamic-job") |
| 289 | +
|
| 290 | +// Trigger job |
| 291 | +executionID, err := cronExt.TriggerJob(ctx, "my-job") |
| 292 | +``` |
| 293 | + |
| 294 | +## Security Considerations |
| 295 | + |
| 296 | +- Command injection: Validate all command inputs |
| 297 | +- API authentication: Integrate with auth extension |
| 298 | +- Rate limiting: Limit manual job triggers |
| 299 | +- Audit logging: Track all job modifications |
| 300 | +- Secrets: Use environment variables, never hardcode |
| 301 | + |
| 302 | +## License |
| 303 | + |
| 304 | +See the main Forge license. |
| 305 | + |
0 commit comments