{"id":47675390,"url":"https://github.com/hardbyte/awa","last_synced_at":"2026-05-03T08:03:53.103Z","repository":{"id":345055408,"uuid":"1184037158","full_name":"hardbyte/awa","owner":"hardbyte","description":"Postgres-native background job queue for Rust and Python","archived":false,"fork":false,"pushed_at":"2026-04-30T20:30:18.000Z","size":7383,"stargazers_count":17,"open_issues_count":22,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-04-30T22:22:58.923Z","etag":null,"topics":["job-scheduler","task-scheduler"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hardbyte.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"docs/security.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-17T07:28:47.000Z","updated_at":"2026-04-30T20:30:18.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/hardbyte/awa","commit_stats":null,"previous_names":["hardbyte/awa"],"tags_count":37,"template":false,"template_full_name":null,"purl":"pkg:github/hardbyte/awa","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hardbyte%2Fawa","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hardbyte%2Fawa/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hardbyte%2Fawa/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hardbyte%2Fawa/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hardbyte","download_url":"https://codeload.github.com/hardbyte/awa/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hardbyte%2Fawa/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32562118,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-03T06:36:36.687Z","status":"ssl_error","status_checked_at":"2026-05-03T06:36:09.306Z","response_time":103,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["job-scheduler","task-scheduler"],"created_at":"2026-04-02T13:28:26.330Z","updated_at":"2026-05-03T08:03:53.059Z","avatar_url":"https://github.com/hardbyte.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Awa\n\n**Postgres-native job queue for Rust and Python.**\n\nAwa (Māori: river) fills the gap between Postgres event queues that are too\nnarrow for real job-queue behavior and language-specific job frameworks (River,\nOban, Sidekiq) that couple you to one ecosystem. If you run Rust or Python (or\nboth) on Postgres and want priorities, cron, DLQ, and transactional enqueue\nwithout Redis or RabbitMQ, Awa is built for you.\n\n![AWA Web UI — Jobs (dark mode)](https://raw.githubusercontent.com/hardbyte/awa/main/docs/images/awa-ui-dark.png)\n\n## Features\n\n### Core queue\n- **Transactional enqueue** — insert jobs inside your business transaction. Commit = visible. Rollback = gone.\n- **Unique jobs** — declare uniqueness by kind/queue/args; cancel by unique key without storing job IDs.\n- **Priorities, retries, snoozes** — exponential backoff with jitter; priority aging for fairness.\n- **Dead Letter Queue** — first-class DLQ with per-queue opt-in, retention, and operator retry/purge.\n- **Periodic/cron jobs** — leader-elected scheduler with timezone support and atomic enqueue.\n- **Sequential callbacks** — `wait_for_callback()` / `resume_external()` for multi-step orchestration within a single handler.\n- **Webhook callbacks** — park jobs for external completion with optional CEL-expression filtering.\n\n### Runtime\n- **Rust and Python workers** — same queues, same storage engine, mixed deployments.\n- **Crash recovery** — heartbeat + hard deadline rescue. Stale jobs recovered automatically.\n- **Runtime-owned maintenance** — dispatch, rescue, segment rotation, and pruning run in the worker fleet; no `pg_cron` ticker required.\n- **Segmented queue storage** — append-only ready and terminal entries with rotating lease segments; queue history and execution churn stay off the dispatch path.\n- **LISTEN/NOTIFY wakeup** — millisecond-scale pickup latency.\n- **HTTP Worker** — feature-gated worker that dispatches jobs to serverless functions (Lambda, Cloud Run) via HTTP with HMAC-BLAKE3 callback auth.\n- **Weighted concurrency + rate limiting** — global worker pool with per-queue guarantees; per-queue token bucket.\n\n### Operations\n- **Web UI** — dashboard, job inspector, queue management, cron controls, DLQ retry/purge.\n- **Structured progress** — handlers report percent, message, and checkpoint metadata; persisted across retries.\n- **OpenTelemetry metrics** — 20+ built-in counters, histograms, and gauges for Prometheus/Grafana. Python workers enable export with `awa.init_telemetry(endpoint, service)`; Rust workers install their own provider.\n- **Operator descriptors** — code-declared queue and job-kind names/descriptions with stale/drift visibility in the UI.\n- **Postgres-only** — one dependency you already have; no Redis, no RabbitMQ, no separate scheduler.\n\n![AWA Web UI — Queue detail (dark mode)](https://raw.githubusercontent.com/hardbyte/awa/main/docs/images/awa-ui-queue-detail-dark.png)\n\n## Correctness\n\nCore concurrency invariants — no duplicate processing after rescue, stale\ncompletions rejected, no claim/rotate/prune deadlock, DLQ round-trip safety,\nprune-segment emptiness, heartbeat-driven short-job rescue — are checked by\n[TLA+ models](https://github.com/hardbyte/awa/blob/main/correctness/README.md)\ncovering the segmented storage engine, the lock-ordering protocol, and the\nsingle/multi-instance worker runtime. The storage model has a trace-replay\nharness that verifies concrete runtime-test event sequences against the spec.\n\n## Delivery Contract\n\n- **Transactional enqueue** is a core Postgres-native feature: enqueue inside\n  the same transaction as application data, and the job commits or rolls back\n  with that data.\n- **At-least-once delivery** is the contract. Awa rejects stale completions\n  and rescues stuck work, but it does not promise “exactly once”.\n- **Idempotency is recommended** for handlers, because retries and recovery are\n  part of the honest failure model.\n- **No lost work under failure** takes priority over clever fast paths. If a\n  design weakens crash/restart safety, it loses even if the benchmark looks\n  better.\n\n## Benchmarks\n\nLocal queue-storage soak, 5k-job runtime run: **9.5k jobs/s**, **22 ms p95\npickup**, **417 exact final dead tuples**. Enqueue: ~30k/s single-producer,\n~100k/s multi-producer.\n\nA phase-driven portable benchmark harness comparing Awa against pgque,\nprocrastinate, pg-boss, river, oban, and pgmq on a shared Postgres\ninstance lives in its own repository:\n[hardbyte/postgresql-job-queue-benchmarking](https://github.com/hardbyte/postgresql-job-queue-benchmarking).\nIt records producer, subscriber, and end-to-end delivery latency\nalongside throughput, queue depth, and dead tuples over time.\n\nMethodology and caveats live in\n[benchmarking notes](docs/benchmarking.md). Validation artifacts:\n[ADR-019 (queue storage)](docs/adr/bench/019-queue-storage-validation-2026-04-19.md)\nand [ADR-023 (receipt-plane ring partitioning)](docs/adr/bench/023-receipt-ring-validation-2026-04-26.md).\n\n## Where Awa Fits\n\nAwa is for teams that already trust Postgres and want a real job queue, not\njust a stream or a framework tied to one host language.\n\n- Choose Awa when you want priorities, unique jobs, retries, cron, callbacks,\n  DLQ, and operator tooling on one Postgres-backed runtime.\n- Choose PgQue-style systems when you want an event queue with independent\n  consumer cursors and event-log semantics first.\n- Choose River or Oban Pro when you want a job framework tightly shaped around\n  one surrounding language ecosystem.\n\nSee [docs/positioning.md](docs/positioning.md) for the category map and messaging guidance.\n\n## Getting Started\n\n```bash\n# 1. Install\npip install awa-pg awa-cli     # Python\n# or: cargo add awa             # Rust\n\n# 2. Start Postgres and run migrations\nawa --database-url $DATABASE_URL migrate\n\n# 3. Write a worker and start processing (see examples below)\n\n# 4. Monitor\nawa --database-url $DATABASE_URL serve   # → http://127.0.0.1:3000\nawa --database-url $DATABASE_URL storage status\nawa --database-url $DATABASE_URL job dump 123\nawa --database-url $DATABASE_URL job dump-run 123\n```\n\nThe Awa mental model: your app inserts durable queue entries inside Postgres,\noften in the same transaction as business data; workers claim runnable entries\nthrough short-lived execution leases and rescue stale work after crashes;\nlong-running attempts touch `attempt_state` only when they need mutable data\nlike progress or callback state; operators inspect live, terminal, and DLQ\nstate through the CLI or the built-in UI.\n\nLanguage-specific guides:\n\n- [Rust getting started](docs/getting-started-rust.md)\n- [Python getting started](docs/getting-started-python.md)\n\nConfiguring real workloads:\n\n- [Worker scope: which queues and which kinds](docs/configuration.md#worker-scope-which-queues-and-which-kinds) — running a worker against one queue, or splitting kinds across queues\n- [Job priority and aging](docs/configuration.md#job-priority-and-aging) — priority scale, escalation, the per-queue `priority_aging_interval`\n- [Reliability timings](docs/configuration.md#reliability-timings-heartbeat-deadline-rescue) — heartbeat / deadline / callback rescue, retention, the 3× heartbeat-staleness rule\n- [Dead Letter Queue](docs/configuration.md#dead-letter-queue) — when to enable, per-queue overrides, operator workflow\n\nAlready running 0.5? Read the [0.5 → 0.6 upgrade guide](docs/upgrade-0.5-to-0.6.md)\nbefore you bump — 0.6 introduces a staged storage transition (canonical →\nprepared → mixed_transition → active) with a refused-by-default gate that\nexpects the operator to roll out queue-storage-capable workers first.\n\n## Python Example\n\n\u003c!-- Tested in CI via awa-python/examples/quickstart.py --\u003e\n\n```python\nimport awa\nimport asyncio\nfrom dataclasses import dataclass\n\n@dataclass\nclass SendEmail:\n    to: str\n    subject: str\n\nasync def main():\n    client = awa.AsyncClient(\"postgres://localhost/mydb\")\n    await client.migrate()\n\n    @client.task(SendEmail, queue=\"email\")\n    async def handle_email(job):\n        print(f\"Sending to {job.args.to}: {job.args.subject}\")\n\n    await client.insert(\n        SendEmail(to=\"alice@example.com\", subject=\"Welcome\"),\n        queue=\"email\",\n    )\n\n    client.start([(\"email\", 2)])\n    await asyncio.sleep(1)\n    await client.shutdown()\n\nasyncio.run(main())\n```\n\n**Progress tracking** — checkpoint and resume on retry:\n\n```python\n@client.task(BatchImport, queue=\"etl\")\nasync def handle_import(job):\n    last_id = (job.progress or {}).get(\"metadata\", {}).get(\"last_id\", 0)\n    for item in fetch_items(after=last_id):\n        process(item)\n        job.set_progress(50, \"halfway\")\n        job.update_metadata({\"last_id\": item.id})\n    await job.flush_progress()\n```\n\n**Transactional enqueue** — atomic with your business logic:\n\n```python\nasync with await client.transaction() as tx:\n    await tx.execute(\"INSERT INTO orders (id) VALUES ($1)\", order_id)\n    await tx.insert(SendEmail(to=\"alice@example.com\", subject=\"Order confirmed\"))\n```\n\n**Sync API** for Django/Flask — use `awa.Client` for sync frameworks; all methods are plain (no suffix):\n\n```python\nclient = awa.Client(\"postgres://localhost/mydb\")\nclient.migrate()\njob = client.insert(SendEmail(to=\"bob@example.com\", subject=\"Hello\"))\n```\n\n**Sequential callbacks** — suspend a handler, wait for an external system, then resume:\n\n```python\n@client.task(ProcessPayment, queue=\"payments\")\nasync def handle_payment(job):\n    token = await job.register_callback(timeout_seconds=3600)\n    send_to_payment_gateway(token.id, job.args.amount)\n    result = await job.wait_for_callback(token)\n    # result contains the payload from resume_external()\n    await record_payment(job.args.order_id, result)\n```\n\nThe external system calls `await client.resume_external(callback_id, {\"status\": \"paid\"})` to wake the handler.\n\n**Periodic jobs** — leader-elected cron scheduling with timezone support:\n\n```python\nclient.periodic(\n    \"daily_report\", \"0 9 * * *\",\n    GenerateReport, GenerateReport(format=\"pdf\"),\n    timezone=\"Pacific/Auckland\",\n)\n```\n\n6-field expressions with seconds precision are also supported: `\"*/15 * * * * *\"` fires every 15 seconds.\n\nSee [`examples/python/`](https://github.com/hardbyte/awa/tree/main/examples/python) for complete runnable scripts tested in CI.\n\n## Rust Example\n\n```rust\nuse awa::{Client, QueueConfig, JobArgs, JobResult, JobError, JobContext, Worker};\nuse serde::{Serialize, Deserialize};\n\n#[derive(Debug, Serialize, Deserialize, JobArgs)]\nstruct SendEmail {\n    to: String,\n    subject: String,\n}\n\nstruct SendEmailWorker;\n\n#[async_trait::async_trait]\nimpl Worker for SendEmailWorker {\n    fn kind(\u0026self) -\u003e \u0026'static str { \"send_email\" }\n\n    async fn perform(\u0026self, ctx: \u0026JobContext) -\u003e Result\u003cJobResult, JobError\u003e {\n        let args: SendEmail = serde_json::from_value(ctx.job.args.clone())\n            .map_err(|e| JobError::terminal(e.to_string()))?;\n        send_email(\u0026args.to, \u0026args.subject).await\n            .map_err(JobError::retryable)?;\n        Ok(JobResult::Completed)\n    }\n}\n\n// Insert a job (with uniqueness)\nawa::insert_with(\u0026pool, \u0026SendEmail { to: \"alice@example.com\".into(), subject: \"Welcome\".into() },\n    awa::InsertOpts { unique: Some(awa::UniqueOpts { by_args: true, ..Default::default() }), ..Default::default() },\n).await?;\n\n// Cancel by unique key (e.g., when the triggering condition is resolved)\nawa::admin::cancel_by_unique_key(\u0026pool, \"send_email\", None, Some(\u0026serde_json::json!({\"to\": \"alice@example.com\", \"subject\": \"Welcome\"})), None).await?;\n\n// Start workers with a typed lifecycle hook\nlet client = Client::builder(pool)\n    .queue(\"default\", QueueConfig::default())\n    .register_worker(SendEmailWorker)\n    .on_event::\u003cSendEmail, _, _\u003e(|event| async move {\n        if let awa::JobEvent::Exhausted { args, error, .. } = event {\n            tracing::error!(to = %args.to, error = %error, \"email job exhausted retries\");\n        }\n    })\n    .build()?;\nclient.start().await?;\n```\n\nCancellation is cooperative for running handlers:\n\n- Rust handlers can poll `ctx.is_cancelled()`.\n- Python handlers can poll `job.is_cancelled()`.\n- Shutdown and runtime rescue paths flip that flag.\n- Admin cancel (`awa::admin::cancel`, `client.cancel`) always updates job state\n  in storage, but a running handler is not currently guaranteed to observe an\n  in-memory cancellation signal from admin cancel alone.\n\n## Installation\n\n### Python\n\n```bash\npip install awa-pg       # SDK: insert, worker, admin, progress\npip install awa-cli      # CLI: migrations, queue admin, web UI\n```\n\n### Rust\n\n```toml\n[dependencies]\nawa = \"0.6\"\n```\n\n### CLI\n\nAvailable via pip (no Rust toolchain needed) or cargo:\n\n```bash\npip install awa-cli\n# or: cargo install awa-cli\n\nawa --database-url $DATABASE_URL migrate\nawa --database-url $DATABASE_URL serve\nawa --database-url $DATABASE_URL queue stats\nawa --database-url $DATABASE_URL job list --state failed\nawa --database-url $DATABASE_URL job dump 123\nawa --database-url $DATABASE_URL job dump-run 123\n```\n\n## Architecture\n\n```\n ┌────────────────┐  ┌────────────────┐\n │ Rust producer  │  │  Python (pip)  │\n └───────┬────────┘  └────────┬───────┘\n         └────────┬───────────┘\n                  ▼\n       ┌──────────────────────────────┐\n       │          PostgreSQL          │\n       │ ready / deferred entries     │\n       │ active leases / attempt_state│\n       │ terminal / dlq entries       │\n       └──────────────┬───────────────┘\n                 │\n       ┌─────────┼─────────┐\n       ▼         ▼         ▼\n   ┌────────┐┌────────┐┌────────┐\n   │ Worker ││ Worker ││ Worker │\n   │ (Rust) ││ (PyO3) ││ (PyO3) │\n   └────────┘└────────┘└────────┘\n```\n\nAll coordination through Postgres. The Rust runtime owns dispatch, leases,\nheartbeats, rescue, rotation, prune, and shutdown for both languages. Mixed\nRust and Python workers coexist on the same queues. See\n[architecture overview](docs/architecture.md) for full details.\n\n## Workspace\n\n| Crate | Purpose |\n|---|---|\n| `awa` | Main crate — re-exports `awa-model` + `awa-worker` |\n| `awa-model` | Types, queries, migrations, admin ops |\n| `awa-macros` | `#[derive(JobArgs)]` proc macro |\n| `awa-worker` | Runtime: dispatch, heartbeat, maintenance |\n| `awa-ui` | Web UI (axum API + embedded React frontend) |\n| `awa-cli` | CLI binary (migrations, admin, serve) |\n| `awa-python` | PyO3 extension module (`pip install awa-pg`) |\n| `awa-testing` | Test helpers (`TestClient`) |\n\n## Documentation\n\n| Doc | Description |\n|---|---|\n| [Rust getting started](docs/getting-started-rust.md) | From `cargo add` to a job reaching `completed` |\n| [Python getting started](docs/getting-started-python.md) | From `pip install` to a job reaching `completed` |\n| [Deployment guide](docs/deployment.md) | Docker, Kubernetes, pool sizing, graceful shutdown |\n| [Migration guide](docs/migrations.md) | Fresh installs, upgrades, extracted SQL, rollback strategy |\n| [0.5 → 0.6 upgrade](docs/upgrade-0.5-to-0.6.md) | Step-by-step operator checklist for the staged storage transition |\n| [Configuration reference](docs/configuration.md) | `QueueConfig`, `ClientBuilder`, Python `start()`, env vars |\n| [Security \u0026 Postgres roles](docs/security.md) | Minimum-privilege roles, callback auth, operational guidance |\n| [Troubleshooting](docs/troubleshooting.md) | Stuck `running` jobs, leader delays, heartbeat timeouts |\n| [Architecture overview](docs/architecture.md) | System design, data flow, state machine, crash recovery |\n| [Web UI design](docs/ui-design.md) | API endpoints, pages, component library |\n| [Benchmarking notes](docs/benchmarking.md) | Methodology, headline numbers, how to run |\n| [Validation test plan](docs/test-plan.md) | Full test matrix with 100+ test cases |\n| [TLA+ correctness models](correctness/README.md) | Formal verification of core invariants |\n| [Grafana dashboards](docs/grafana/README.md) | Pre-built Prometheus dashboards for monitoring |\n\n\u003cdetails\u003e\n\u003csummary\u003eArchitecture Decision Records (ADRs)\u003c/summary\u003e\n\n- [001: Postgres-only](docs/adr/001-postgres-only.md)\n- [002: BLAKE3 uniqueness](docs/adr/002-blake3-uniqueness.md)\n- [003: Heartbeat + deadline hybrid](docs/adr/003-heartbeat-deadline-hybrid.md)\n- [004: PyO3 async bridge](docs/adr/004-pyo3-async-bridge.md)\n- [005: Priority aging](docs/adr/005-priority-aging.md)\n- [006: AwaTransaction as narrow SQL surface](docs/adr/006-awa-transaction.md)\n- [007: Periodic cron jobs](docs/adr/007-periodic-cron-jobs.md)\n- [008: COPY batch ingestion](docs/adr/008-copy-batch-ingestion.md)\n- [009: Python sync support](docs/adr/009-python-sync-support.md)\n- [010: Per-queue rate limiting](docs/adr/010-rate-limiting.md)\n- [011: Weighted concurrency](docs/adr/011-weighted-concurrency.md)\n- [012: Split hot and deferred job storage](docs/adr/012-hot-deferred-job-storage.md)\n- [013: Durable run leases and guarded finalization](docs/adr/013-run-lease-and-guarded-finalization.md)\n- [014: Structured progress and metadata](docs/adr/014-structured-progress.md)\n- [015: Builder-side post-commit lifecycle hooks](docs/adr/015-post-commit-lifecycle-hooks.md)\n- [016: Shared insert preparation and tokio-postgres adapter](docs/adr/016-bridge-adapters.md)\n- [017: Python insert-only transaction bridging](docs/adr/017-python-transaction-bridging.md)\n- [018: HTTP Worker for serverless job dispatch](docs/adr/018-http-worker.md)\n- [019: Queue Storage Engine](docs/adr/019-queue-storage-redesign.md)\n- [020: Dead Letter Queue](docs/adr/020-dead-letter-queue.md)\n- [021: Sequential callbacks and callback heartbeats](docs/adr/021-enhanced-external-wait.md)\n- [022: Descriptor catalog](docs/adr/022-descriptor-catalog.md)\n- [023: Receipt plane ring partitioning](docs/adr/023-receipt-plane-ring-partitioning.md)\n\nSee [docs/adr/README.md](docs/adr/README.md) for the index with status and supersession.\n\n\u003c/details\u003e\n\n## License\n\nMIT OR Apache-2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhardbyte%2Fawa","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhardbyte%2Fawa","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhardbyte%2Fawa/lists"}