Skip to content

Repository files navigation

Distributed Job Scheduler

A production-inspired distributed job scheduling platform — users authenticate, create projects, each project owns queues, queues hold jobs (immediate, delayed, scheduled-cron, batch). A pool of independent worker processes atomically claims jobs from queues, executes them concurrently under per-queue concurrency limits, retries failures with configurable backoff, and dead-letters permanent failures. Every step is observable via structured logs, Prometheus metrics, and Grafana dashboards.

Stack: Rust (Axum / Tokio / SQLx) · TypeScript + Vite (React) · PostgreSQL 16 · Docker Compose


Table of Contents


Quick Start

Prerequisites

  • Docker and Docker Compose v2+
  • OR Rust 1.75+ and Node.js 20+ for local development

Run with Docker Compose (recommended)

# Bring up the entire stack
docker compose up --build
# Scale workers independently
docker compose up --build --scale worker=3

The stack will be available at:

ServiceURLAuth
Frontend dashboardhttp://localhost:8080admin@example.com / password123
API serverhttp://localhost:3000JWT Bearer token
Prometheushttp://localhost:9090
Grafanahttp://localhost:3001admin / admin

Run locally (development)

# 1. Start PostgreSQL
docker run -d --name pg-scheduler \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=job_scheduler \
-p 5432:5432 postgres:16-alpine
# 2. Backend (env vars use APP_ prefix — see .env.example)export APP_DATABASE_URL=postgres://postgres:postgres@localhost:5432/job_scheduler
cargo run --bin api-server # API on :3000
cargo run --bin worker # Worker process
cargo run --bin scheduler-daemon # Scheduler singleton# 3. Frontendcd frontend
npm install
npm run dev # Vite dev server on :5173, proxies /api → :3000

Seed Data

On first deployment, three database migrations are applied automatically:

  1. Initial schema — all 14 tables, 6 enum types, indexes, triggers
  2. Idempotency fix — replaces UNIQUE NULLS NOT DISTINCT with a partial unique index (so multiple jobs can have a NULL idempotency key)
  3. Seed data — a complete set of sample data for exploration

Accounts (password: password123)

EmailFull NameRole in Acme Corp
admin@example.comAlice Adminowner
dev@example.comBob Developeradmin
viewer@example.comCarol Viewerviewer

Projects & Queues

OrgProjectQueues
Acme CorpWeb Appdefault (×5), high-priority (×10, p=10), email (×3)
Acme CorpBackground Jobsdefault (persistent retry, 10 attempts)
Startup IncAnalytics Pipelineanalytics (×2)

Jobs (10 across 7 statuses)

StatusCountExamples
completed3noop, sleep_100
queued1sleep_1000 — waiting for a worker
scheduled1welcome email — runs in 2 hours
running1sleep_5000 — in-flight
failed2Connection refused, Payment timeout
dead_letter1Exhausted 5 attempts: downstream 500
cancelled1Cancelled by user

Plus: 11 execution records, 7 log lines, 2 DLQ entries (one from max attempts, one from lease expiration), 4 cron scheduled jobs (daily cleanup, hourly digest, weekly report, inactive job), 3 job batches (100-job nightly, 500-job import, 50-job migration).


Architecture

System Topology

graph TB
subgraph "Client Layer"
Browser["Browser<br/>(React SPA)"]
API_Client["External API Client<br/>(JWT / API Key)"]
end
subgraph "Gateway"
Nginx["Nginx<br/>(SPA + reverse proxy)"]
end
subgraph "Application Layer"
API["api-server<br/>(Axum HTTP + WS)<br/>N replicas, stateless"]
Worker["worker<br/>(poll + claim + execute)<br/>N replicas"]
Scheduler["scheduler-daemon<br/>(advisory-lock singleton)<br/>1 active + 1 standby"]
end
subgraph "Data Layer"
PG[("PostgreSQL 16<br/>jobs · queues · workers<br/>executions · logs · DLQ")]
end
subgraph "Observability"
Prom["Prometheus<br/>/metrics scraping"]
Grafana["Grafana<br/>dashboards"]
end
Browser --> Nginx
API_Client --> Nginx
Nginx -->|"REST + WebSocket"| API
Worker -->|"poll + claim<br/>LISTEN/NOTIFY"| PG
Scheduler -->|"promote · cron · reap"| PG
API -->|"queries"| PG
PG --> Prom
API --> Prom
Worker --> Prom
Scheduler --> Prom
Prom --> Grafana
Loading

Hexagonal (Ports & Adapters) Architecture

The backend is a modular monolith compiled as three independent binaries from a single Cargo workspace. Each binary scales independently in production without being separate repos.

graph TB
subgraph "bin/"
API_BIN["api-server / main.rs"]
WORKER_BIN["worker / main.rs"]
SCHED_BIN["scheduler-daemon / main.rs"]
end
subgraph "crates/ — Application"
API_CRATE["api<br/>Axum routes · middleware · DTOs<br/>error.rs · validation.rs"]
WORKER_RT["worker-runtime<br/>executor · heartbeat · shutdown"]
end
subgraph "crates/ — Domain Logic"
SCHED_CORE["scheduler-core<br/>claim · retry · cron · reaper"]
end
subgraph "crates/ — Domain (zero I/O)"
DOMAIN["domain<br/>entities · value objects · ports (traits)"]
end
subgraph "crates/ — Infrastructure"
INFRA["infra-postgres<br/>SQLx repository adapters"]
COMMON["common<br/>config · telemetry · errors"]
end
subgraph "External"
PG[("PostgreSQL")]
end
API_BIN --> API_CRATE
WORKER_BIN --> WORKER_RT
SCHED_BIN --> SCHED_CORE
API_CRATE --> DOMAIN
API_CRATE --> INFRA
WORKER_RT --> DOMAIN
WORKER_RT --> INFRA
SCHED_CORE --> DOMAIN
DOMAIN -. "defines traits" .-> INFRA
INFRA --> PG
COMMON --> DOMAIN
Loading

Dependency rule (enforced by Cargo.toml):

  • domain depends on nothing in the workspace — zero I/O dependencies
  • infra-postgres and scheduler-core depend on domain only
  • api and worker-runtime depend on domain + scheduler-core + infra-postgres — never on each other
  • Binaries are thin entry points wiring everything together

Claim Flow (the core reliability guarantee)

sequenceDiagram
participant W1 as Worker 1
participant W2 as Worker 2
participant PG as PostgreSQL
W1->>PG: BEGIN
W2->>PG: BEGIN
Note over W1,PG: CTE: SELECT id FROM jobs<br/>WHERE status='queued'<br/>ORDER BY priority DESC, run_at ASC<br/>FOR UPDATE SKIP LOCKED<br/>LIMIT free_permits
W1->>PG: Locks rows 1,2,3 (SKIP LOCKED)
W2->>PG: Locks rows 4,5 (skips 1,2,3)
W1->>PG: UPDATE SET status='claimed', locked_by=W1<br/>RETURNING *
W2->>PG: UPDATE SET status='claimed', locked_by=W2<br/>RETURNING *
W1->>PG: COMMIT
W2->>PG: COMMIT
Note over W1,W2: Zero contention, zero duplicate execution
Loading

Workers never SELECT then UPDATE separately — that's a race condition. The CTE with FOR UPDATE SKIP LOCKED is a single atomic operation.


Database Design

Entity-Relationship Diagram

erDiagram
users ||--o{ organization_members : "belongs to"
organizations ||--o{ organization_members : "has"
organizations ||--o{ projects : "owns"
projects ||--o{ queues : "contains"
projects ||--o{ retry_policies : "defines"
projects ||--o{ job_batches : "groups"
queues ||--o{ jobs : "holds"
queues ||--o{ scheduled_jobs : "templates"
queues ||--o{ dead_letter_queue : "failed"
retry_policies ||--o| queues : "default for"
retry_policies ||--o| jobs : "overrides"
retry_policies ||--o| scheduled_jobs : "applies to"
workers ||--o{ worker_heartbeats : "emits"
workers ||--o{ jobs : "locks"
workers ||--o{ job_executions : "executes"
jobs ||--o{ job_executions : "has attempts"
jobs ||--o{ job_logs : "produces"
jobs ||--o{ job_dependencies : "depends on"
jobs ||--o| dead_letter_queue : "dead-letters to"
job_executions ||--o{ job_logs : "contains"
job_batches ||--o{ jobs : "batches"
scheduled_jobs ||--o{ jobs : "spawns"
users {
uuid id PK
text email UK
text password_hash
text full_name
boolean is_active
timestamptz created_at
timestamptz updated_at
}
organizations {
uuid id PK
text name
text slug UK
timestamptz created_at
}
organization_members {
uuid org_id PK_FK
uuid user_id PK_FK
org_role role
}
projects {
uuid id PK
uuid org_id FK
text name
text api_key_hash UK
uuid created_by FK
timestamptz created_at
timestamptz updated_at
}
queues {
uuid id PK
uuid project_id FK
text name
int priority
int max_concurrency
boolean is_paused
uuid default_retry_policy_id FK
int visibility_timeout_secs
timestamptz created_at
timestamptz updated_at
}
retry_policies {
uuid id PK
uuid project_id FK
text name
retry_strategy strategy
bigint base_delay_ms
float multiplier
bigint max_delay_ms
int max_attempts
boolean jitter
timestamptz created_at
}
jobs {
uuid id PK
uuid queue_id FK
uuid scheduled_job_id FK
uuid batch_id FK
text job_type
jsonb payload
int priority
job_status status
timestamptz run_at
text idempotency_key
uuid retry_policy_id FK
int max_attempts_override
int attempt_count
uuid locked_by FK
timestamptz locked_at
timestamptz lease_expires_at
jsonb result
text error_message
text trace_id
timestamptz created_at
timestamptz updated_at
timestamptz completed_at
}
job_executions {
uuid id PK
uuid job_id FK
uuid worker_id FK
int attempt_number
execution_status status
timestamptz started_at
timestamptz finished_at
bigint duration_ms
text error_message
text error_stack
jsonb result
timestamptz created_at
}
job_logs {
bigserial id PK
uuid execution_id FK
uuid job_id FK
timestamptz ts
text level
text message
}
workers {
uuid id PK
text hostname
int pid
text version
uuid[] subscribed_queues
int max_concurrency
worker_status status
timestamptz started_at
timestamptz last_seen_at
}
worker_heartbeats {
bigserial id PK
uuid worker_id FK
timestamptz ts
int active_job_count
float cpu_percent
float memory_mb
jsonb metadata
}
dead_letter_queue {
uuid id PK
uuid job_id FK
uuid queue_id FK
jsonb payload_snapshot
text failure_reason
int attempt_count
text last_error
timestamptz moved_at
boolean resolved
uuid resolved_by FK
timestamptz resolved_at
text resolution_action
}
scheduled_jobs {
uuid id PK
uuid queue_id FK
text name
text cron_expression
text timezone
text job_type
jsonb payload_template
uuid retry_policy_id FK
boolean is_active
timestamptz next_run_at
timestamptz last_run_at
timestamptz created_at
timestamptz updated_at
}
job_batches {
uuid id PK
uuid project_id FK
text name
int total_jobs
int completed_jobs
int failed_jobs
batch_status status
timestamptz created_at
timestamptz updated_at
}
job_dependencies {
uuid job_id PK_FK
uuid depends_on_job_id PK_FK
}
Loading

Key Indexes

TableIndexPurpose
jobs(queue_id, status, priority DESC, run_at) WHERE status = 'queued'Claim query access path — the most important index in the schema
jobs(status, run_at) WHERE status = 'scheduled'Scheduler promotion sweep
jobs(lease_expires_at) WHERE status IN ('claimed','running')Reaper expired-lease sweep
jobs(queue_id, idempotency_key) WHERE idempotency_key IS NOT NULLIdempotent submission (partial unique)
job_executions(job_id, attempt_number)Retry history per job
job_logs(job_id, ts)Log tail per job
worker_heartbeats(worker_id, ts DESC)Staleness detection

Enums

graph LR
subgraph "job_status"
scheduled --> queued
queued --> claimed
claimed --> running
running --> completed
running --> failed
failed --> scheduled
failed --> dead_letter
scheduled --> dead_letter
claimed --> dead_letter
running --> dead_letter
queued --> cancelled
scheduled --> cancelled
end
Loading

Six enum types: job_status, execution_status, retry_strategy, worker_status, batch_status, org_role.


Job Lifecycle

Full State Machine

stateDiagram-v2
[*] --> scheduled : Job created with<br/>run_at in the future
[*] --> queued : Job created with<br/>run_at ≤ now()
scheduled --> queued : scheduler-daemon<br/>promotion sweep
queued --> claimed : worker atomically claims<br/>(FOR UPDATE SKIP LOCKED)
claimed --> running : worker begins execution
running --> completed : handler returns Ok
running --> failed : handler returns Err
failed --> scheduled : attempt_count < max_attempts<br/>(retry with backoff)
failed --> dead_letter : attempt_count ≥ max_attempts
scheduled --> dead_letter : lease expired + attempts exhausted
claimed --> scheduled : lease expired + attempts remain
running --> scheduled : lease expired + attempts remain
queued --> cancelled : user cancels
scheduled --> cancelled : user cancels
completed --> [*]
dead_letter --> [*]
cancelled --> [*]
note right of failed
Retry backoff reuses the
"scheduled" state.
attempt_count > 0
distinguishes retries.
end note
Loading

Retry Strategies

StrategyFormulaJitter
fixeddelay = base_delay_msdelay × uniform(0.5, 1.5)
lineardelay = base_delay_ms × attempt_countdelay × uniform(0.5, 1.5)
exponentialdelay = min(base_delay_ms × multiplier^(attempt_count−1), max_delay_ms)delay × uniform(0.5, 1.5)

Lease & Reaper

  • Every claimed job gets a lease_expires_at = now() + visibility_timeout_secs
  • Healthy workers renew leases via the heartbeat ticker (every 10s)
  • scheduler-daemon's reaper sweeps expired leases every 15s:
    • attempt_count < max_attempts → reschedule for retry
    • attempt_count ≥ max_attempts → dead letter

API Overview

Authentication

MethodPathAuthDescription
POST/api/auth/registerPublicRegister + get JWT tokens
POST/api/auth/loginPublicLogin + get JWT tokens
POST/api/auth/refreshPublicRotate tokens with refresh token

Protected routes require Authorization: Bearer <token> header. JWT claims contain sub (user UUID), email, exp, iat.

Organizations & Projects

MethodPathDescription
POST/api/organizationsCreate org (user becomes owner)
GET/api/organizationsList orgs for authenticated user
POST/api/projectsCreate project (returns API key once)
GET/api/projects?org_id=List projects
GET/api/projects/{id}Get project
DELETE/api/projects/{id}Delete project

Queues

MethodPathDescription
POST/api/queuesCreate queue
GET/api/queues?project_id=List queues with depth
GET/api/queues/{id}Get queue details + depth
DELETE/api/queues/{id}Delete queue
POST/api/queues/{id}/pausePause processing
POST/api/queues/{id}/resumeResume processing

Jobs

MethodPathDescription
POST/api/jobsSubmit job (supports idempotency keys, delayed run_at, dependencies)
GET/api/jobs?queue_id=&status=&job_type=&limit=&offset=List jobs with filtering & pagination
GET/api/jobs/{id}Get job details
POST/api/jobs/{id}/cancelCancel a job
GET/api/jobs/{id}/executionsGet execution history
GET/api/jobs/{id}/logsGet job logs

Dead Letter Queue

MethodPathDescription
GET/api/dead-letter?queue_id=&resolved=List DLQ entries (no queue_id = all queues)
POST/api/dead-letter/{id}/resolveResolve entry (requeued or discarded)

Retry Policies

MethodPathDescription
POST/api/retry-policiesCreate policy
GET/api/retry-policies?project_id=List policies for project
GET/api/retry-policies/{id}Get policy
DELETE/api/retry-policies/{id}Delete policy

Scheduled Jobs (Cron)

MethodPathDescription
POST/api/scheduled-jobsSchedule a cron job
GET/api/scheduled-jobsList active scheduled jobs
GET/api/scheduled-jobs/{id}Get scheduled job
POST/api/scheduled-jobs/{id}/deactivateDeactivate

Batches, Workers, Health & Metrics

MethodPathAuthDescription
GET/api/batches/{id}JWTGet batch progress
GET/api/workersJWTList active workers
GET/healthzPublicLiveness (always 200)
GET/readyzPublicReadiness (checks DB)
GET/metricsPublicPrometheus metrics
WS/ws/eventsPublicWebSocket for live events

Error Response Format

All errors return a consistent JSON structure:

{
"error": "validation_error",
"message": "Validation failed",
"details": [
{"field": "email", "message": "Invalid email format"},
{"field": "password", "message": "Password must be at least 8 characters"}
]
}

Error codes: bad_request, unauthorized, forbidden, not_found, conflict, validation_error, too_many_requests, internal_error.


Frontend

A React 18 SPA served by nginx, with TanStack Query (server state), Zustand (auth store), Recharts (dashboard charts), and Tailwind CSS.

Pages

PageRouteFeatures
Dashboard/Stats cards (queues, queued, running, DLQ, workers), Recharts bar chart (queue depth), pie chart (status distribution), cron job list, org selector
Queues/queuesProject selector, create queue form, pause/resume, per-queue depth breakdown (queued/scheduled/running/DLQ), link to jobs
Jobs/jobsStatus filter, paginated data table with status badges, execution history, job log viewer, cancel action
Workers/workersLive grid with status dots, auto-refresh every 5s, host/version/concurrency details
Dead Letter/dead-letterDLQ entries with requeue/discard, payload snapshot expand, failure reason + last error
Retry Policies/retry-policiesCreator form, strategy badges (fixed/linear/exponential), delay configs
Scheduled Jobs/scheduled-jobsCron job cards with next-run display, create/deactivate, queue selector
Login/loginLogin/register toggle, Zustand auth store integration, protected route redirect

Authentication Flow

  1. User logs in → JWT access + refresh tokens stored in localStorage
  2. All API calls attach Authorization: Bearer <token> header automatically
  3. Protected pages redirect to /login when unauthenticated
  4. Auth state managed via Zustand store (useAuthStore)

Observability

Metrics Catalogue

MetricTypeLabelsPurpose
jobs_created_totalCounterqueue, job_typeIntake rate
jobs_claimed_totalCounterqueue, worker_idClaim throughput
jobs_completed_totalCounterqueue, job_typeSuccess throughput
jobs_failed_totalCounterqueue, job_type, terminalFailure rate
job_execution_duration_secondsHistogramqueue, job_typep50/p95/p99 latency
queue_depthGaugequeue, statusBacklog per status
worker_active_jobsGaugeworker_idConcurrency utilization
worker_upGaugeworker_idWorker health
dlq_sizeGaugequeueDLQ growth
lease_expirations_totalCounterqueueCrashed worker rate

Logging

All three binaries emit structured JSON logs (configurable to pretty-format for development). Every log line carries: timestamp, level, target (module path), span fields (job_id, queue_id, worker_id, attempt), thread.id, thread.name.

Example:

{
"timestamp": "2026-07-04T12:00:00.000Z",
"level": "INFO",
"target": "worker_runtime::executor",
"fields": {
"job_id": "a1b2c3d4-...",
"queue_id": "e5f6a7b8-...",
"attempt": 1,
"duration_ms": 234
},
"message": "Job completed successfully"
}

Project Structure

├── Cargo.toml # Workspace root
├── .env.example # All configuration (APP_ prefixed)
├── docker-compose.yml # Full stack: PG + 3 binaries + frontend + Prom + Grafana
│
├── bin/
│ ├── api-server/main.rs # Thin: metrics init + axum::serve
│ ├── worker/main.rs # Thin: queue discovery + poll loop + graceful shutdown
│ └── scheduler-daemon/main.rs # Thin: advisory lock + promotion/cron/reaper loops
│
├── crates/
│ ├── domain/ # Entities, value objects, ports (ZERO I/O)
│ │ └── src/
│ │ ├── entities.rs # 14 entity structs
│ │ ├── value_objects.rs # 6 enums (JobStatus, RetryStrategy, …)
│ │ ├── ports.rs # 11 async repository traits
│ │ └── errors.rs # DomainError enum
│ │
│ ├── common/ # Shared infrastructure
│ │ └── src/
│ │ ├── config.rs # Env-var config (APP_ prefix via figment)
│ │ ├── telemetry.rs # tracing-subscriber + Prometheus init
│ │ └── error.rs # CommonError type
│ │
│ ├── infra-postgres/ # SQLx adapters (implements domain::ports)
│ │ └── src/
│ │ ├── repositories/ # 11 repository impls
│ │ │ ├── job.rs # ★ Claim query (SKIP LOCKED CTE)
│ │ │ ├── queue.rs # CRUD + depth (Uuid::nil = all)
│ │ │ ├── worker.rs # Registration + heartbeats
│ │ │ ├── execution.rs # Execution records + logs
│ │ │ ├── dead_letter.rs # DLQ (Option<Uuid> = all queues)
│ │ │ ├── scheduled_job.rs # Cron template management
│ │ │ ├── user.rs # Registration + lookup
│ │ │ ├── organization.rs # Org membership + RBAC
│ │ │ ├── project.rs # Project CRUD
│ │ │ ├── retry_policy.rs # Retry policy CRUD
│ │ │ └── batch.rs # Batch progress tracking
│ │ └── migrations.rs # sqlx::migrate! runner
│ │
│ ├── scheduler-core/ # Pure business logic
│ │ └── src/
│ │ ├── claim.rs # JobClaimer
│ │ ├── retry.rs # RetryCalculator (pure fn, unit tested)
│ │ ├── cron_eval.rs # CronEvaluator (parse + next run)
│ │ └── lease_reaper.rs # LeaseReaper (sweep + DLQ insert)
│ │
│ ├── api/ # Axum HTTP layer
│ │ └── src/
│ │ ├── lib.rs # AppState + build_router()
│ │ ├── dto.rs # Request/response DTOs
│ │ ├── error.rs # AppError (8 variants → HTTP status)
│ │ ├── validation.rs # Validator (email, password, bounds, enums)
│ │ ├── middleware/
│ │ │ ├── auth.rs # JWT decode + Claims → Extension
│ │ │ ├── request_id.rs # UUID X-Request-Id
│ │ │ ├── rate_limit.rs # Token bucket limiter (bonus)
│ │ │ ├── api_key.rs # X-API-Key auth (bonus)
│ │ │ └── rbac.rs # Org role checking (bonus)
│ │ ├── routes/
│ │ │ ├── auth.rs # register, login, refresh
│ │ │ ├── health.rs # GET /healthz, /readyz
│ │ │ ├── metrics.rs # GET /metrics (global handle)
│ │ │ ├── projects.rs # CRUD /api/projects
│ │ │ ├── queues.rs # CRUD + pause/resume
│ │ │ ├── jobs.rs # CRUD + cancel + executions + logs
│ │ │ ├── workers.rs # GET /api/workers
│ │ │ ├── organizations.rs # CRUD /api/organizations (real JWT user)
│ │ │ ├── dead_letter.rs # List + resolve
│ │ │ ├── retry_policies.rs # CRUD /api/retry-policies
│ │ │ ├── scheduled_jobs.rs # CRUD + deactivate
│ │ │ └── batches.rs # GET /api/batches/{id}
│ │ └── websocket.rs # WS /ws/events
│ │
│ └── worker-runtime/ # Job execution engine
│ └── src/
│ ├── lib.rs # WorkerConfig + QueueRuntime
│ ├── executor.rs # poll + claim + execute + retry/DLQ
│ ├── heartbeat.rs # HeartbeatService (ticker + lease renew)
│ ├── shutdown.rs # GracefulShutdown (SIGTERM → drain)
│ └── job_handlers.rs # HandlerRegistry + noop/sleep/failing handlers
│
├── migrations/
│ ├── 20240704000001_initial_schema.up.sql # Full schema
│ ├── 20240704000002_fix_idempotency.up.sql # Partial unique index
│ └── 20240704000003_seed_data.up.sql # Sample users/orgs/jobs/DLQ/cron
│
├── docker/
│ ├── Dockerfile.backend # Multi-stage (cargo-chef, --release)
│ ├── Dockerfile.frontend # Multi-stage (Vite → nginx)
│ ├── nginx.conf # SPA fallback + /api + /ws proxy
│ ├── prometheus.yml # Scrape config for all 3 binaries
│ └── grafana/provisioning/ # Datasource auto-provisioning
│
└── frontend/
├── package.json
├── vite.config.ts
├── tailwind.config.js
└── src/
├── main.tsx # React root + QueryClient + Router
├── App.tsx # Navigation + 7 routes + sign out
├── api-client/index.ts # Typed API client (all endpoints)
├── stores/auth.ts # Zustand auth store
├── types/index.ts # TypeScript interfaces
└── pages/
├── Dashboard.tsx # Recharts + stats + cron list
├── Queues.tsx # Create/pause/resume + depth cards
├── Jobs.tsx # Filters + table + executions + logs
├── Workers.tsx # Live grid with status dots
├── DeadLetter.tsx # Requeue/discard + payload view
├── RetryPolicies.tsx # Strategy badges + create form
├── ScheduledJobs.tsx # Cron cards + create/deactivate
└── Login.tsx # Login/register toggle

Configuration

All configuration via environment variables with APP_ prefix (see .env.example):

VariableDefaultDescription
APP_DATABASE_URLpostgres://postgres:postgres@localhost:5432/job_schedulerConnection string
APP_DATABASE_MAX_CONNECTIONS10Pool max (per binary)
APP_JWT_SECRETchange-me-in-productionHMAC signing key
APP_JWT_ACCESS_EXPIRY_SECS900Access token TTL
APP_JWT_REFRESH_EXPIRY_SECS604800Refresh token TTL
APP_API_HOST0.0.0.0Bind address
APP_API_PORT3000HTTP port
APP_METRICS_PORT9001Prometheus port
APP_WORKER_POLL_INTERVAL_MS1000Poll interval
APP_WORKER_MAX_CONCURRENCY10Per-worker cap
APP_WORKER_HEARTBEAT_INTERVAL_SECS10Heartbeat interval
APP_WORKER_GRACEFUL_SHUTDOWN_TIMEOUT_SECS30Drain timeout
APP_SCHEDULER_POLL_INTERVAL_MS2000Promotion interval
APP_SCHEDULER_LEASE_REAPER_INTERVAL_SECS15Reaper interval
APP_SCHEDULER_ADVISORY_LOCK_ID42Singleton lock key
APP_LOG_LEVELinfoTracing filter
APP_LOG_FORMATjsonjson or pretty

Design Decisions

Postgres as the Queue (not Kafka/RabbitMQ)

ProCon
One fewer moving partLower raw throughput than dedicated broker
SKIP LOCKED for lock-free consumersQueue depth growth increases claim cost
LISTEN/NOTIFY for event-driven wake-upPolling is the safety net
Transactional create + enqueue in one writeHot-table vacuum needed at scale

For throughput up to ~10k jobs/sec, Postgres-as-queue is more than sufficient and operationally simpler.

Retry Backoff Reuses 'scheduled' State

Rather than introducing a separate retrying state, a job waiting for its initial run_at and a job waiting for its next retry are the same: "not ready yet." attempt_count > 0 distinguishes retries.

Idempotency: At-Least-Once + Idempotent Handlers

The system guarantees at-least-once delivery. Exactly-once is provably impossible in a distributed system. The correct target is at-least-once with idempotent job handlers.

Denormalization Trade-offs

  1. job_logs.job_id — avoids a JOIN on the highest-write table for "tail this job's logs"
  2. dead_letter_queue.payload_snapshot — DLQ entries survive job purges/archives

Known Future Work

  • job_logs partitioning — monthly range with retention-based DROP
  • Soft deletes on projects and queues
  • Job archiving — move old completed/dead_letter jobs to jobs_archive
  • Redis rate limiting — cross-replica token buckets
  • Workflow dependenciesjob_dependencies table exists; promotion sweep should check satisfaction

About

Assignment Given by Codity.ai

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages