diff --git a/docs/backend-roadmap.md b/docs/backend-roadmap.md index 13646733..318835b4 100644 --- a/docs/backend-roadmap.md +++ b/docs/backend-roadmap.md @@ -286,7 +286,7 @@ queue, cache, and repository ports defined in section 1. ### 5.2. Queue adapter (Apalis) -- [ ] 5.2.1. Implement `RouteQueue` using Apalis with PostgreSQL backend, +- [x] 5.2.1. Implement `RouteQueue` using Apalis with PostgreSQL backend, replacing the current stub adapter. - [ ] 5.2.2. Define job structs for `GenerateRouteJob` and `EnrichmentJob`. - [ ] 5.2.3. Implement retry policies with exponential backoff and dead-letter diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 8d9f4dad..69ff3d1f 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -637,7 +637,7 @@ The hexagonal boundary is enforced via visibility: |--------------------------------------|---------------------------|--------------------------------------------| | `ApalisRouteQueue

` | `pub` | Public adapter for domain use | | `ApalisPostgresProvider` | `pub` | Production `QueueProvider` implementation | -| `GenericApalisRouteQueue` | Internal; not re-exported | Generic adapter implementation | +| `GenericApalisRouteQueue` | `pub` | Generic adapter and BDD harness seam | | `QueueProvider` | `pub(crate)` | Test seam for provider abstraction | | `test_helpers::FakeQueueProvider` | `pub(crate)` (test-only) | In-memory test double | | `test_helpers::FailingQueueProvider` | `pub(crate)` (test-only) | Always-failing test double | @@ -659,10 +659,10 @@ Public production API: Implementation details within `outbound::queue`: -- `GenericApalisRouteQueue` – Declared `pub` inside the private - `apalis_route_queue` module, but not re-exported at crate root. It +- `GenericApalisRouteQueue` – Re-exported beside the production alias + because the BDD harness constructs the adapter with a test provider. It parameterises the adapter over the queue provider type `Q` so tests can - substitute doubles. + substitute doubles, while production code should prefer `ApalisRouteQueue

`. - `QueueProvider` – Declared `pub(crate)` inside the private `apalis_route_queue` module. Defines `async fn push_job(&self, payload: serde_json::Value) -> Result<(), JobDispatchError>` as the test diff --git a/docs/execplans/backend-5-2-1-apalis-route-queue.md b/docs/execplans/backend-5-2-1-apalis-route-queue.md index 871b6753..830f7346 100644 --- a/docs/execplans/backend-5-2-1-apalis-route-queue.md +++ b/docs/execplans/backend-5-2-1-apalis-route-queue.md @@ -1,888 +1,590 @@ -# Implement the Apalis-backed `RouteQueue` adapter (roadmap 5.2.1) +# Validate and close the Apalis-backed `RouteQueue` adapter (backend 5.2.1) This ExecPlan (execution plan) is a living document. The sections `Constraints`, `Tolerances`, `Risks`, `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds. -Status: IN PROGRESS +Status: COMPLETE This plan covers roadmap item 5.2.1 only: `Implement RouteQueue using Apalis with PostgreSQL backend, replacing the current stub adapter.` +No implementation work may begin from this plan until the plan is explicitly +approved. Approval authorizes the implementation and closure milestones below; +it does not authorize later queue tasks such as job-struct modelling, retry +policy, trace propagation, worker deployment, or route-submission dispatch. + ## Purpose / big picture -Today `backend/src/domain/ports/route_queue.rs` defines the `RouteQueue` port -with a single async method `enqueue`, but -`backend/src/outbound/queue/mod.rs` only provides `StubRouteQueue`, which -discards every job with a one-time warning. That keeps the crate compiling, but -means no enqueued work is persisted or available for downstream processing. - -After this change, the backend will have a real Apalis-backed driven adapter -for the `RouteQueue` port, built on `apalis-postgres` with PostgreSQL storage. -The adapter will accept typed plan payloads, serialize them into the Apalis job -table, and surface domain-owned `JobDispatchError` variants when the queue -infrastructure is unavailable or rejects a job. The repository will gain -focused unit and behavioural test coverage proving enqueue success, queue -unavailability errors, and rejected-job error mapping against a real embedded -PostgreSQL instance via `pg-embedded-setup-unpriv`. - -Because no runtime service currently dispatches to `RouteQueue` (the dispatch -points in `backend/src/domain/route_submission/mod.rs` are gated behind -`TODO(#276)`), this work intentionally stops at the adapter boundary. -Production request-path queue dispatch, worker consumption, job struct -definitions (roadmap 5.2.2), retry policies (5.2.3), and trace propagation -(5.2.4) remain later roadmap items. - -Observable success criteria: - -- `backend::outbound::queue` exports a real Apalis-backed `RouteQueue` - implementation instead of only a stub. -- The adapter uses `apalis-postgres` with `PostgresStorage` for job - persistence and maps connection or dispatch failures to - `JobDispatchError::Unavailable`. -- Job serialization failures (if the plan type cannot be encoded) map to - `JobDispatchError::Rejected`. -- `rstest` coverage proves happy, unhappy, and edge cases for the adapter. -- Behavioural (BDD) coverage via `rstest-bdd` proves adapter behaviour against - a real embedded PostgreSQL instance, not a handwritten mock. -- `docs/wildside-backend-architecture.md` records the scope decision that - 5.2.1 delivers the driven adapter but does not yet enable request-path queue - dispatch or worker consumption. -- `docs/backend-roadmap.md` marks 5.2.1 done only after all required gates - pass. -- `make check-fmt`, `make lint`, and `make test` pass with logs retained. +Wildside's route generation work needs a real queue adapter so route jobs can +be persisted for background processing instead of disappearing through a stub. +The domain already owns the queue contract through the `RouteQueue` port, and +the adapter boundary must keep Apalis, SQLx, and PostgreSQL details out of the +domain. + +The repository currently contains an Apalis/PostgreSQL queue adapter and tests +from prior work, but `docs/backend-roadmap.md` still lists 5.2.1 as open and +the existing plan history is stale. This plan therefore treats 5.2.1 as a +validation-and-closure activity: confirm the adapter still satisfies the +roadmap item on the current base, repair any defects found within the +tolerances below, run the required gates, record CodeRabbit review outcomes, +and mark the roadmap item done only after evidence is clean. + +Observable success means: + +- `backend::outbound::queue::ApalisRouteQueue` persists queue payloads through + Apalis PostgreSQL storage. +- `backend::domain::ports::RouteQueue` remains the only domain-facing queue + contract. +- Unit tests using `rstest` cover happy and unhappy adapter paths. +- Behavioural tests using `rstest-bdd` cover PostgreSQL-backed queue + persistence and failure behaviour. +- Documentation accurately explains the adapter scope, the SQLx pool used by + Apalis, and the fact that worker consumption remains future work. +- `make check-fmt`, `make lint`, and `make test` pass. +- CodeRabbit review has been run after each major milestone, and all concerns + have either been fixed or explicitly documented as not applicable. +- `docs/backend-roadmap.md` marks 5.2.1 done only after the evidence above is + available. ## Constraints -- Scope is roadmap item 5.2.1 only. Do not mark 5.2.2, 5.2.3, 5.2.4, or - 5.3.x done as part of this change unless the implementation is explicitly - widened and re-approved. -- Preserve hexagonal boundaries: - - `backend/src/domain/ports/route_queue.rs` remains the domain-owned - contract. The `RouteQueue` trait and `JobDispatchError` enum must not be - widened to accommodate Apalis-specific concerns. - - Apalis client, storage, and serialization details live under - `backend/src/outbound/queue/*`. - - Inbound adapters and domain services must not import Apalis types. -- Do not add queue dispatch calls to the `RouteSubmissionServiceImpl` or any - HTTP handler in this task. The `TODO(#276)` markers at - `backend/src/domain/route_submission/mod.rs` lines 241 and 295 are - intentionally left for a later integration step. -- Do not implement worker consumption (`Monitor`, `WorkerBuilder`) in this - task. The adapter covers the "push" side only; the "pull" side belongs to - roadmap items 5.2.2 through 5.3.1. -- Replace the production stub adapter with a real driven adapter, but preserve - lightweight in-memory or fixture doubles for tests that do not need - PostgreSQL. -- Use `apalis-postgres` (from the `apalis` 1.x release candidate family) for - PostgreSQL-backed job storage as specified by the roadmap. -- Use `rstest` for focused unit coverage and `rstest-bdd` for behavioural - coverage. -- Ensure the existing Postgres-backed suites still run through - `pg-embedded-setup-unpriv` as part of the full `make test` gate. -- Keep files under 400 lines by splitting queue code into coherent modules if - needed. -- New public Rust APIs must carry Rustdoc comments and examples that follow - `docs/rust-doctest-dry-guide.md`. -- Update documentation in en-GB-oxendict style. -- Follow the adapter pattern established by `RedisRouteCache` in - `backend/src/outbound/cache/redis_route_cache.rs`: use a - `ConnectionProvider`-style trait to abstract the storage backend, keep serde - bounds on the adapter (not the port), and provide a `FakeProvider` for unit - tests. +- Do not start implementation until this DRAFT plan has explicit approval. +- Keep scope to backend roadmap item 5.2.1. Do not implement or mark done + 5.2.2, 5.2.3, 5.2.4, 5.3.1, or any route-submission dispatch item. +- Preserve hexagonal architecture. Domain and inbound modules must not import + Apalis, SQLx, or concrete outbound queue types. +- Keep `RouteQueue` and `JobDispatchError` domain-owned. Do not widen the port + just to expose Apalis details. +- Keep worker consumption out of scope. No `WorkerBuilder`, `Monitor`, retry + policy, dead-letter queue, or worker deployment work belongs in this plan. +- Keep request-path dispatch out of scope unless the plan is revised and + re-approved. The current `TODO(#276)` queue-dispatch markers in + `backend/src/domain/route_submission/mod.rs` remain later work. +- Retain `StubRouteQueue` for tests and non-PostgreSQL development paths if it + is still used. +- Use `apalis-postgres` with PostgreSQL storage as required by the roadmap. +- Use `rstest` for focused unit tests and `rstest-bdd` for behavioural tests. +- Use the existing `pg-embedded-setup-unpriv` test infrastructure for live + PostgreSQL tests. +- Keep source files below 400 lines, or split them before completing the + milestone. +- Keep documentation in en-GB-oxendict style and follow + `docs/documentation-style-guide.md`. +- Prefer Makefile gates over raw tool invocations for final validation. +- Run tests, formatting, and linting sequentially, not in parallel. +- Capture long command output with `tee` under `/tmp`. +- Commit each approved implementation milestone only after its gate passes. +- Do not mark the roadmap item done until all required gates and CodeRabbit + review concerns are clear. ## Tolerances (exception triggers) -- Scope tolerance: if the work requires changing the `RouteQueue` port trait - signature or `JobDispatchError` enum beyond what is already defined, the work - should be stopped and escalated. -- Runtime-wiring tolerance: if a real Apalis adapter cannot be introduced - without also wiring new server/application configuration or modifying - `state_builders.rs`, the work should be stopped and an explicit decision - made about whether that extra wiring belongs in 5.2.1. -- Dependency tolerance: adding `apalis`, `apalis-core`, `apalis-sql`, and - `apalis-postgres` (plus `sqlx` if not already present) is expected. If more - than two additional production dependencies beyond these are required, the - work should be stopped and the trade-off reviewed. -- Error-contract tolerance: if Apalis or SQLx failures cannot be mapped - cleanly into the existing `JobDispatchError::{Unavailable, Rejected}` shape, - the work should be stopped and the domain error contract revisited before - proceeding. -- Test-harness tolerance: the implementation uses the existing - `pg-embedded-setup-unpriv` infrastructure for live adapter tests (the same - harness used by Diesel repository tests). If the embedded PostgreSQL cluster - cannot be started, the work should be stopped and the blocker documented - before continuing. -- Migration tolerance: if Apalis requires database migrations that conflict - with or duplicate existing Diesel migrations, the work should be stopped and - escalated. `PostgresStorage::setup()` creates its own tables; this must be - verified to coexist safely with the existing Diesel-managed schema. -- Gate tolerance: if `make check-fmt`, `make lint`, or `make test` fail after - three repair loops, the work should be stopped and the failing logs captured - instead of pushing past the quality gates. -- Environment tolerance: if embedded PostgreSQL cannot start locally, the work - should be stopped and the exact blocker plus the command output documented. -- File-size tolerance: if any single source file exceeds 400 lines, it should - be split into coherent sub-modules before proceeding. +- Scope: stop and escalate if satisfying 5.2.1 requires changing more than 12 + production source files or more than 800 net lines outside tests and + documentation. +- Port shape: stop and escalate before changing the `RouteQueue` trait + signature or adding Apalis-specific variants to `JobDispatchError`. +- Runtime wiring: stop and escalate before wiring `RouteQueue` into + `RouteSubmissionServiceImpl`, HTTP handlers, or server startup. +- Dependencies: `apalis-core`, `apalis-postgres`, and `sqlx` are expected. Stop + and escalate if more than two additional production dependencies are needed. +- Persistence schema: stop and escalate if Apalis table setup conflicts with + Diesel migrations or requires Diesel-managed Apalis migrations. +- Test harness: stop and document the blocker if embedded PostgreSQL cannot be + started with the existing `pg-embedded-setup-unpriv` flow. +- Verification: stop and document logs if `make check-fmt`, `make lint`, or + `make test` still fail after three focused repair loops. +- CodeRabbit: stop and document the concern if `coderabbit review --agent` + reports a finding that would require widening the approved scope. ## Risks -- Risk: Apalis 1.x is in release-candidate status, not a stable 1.0 release. - The API surface may change between release candidates. - Severity: medium. - Likelihood: medium. - Mitigation: pin the `apalis-postgres` version tightly in `Cargo.toml` (for - example, `"1.0.0-rc.6"`) and document the pinned version in the decision - log. If a breaking change is encountered, evaluate whether an older RC or - the 0.7.x stable line is more appropriate. - -- Risk: `PostgresStorage::setup()` creates its own tables in the connected - database. These tables may conflict with existing Diesel-managed migrations - or with `pg-embedded-setup-unpriv` template databases. - Severity: high. - Likelihood: medium. - Mitigation: call `PostgresStorage::setup()` after Diesel migrations in test - setup, and verify that the Apalis tables do not collide with existing table - names. If collisions occur, use a separate PostgreSQL schema or database for - the queue. - -- Risk: the `RouteQueue` port is generic over `Plan` via an associated type, - so the Apalis adapter must choose serialization bounds without leaking them - into the domain. - Severity: high. - Likelihood: medium. - Mitigation: keep `Serialize` bounds on the adapter implementation only - (not on the port trait), following the pattern established by - `RedisRouteCache`. Use `serde_json::Value` as the intermediate payload - format between the adapter and provider. Test with a representative fixture - plan type. - -- Risk: Apalis uses `sqlx` for PostgreSQL access whereas the existing - repository layer uses `diesel-async`. Two connection pool implementations - coexisting may introduce complexity. - Severity: medium. - Likelihood: high. - Mitigation: the Apalis adapter owns its own `sqlx::PgPool`, which is - separate from the Diesel `bb8` pool. Both connect to the same PostgreSQL - instance but through independent pool implementations. Document this - dual-pool arrangement in the architecture doc. - -- Risk: the roadmap text says "Apalis with PostgreSQL backend", but the - architecture document's code samples reference Redis as the job broker. The - implementation must follow the roadmap, not the older architecture prose. - Severity: low. - Likelihood: low. - Mitigation: use `apalis-postgres` as specified by the roadmap. Record the - divergence from older architecture prose in the decision log and update the - architecture document to reflect the chosen backend. - -- Risk: full-gate failures may come from the existing embedded-Postgres setup, - not from the Apalis adapter. - Severity: medium. - Likelihood: medium. - Mitigation: retain logs with `tee`, rely on `make test` so - `PG_EMBEDDED_WORKER` is wired automatically, and record any environment - failures explicitly before judging the feature incomplete. - -## Agent team and ownership - -This implementation should use an explicit agent team. One person may play more -than one role, but the ownership boundaries should remain visible. - -- Coordinator agent: - owns sequencing, keeps this ExecPlan current, enforces tolerances, collects - gate evidence, and decides when roadmap item 5.2.1 is ready to close. - -- Queue adapter agent: - owns `backend/Cargo.toml` dependency additions and - `backend/src/outbound/queue/*`, including the Apalis storage wrapper, adapter - struct, error translation, and connection provider trait. - -- Test harness agent: - owns `backend/tests/support/*` additions needed to provision an - Apalis-compatible PostgreSQL database within the existing - `pg-embedded-setup-unpriv` infrastructure, and any shared fixtures for queue - integration tests. - -- Quality Assurance (QA) agent: - owns adapter `rstest` coverage plus `rstest-bdd` scenarios and feature files - proving happy, unhappy, and edge behaviour. - -- Documentation agent: - owns `docs/wildside-backend-architecture.md` and `docs/backend-roadmap.md`, - and updates the latter only after the coordinator confirms all gates passed. - -Hand-off order: - -1. Queue adapter agent adds the dependencies and module layout plus failing - unit tests. -2. Test harness agent lands the Postgres integration fixture additions and - failing behavioural scenarios. -3. Queue adapter agent makes the Apalis adapter pass both focused suites. -4. QA agent broadens error-path and edge-case coverage. -5. Documentation agent records the design decision and closes the roadmap item. -6. Coordinator agent runs final gates and updates this ExecPlan. - -## Progress +- Risk: the current base already contains an Apalis adapter, so the remaining + work may be partly reconciliation rather than new implementation. + Mitigation: start by auditing the current symbols, tests, dependencies, and + docs. If the adapter is already correct, make only closure changes such as + roadmap updates and plan evidence. + +- Risk: Apalis release-candidate APIs may have shifted. Firecrawl research on + 2026-05-21 found `apalis-postgres` latest documentation at 1.0.0-rc.8, while + this repository currently pins older 1.0.0 release-candidate crates. + Mitigation: do not upgrade during 5.2.1 unless a gate failure requires it. + Record any version decision in this plan before changing dependencies. + +- Risk: Apalis uses SQLx while repository adapters use Diesel and `bb8`. + Mitigation: keep the Apalis SQLx `PgPool` contained in + `backend/src/outbound/queue/*` and document the dual-pool boundary. + +- Risk: `PostgresStorage::setup()` creates Apalis-owned tables that are not + represented in Diesel migrations. + Mitigation: verify setup is idempotent in tests and document that Apalis owns + its internal queue schema. + +- Risk: route-submission user flows may appear to require end-to-end queue + dispatch to satisfy "replacing the current stub adapter". + Mitigation: keep the acceptance boundary explicit. 5.2.1 closes when the + driven adapter exists and is validated; dispatch and worker processing are + later roadmap items. + +- Risk: `docs/users-guide.md` was requested, but this workspace does not + contain that file. + Mitigation: update `docs/developers-guide.md` and + `docs/wildside-backend-architecture.md` for internal behaviour. If no + end-user server behaviour changes, record that `docs/users-guide.md` is + absent and no user-facing guide update was possible. + +## Skills and reference documents + +Use the following skills while executing this plan: + +- `leta`: navigate symbols and references before editing code. +- `rust-router`: route any Rust language issue to the smallest useful Rust + skill. +- `hexagonal-architecture`: enforce port and adapter boundaries. +- `rust-async-and-concurrency`: review async ownership, storage handles, and + worker-adjacent lifecycle implications. +- `rust-errors`: review `JobDispatchError` mapping. +- `domain-cli-and-daemons`: keep future worker process concerns out of this + adapter milestone. +- `firecrawl-mcp`: verify external Apalis and PostgreSQL queue-tooling facts. +- `commit-message`: commit with a file-based commit message. +- `pr-creation` and `en-gb-oxendict-style`: prepare the draft pull request. + +Read these repository documents before implementation: -- [x] Review roadmap item 5.2.1, the current `RouteQueue` port, the stub - adapter, the architecture guidance, and the testing guides. -- [x] Confirm that no current runtime service or server builder dispatches to - `RouteQueue`; verify the change is adapter-first. -- [x] Draft this ExecPlan at - `docs/execplans/backend-5-2-1-apalis-route-queue.md`. -- [x] Await approval gate. -- [x] Run baseline queue tests (`/tmp/5-2-1-queue-baseline.out` - - 1 passing test confirmed) -- [x] Add `apalis-postgres` and related dependencies to `backend/Cargo.toml`. - - Updated wildside-data git rev from 894aa38 to 2db6cbf (rusqlite 0.32.1) - - Added apalis-postgres 1.0.0-rc.6 and sqlx 0.8 (postgres, runtime-tokio-rustls) - - `cargo check -p backend` succeeded - - Log: `/tmp/5-2-1-check-deps-updated-wildside.out` -- [x] Create `backend/src/outbound/queue/apalis_route_queue.rs` with the - adapter struct, connection provider trait, and error mapping. -- [x] Create `backend/src/outbound/queue/test_helpers.rs` with a fake provider +- `docs/backend-roadmap.md` +- `docs/wildside-backend-architecture.md` +- `docs/developers-guide.md` +- `docs/documentation-style-guide.md` +- `docs/rust-testing-with-rstest-fixtures.md` +- `docs/rstest-bdd-users-guide.md` +- `docs/rust-doctest-dry-guide.md` +- `docs/complexity-antipatterns-and-refactoring-strategies.md` +- `docs/pg-embed-setup-unpriv-users-guide.md` + +External references checked during planning: + +- `https://docs.rs/apalis-postgres/latest/apalis_postgres/` +- `https://apalis.dev/docs/introduction/quickstart` + +## Current repository orientation + +The current base contains these relevant files: + +- `backend/src/domain/ports/route_queue.rs` defines the domain-owned + `RouteQueue` trait and `JobDispatchError`. +- `backend/src/outbound/queue/mod.rs` exports queue adapters. +- `backend/src/outbound/queue/stub_route_queue.rs` retains the no-op stub. +- `backend/src/outbound/queue/apalis_route_queue.rs` contains the + Apalis-backed adapter and its focused unit tests. +- `backend/src/outbound/queue/test_helpers.rs` contains fake queue providers for unit tests. -- [x] Add focused `rstest` unit tests in - `backend/src/outbound/queue/apalis_route_queue.rs` (or a sibling `tests` - module) covering enqueue round-trip, unavailable backend, and rejected job - paths. -- [x] Extend `backend/tests/support/` with Apalis PostgreSQL setup helpers - that reuse the `pg-embedded-setup-unpriv` cluster. - - Added `setup_apalis_storage` helper to `backend/tests/support/embedded_postgres.rs` -- [x] Create `backend/tests/features/route_queue_apalis.feature` with BDD - scenarios. -- [x] Create `backend/tests/route_queue_apalis_bdd.rs` with step definitions - exercising the adapter against a real PostgreSQL instance. -- [x] Update `docs/wildside-backend-architecture.md` with the 5.2.1 scope - decision and dual-pool documentation. -- [x] Run `make check-fmt` and capture the log. - - Log: `/tmp/5-2-1-check-fmt-after.out` (passed after `make fmt`) -- [ ] Run `make lint` and capture the log. - - In progress: `/tmp/5-2-1-lint-attempt2.out` -- [ ] Run `make test` and capture the log. - - In progress: `/tmp/5-2-1-make-test-full.out` -- [ ] Mark `docs/backend-roadmap.md` item 5.2.1 done after all gates pass. - -## Surprises & discoveries +- `backend/tests/features/route_queue_apalis.feature` describes behavioural + queue scenarios. +- `backend/tests/route_queue_apalis_bdd.rs` implements the behavioural tests. +- `backend/tests/support/embedded_postgres.rs` contains embedded PostgreSQL + support, including Apalis storage setup helpers. +- `backend/src/domain/route_submission/mod.rs` still has queue-dispatch TODOs + and must remain out of scope for this plan. +- `backend/src/server/mod.rs` does not currently construct or inject a + route-queue dependency into route submission. +- `docs/wildside-backend-architecture.md` and `docs/developers-guide.md` + already mention Apalis queue adapter behaviour and must be checked against + the implementation before closure. + +Key terms: + +- Apalis is a Rust background task processing library. +- `apalis-postgres` is the Apalis backend that stores tasks in PostgreSQL. +- `PostgresStorage` is the Apalis storage type used for polling-backed + PostgreSQL queues. +- `PostgresStorage::setup()` provisions Apalis-owned queue tables. +- A driven adapter is outbound infrastructure code that implements a + domain-owned port. + +## Implementation plan + +Milestone 0: approval and baseline audit. + +After approval, confirm the branch and workspace: + +```bash +git branch --show-current +git status --short --branch +leta workspace add "$(pwd)" +``` -### Discovery 1: Dependency conflict blocked apalis-postgres integration (2026-04-03) - -**Issue**: `apalis-postgres` (either 1.0.0-rc.6 or apalis-sql 0.7.x) -could not be added due to a `libsqlite3-sys` native library version -conflict: - -- `wildside-data` (git dependency at rev 894aa38) depends on - `rusqlite` 0.31 → `libsqlite3-sys` 0.28 -- `pg-embed-setup-unpriv` 0.5.0 (used for test infrastructure) depends - on `postgresql_embedded` 0.20.1 → `sqlx` 0.8.6 → - `libsqlite3-sys` 0.30+ -- Cargo enforces that only one version of a native library (`sqlite3`) - can be linked per binary - -**Attempted resolutions (all failed)**: - -- Adjusting version constraints for `url`, `hex`, - `postgresql_embedded` -- Disabling sqlx default features -- Explicitly adding `rusqlite` 0.32 or `libsqlite3-sys` 0.30 to - backend/Cargo.toml -- Using `[patch.crates-io]` at workspace level (patches require - different source, can't patch crates.io with crates.io) -- Using `[workspace.dependencies]` (doesn't override git dependency - locks) -- Switching to apalis 0.7.x stable (still blocked by - pg-embed-setup-unpriv → sqlx 0.8) - -**Root cause**: The git dependency `wildside-data` is locked to a -specific revision that uses rusqlite 0.31, and Cargo cannot override -transitive dependencies of git sources. - -**Options forward**: - -1. **Update wildside-engine upstream**: Modify the wildside-engine - repository to use rusqlite 0.32+, then update the git revision in - this project - - Pros: Clean resolution, no workarounds - - Cons: Requires upstream coordination, blocks this task - -2. **Use apalis-core directly without apalis-postgres**: Implement a - custom PostgreSQL storage adapter using Diesel instead of sqlx - - Pros: Avoids sqlx entirely, keeps Diesel as single DB access layer - - Cons: Significantly more implementation work, diverges from roadmap plan - -3. **Use an alternate job queue library**: Evaluate alternatives like - `fang`, `tokio-cron-scheduler`, or custom implementation - - Pros: May avoid dependency conflicts - - Cons: Deviates from approved roadmap, requires re-evaluation of architecture - -4. **Defer 5.2.1 until wildside-data conflict is resolved**: Mark this - task as blocked and continue with other roadmap items - - Pros: Clean path forward once unblocked - - Cons: Delays queue functionality - -**Resolution (2026-04-03)**: Wildside-engine upstream was updated to -rusqlite 0.32.1 via commit 2db6cbf. Updated backend/Cargo.toml to use -this revision, which resolved the libsqlite3-sys conflict. -`cargo check -p backend` now succeeds with apalis-postgres 1.0.0-rc.6 -and sqlx 0.8 dependencies added successfully. - -## Decision log - -- Decision: 5.2.1 delivers the Apalis-driven adapter itself, not - request-path queue dispatch or worker consumption. - Rationale: no domain service currently dispatches to `RouteQueue` (gated - behind `TODO(#276)` in `route_submission`), and adding that behaviour would - spill into later roadmap items covering job structs, retry policies, and - worker deployment. - Date/Author: 2026-04-03 / planning team. - -- Decision: use `apalis-postgres` (1.0.0-rc family) rather than - `apalis-sql` with the `postgres` feature flag. - Rationale: `apalis-postgres` is the dedicated PostgreSQL crate with - purpose-built storage types (`PostgresStorage`, `PostgresStorageWithListener`) - and is the recommended path for PostgreSQL-backed queues in the Apalis - ecosystem. It provides `NOTIFY`/`SKIP LOCKED` support and heartbeat-based - orphan recovery. - Date/Author: 2026-04-03 / planning team. - -- Decision: use `sqlx::PgPool` for the Apalis adapter, coexisting with the - Diesel `bb8` pool used by repository adapters. - Rationale: Apalis is built on SQLx and expects a `sqlx::PgPool`. Attempting - to share the Diesel pool would require a brittle adapter layer. Both pools - connect to the same PostgreSQL instance with independent lifecycle - management. This dual-pool arrangement mirrors how the Redis cache adapter - uses its own `bb8-redis` pool independently of Diesel. - Date/Author: 2026-04-03 / planning team. - -- Decision: follow the `RedisRouteCache` adapter pattern — use a - `QueueProvider` trait to abstract the Apalis storage backend, keeping the - adapter generic over both the plan type and the provider. - Rationale: this pattern is proven in the codebase, enables unit testing with - a `FakeProvider`, and keeps the adapter decoupled from Apalis internals. - Date/Author: 2026-04-03 / planning team. - -- Decision: the Apalis adapter's PostgreSQL tables are created by - `PostgresStorage::setup()` at connection time, not by Diesel migrations. - Rationale: Apalis owns its table schema and the `setup()` call is - idempotent. Mixing Apalis schema into Diesel migrations would create a - coupling between the two object–relational mappers (ORMs). The adapter calls - `setup()` during construction, and tests call it during harness provisioning - after Diesel migrations complete. - Date/Author: 2026-04-03 / planning team. - -- Decision: use PostgreSQL rather than Advanced Message Queuing Protocol - (AMQP) brokers (RabbitMQ / LavinMQ) as the queue backend for 5.2.1, on the - explicit condition that the `QueueProvider` abstraction provides sufficient - isolation to migrate to an AMQP backend at a later date should volume - warrant it. - Rationale: PostgreSQL is already provisioned in production and in the test - harness (`pg-embedded-setup-unpriv`), so the adapter can be tested and - deployed with zero new infrastructure. AMQP would require a new broker - service in production, a new test harness (no embedded RabbitMQ equivalent - exists for Rust), and depends on the less mature `apalis-amqp` crate. The - hexagonal boundary — specifically the `QueueProvider` trait and the - domain-owned `RouteQueue` port — ensures that swapping to an - `ApalisAmqpProvider` is a contained adapter change: the domain port, all - `FakeQueueProvider` unit tests, and consuming services remain untouched. - If queue throughput or routing/fanout requirements outgrow PostgreSQL - `NOTIFY`/`SKIP LOCKED`, the migration path is to implement a new provider - behind the same trait and update the composition root. - Date/Author: 2026-04-03 / planning team. - -- Decision: the architecture document will be updated to reflect that the - queue backend is PostgreSQL (via `apalis-postgres`), not Redis, for job - storage. The older architecture prose references Redis as the job broker; - the roadmap explicitly specifies PostgreSQL. - Rationale: the roadmap is the authoritative source for delivery scope. The - architecture document must be updated to match. - Date/Author: 2026-04-03 / planning team. - -## Outcomes & retrospective - -(To be populated upon completion.) - -## Context and orientation - -The relevant current files are: - -- `backend/src/domain/ports/route_queue.rs` - defines the `RouteQueue` trait with a single async method `enqueue` and an - associated type `Plan: Send + Sync`. It also defines `JobDispatchError` with - two variants: `Unavailable { message: String }` (queue infrastructure is - down) and `Rejected { message: String }` (the job could not be acknowledged - or persisted). Both variants have auto-generated constructors via the - `define_port_error!` macro (for example, `JobDispatchError::unavailable("…")` - and `JobDispatchError::rejected("…")`). - -- `backend/src/outbound/queue/mod.rs` - now exposes `StubRouteQueue

`, which is generic over any `P: Send + Sync` - and implements `RouteQueue` with a no-op `enqueue` that logs a warning once - per process, alongside the Apalis adapter re-exports - `ApalisPostgresProvider`, `ApalisRouteQueue

`, and - `GenericApalisRouteQueue`. The stub still has a small unit test - confirming enqueue succeeds. - -- `backend/src/domain/route_submission/mod.rs` - contains two `TODO(#276)` markers (lines 241 and 295) where queue dispatch - will eventually be wired. This integration is out of scope for 5.2.1. - -- `backend/src/server/state_builders.rs` - does not currently construct or inject a `RouteQueue`. The server composition - root is unchanged by this task. - -- `backend/src/outbound/cache/redis_route_cache.rs` - is the reference implementation for the adapter pattern: it defines a - `ConnectionProvider` trait, a `RedisPoolProvider` concrete implementation, - and a `GenericRedisRouteCache` struct parameterised over the plan type - and provider. Error mapping consolidates all infrastructure failures into - domain error variants. A `FakeProvider` in - `backend/src/outbound/cache/test_helpers.rs` enables unit testing without - Redis. - -- `backend/tests/support/embedded_postgres.rs` - provides `provision_template_database()` for test database creation using - `pg-embedded-setup-unpriv`. This harness will be reused for the queue - adapter's integration tests. +The expected branch is `backend-5-2-1-apalis-route-queue`. The working tree +should be clean before implementation begins. If there are user changes, do +not overwrite them. -- `docs/wildside-backend-architecture.md` - describes the hexagonal architecture, the `RouteQueue` port, and the - outbound adapter pattern. It will be updated to document the Apalis adapter - scope and the dual-pool (SQLx + Diesel) arrangement. +Use Leta and plain text search for non-code documents to confirm the current +adapter state: -- `docs/backend-roadmap.md` - lists 5.2.1 as the first queue adapter task. The item will be marked done - only after all gates pass. - -Key terminology: - -- **Apalis**: a Rust background job processing library that uses tower - `Service` semantics. Jobs are defined as serializable structs; workers - consume them from a storage backend. -- **`apalis-postgres`**: the PostgreSQL storage backend for Apalis, using SQLx - and `NOTIFY`/`SKIP LOCKED` for reliable job delivery. -- **`PostgresStorage`**: the concrete Apalis type that implements job - enqueueing and consumption against a PostgreSQL database. -- **`pg-embedded-setup-unpriv`**: a crate used in this repository to provision - ephemeral PostgreSQL clusters for integration testing without requiring - system-level PostgreSQL installation. -- **Driven adapter**: an outbound adapter that implements a domain port, - translating domain calls into infrastructure operations (for example, - `enqueue` → Apalis `push_request`). -- **Connection provider**: a trait abstracting the storage backend so the - adapter can be tested with fakes in unit tests and with real PostgreSQL in - integration tests. - -## Plan of work - -Stage A: add dependencies and scaffold the adapter module. - -Add `apalis-core` and `apalis-postgres` to `backend/Cargo.toml` as production -dependencies, plus `sqlx` with the `postgres` and `runtime-tokio` features (if -not already present). The `apalis-postgres` crate transitively brings in -`apalis-core` and `sqlx`, but explicit entries ensure version pinning. Pin -`apalis-postgres` to a specific 1.0.0 release candidate version. - -Restructure `backend/src/outbound/queue/` from a single `mod.rs` into a -multi-file module: - -- keep `backend/src/outbound/queue/mod.rs` as a thin module header with - re-exports; -- add `backend/src/outbound/queue/stub_route_queue.rs` containing the existing - `StubRouteQueue` (moved from `mod.rs`); -- add `backend/src/outbound/queue/apalis_route_queue.rs` containing the real - adapter; -- add `backend/src/outbound/queue/test_helpers.rs` (gated behind `#[cfg(test)]`) - containing a `FakeQueueProvider` for unit tests. - -The adapter should follow this shape: - -```rust -// backend/src/outbound/queue/apalis_route_queue.rs - -use async_trait::async_trait; -use serde::Serialize; -use serde_json::Value; -use std::marker::PhantomData; - -use crate::domain::ports::{JobDispatchError, RouteQueue}; - -/// Abstracts the queue storage backend for testability. -#[async_trait] -pub(crate) trait QueueProvider: Send + Sync { - /// Push a JSON job payload into the queue. - async fn push_job(&self, payload: Value) -> Result<(), JobDispatchError>; -} - -/// Apalis-backed `RouteQueue` adapter using PostgreSQL storage. -#[derive(Debug, Clone)] -pub struct GenericApalisRouteQueue { - provider: Q, - _plan: PhantomData P>, -} - -/// Production type alias with the real Apalis PostgreSQL provider. -pub type ApalisRouteQueue

= - GenericApalisRouteQueue; - -#[async_trait] -impl RouteQueue for GenericApalisRouteQueue -where - P: Serialize + Send + Sync, - Q: QueueProvider, -{ - type Plan = P; - - async fn enqueue( - &self, - plan: &Self::Plan, - ) -> Result<(), JobDispatchError> { - let payload = serde_json::to_value(plan) - .map_err(|e| JobDispatchError::rejected( - format!("Failed to serialize plan: {e}") - ))?; - self.provider.push_job(payload).await - } -} +```bash +leta grep "RouteQueue|ApalisRouteQueue|GenericApalisRouteQueue" backend \ + -k trait,struct,enum,function,method --head 200 +rg -n "5.2.1|Apalis|RouteQueue|PostgresStorage" \ + docs/backend-roadmap.md docs/wildside-backend-architecture.md \ + docs/developers-guide.md backend/Cargo.toml ``` -The `ApalisPostgresProvider` wraps `apalis_postgres::PostgresStorage` and maps -its errors to `JobDispatchError` variants: - -```rust -// backend/src/outbound/queue/apalis_route_queue.rs (continued) - -use apalis_postgres::PostgresStorage; -use sqlx::PgPool; - -/// Real provider backed by Apalis PostgreSQL storage. -#[derive(Debug, Clone)] -pub struct ApalisPostgresProvider { - storage: PostgresStorage, -} - -#[async_trait] -impl QueueProvider for ApalisPostgresProvider { - async fn push_job( - &self, - payload: Value, - ) -> Result<(), JobDispatchError> { - let mut storage = self.storage.clone(); - storage - .push(payload) - .await - .map_err(|e| JobDispatchError::unavailable( - format!("Failed to enqueue job: {e}") - )) - } -} +Record the findings in `Surprises & Discoveries` before editing code. + +Milestone 1: targeted adapter verification. + +Run the focused queue unit and behavioural suites. These commands intentionally +use Cargo directly because they isolate the feature before the full Makefile +gates. + +```bash +set -o pipefail +cargo test -p backend outbound::queue --lib 2>&1 \ + | tee /tmp/backend-5-2-1-queue-unit.out + +set -o pipefail +cargo test -p backend --test route_queue_apalis_bdd 2>&1 \ + | tee /tmp/backend-5-2-1-queue-bdd.out ``` -Note: the exact Apalis API may differ from what is shown above. The -implementation agent must consult the `apalis-postgres` 1.0.0-rc documentation -at build time to determine the correct method names (`push_request`, -`push_job`, `enqueue`, etc.) and adjust accordingly. The key invariant is that -the provider maps all Apalis/SQLx errors to `JobDispatchError::Unavailable` -and all serialization errors to `JobDispatchError::Rejected`. - -Stage B: lock behaviour with focused unit tests first. - -Before chasing integration wiring, add `rstest` coverage around the adapter's -smallest meaningful behaviours, using the `FakeQueueProvider`: - -- `enqueue` with a valid plan succeeds and the fake provider receives the - serialized payload; -- `enqueue` with a plan that fails serialization returns - `JobDispatchError::Rejected`; -- `enqueue` when the provider returns an error returns - `JobDispatchError::Unavailable`; -- adapter constructors preserve stable defaults; -- the stub adapter continues to work unchanged (regression). - -Use a simple fixture plan type (for example, a `TestPlan` struct deriving -`Debug`, `Clone`, `PartialEq`, `Eq`, `Serialize`, and `Deserialize` with a -single `name: String` field) in these tests. The goal is to pin the error -contract and round-trip semantics -before adding broader behavioural scenarios. - -Stage C: add PostgreSQL integration harness and behavioural coverage. - -Extend the existing `backend/tests/support/` infrastructure to provision an -Apalis-compatible database. The approach is: - -1. Reuse `pg-embedded-setup-unpriv` to create an embedded PostgreSQL cluster - (the same cluster used by Diesel repository tests). -2. After Diesel migrations complete, call `PostgresStorage::setup()` on the - same database URL to create the Apalis job tables. -3. Create a `sqlx::PgPool` connected to the test database for the Apalis - adapter. - -Then add a behavioural suite at -`backend/tests/route_queue_apalis_bdd.rs` with a companion feature file at -`backend/tests/features/route_queue_apalis.feature`. Recommended scenarios: - -- Happy path: enqueueing a plan persists it in the PostgreSQL job table. -- Happy path: enqueueing multiple distinct plans does not overwrite each other. -- Unhappy path: enqueueing when the database connection is invalid returns an - unavailable error. -- Edge path: enqueueing the same plan twice results in two independent jobs - (no deduplication at the adapter level). - -Because this is a driven-adapter feature rather than an HTTP endpoint, the BDD -world can operate directly on the adapter and PostgreSQL harness instead of -booting the Actix server. - -The BDD World struct should follow the pattern established by existing suites: - -```rust -struct World { - mode: TestMode, - db_context: Option, - enqueue_result: Option>, -} +If both pass, update `Progress` with the log paths and move to Milestone 2. If +either fails, repair only adapter, test-harness, or documentation defects that +are inside this plan's tolerances. Do not wire request-path dispatch to make +these tests pass. + +Run CodeRabbit after this milestone: + +```bash +coderabbit review --agent ``` -Use `is_skipped(world)` gates for cluster setup failures, matching the pattern -in `backend/tests/user_state_startup_modes_bdd.rs`. - -Stage D: document the architectural scope explicitly. - -Update `docs/wildside-backend-architecture.md` to record: - -- Roadmap item 5.2.1 introduces the Apalis-backed `RouteQueue` adapter with - PostgreSQL storage, but does not yet enable route queue dispatch in runtime - request flows or worker consumption. -- The queue adapter uses `sqlx::PgPool`, coexisting with the Diesel `bb8` - pool used by repository adapters. Both pools connect to the same PostgreSQL - instance. -- The adapter pattern follows the same `ConnectionProvider`-style abstraction - used by the Redis cache adapter. -- The Apalis job tables are managed by `PostgresStorage::setup()`, not by - Diesel migrations. -- The architecture document's earlier references to Redis as the job broker are - superseded by the roadmap's specification of PostgreSQL. - -Only after the implementation, tests, and full gates pass should -`docs/backend-roadmap.md` mark 5.2.1 done. - -Stage E: replay the full repository gates. - -Once focused queue tests are green, run the required repository gates with log -capture. `make test` is required even though the queue work uses its own SQLx -pool, because the repo's standard backend suites still rely on -`pg-embed-setup-unpriv` and the roadmap item cannot close without a clean -global gate run. - -## Concrete steps - -All commands should be run from `/home/user/project`. `set -o pipefail` and -`tee` should be used for every meaningful command so the exit code survives -truncation and the log is retained. - -1. Capture the current stub-only baseline and confirm there is no Apalis - adapter yet. - - ```bash - set -o pipefail - cargo test -p backend outbound::queue --lib 2>&1 \ - | tee /tmp/5-2-1-queue-baseline.out - ``` - - Expected pre-change signal: - - ```plaintext - test ...stub_queue_enqueue_succeeds ... ok - test result: ok. 1 passed; 0 failed; - ``` - -2. Add Apalis dependencies to `backend/Cargo.toml` and verify the workspace - compiles. - - ```bash - set -o pipefail - cargo check -p backend 2>&1 | tee /tmp/5-2-1-check-deps.out - ``` - -3. Scaffold the adapter module, move the stub, and add focused `rstest` - coverage. Then run the targeted queue tests until they pass. - - ```bash - set -o pipefail - cargo test -p backend outbound::queue --lib 2>&1 \ - | tee /tmp/5-2-1-queue-unit.out - ``` - - Expected green-state examples: - - ```plaintext - test ...apalis_queue_enqueue_round_trips ... ok - test ...apalis_queue_maps_provider_error_to_unavailable ... ok - test ...apalis_queue_maps_serialization_failure_to_rejected ... ok - test ...stub_queue_enqueue_succeeds ... ok - test result: ok. - ``` - -4. Add the PostgreSQL integration harness and BDD coverage, then run the - focused scenarios. - - ```bash - set -o pipefail - cargo test -p backend --test route_queue_apalis_bdd \ - 2>&1 | tee /tmp/5-2-1-queue-bdd.out - ``` - - Expected green-state examples: - - ```plaintext - test route_queue_apalis_bdd::plan_is_persisted_in_queue ... ok - test route_queue_apalis_bdd::invalid_connection_returns_unavailable ... ok - test result: ok. - ``` - -5. Update documentation after the focused suites are stable. - - ```bash - set -o pipefail - make markdownlint 2>&1 | tee /tmp/5-2-1-markdownlint.out - set -o pipefail - make nixie 2>&1 | tee /tmp/5-2-1-nixie.out - ``` - -6. Run the required full gates before marking the roadmap item done. - - ```bash - set -o pipefail - make check-fmt 2>&1 | tee /tmp/5-2-1-check-fmt.out - set -o pipefail - make lint 2>&1 | tee /tmp/5-2-1-lint.out - set -o pipefail - make test 2>&1 | tee /tmp/5-2-1-test.out - ``` - -7. Mark `docs/backend-roadmap.md` item 5.2.1 done only after the gate logs - are clean, then append the log paths and outcome summary to this ExecPlan. +Fix all in-scope concerns before continuing. If CodeRabbit requests +out-of-scope work, document it and escalate. -## Validation and acceptance +Commit the passing milestone with a file-based commit message if any files +changed. -The implementation is done only when all of the following are true: - -- Adapter behaviour: - - `ApalisRouteQueue

` implements `RouteQueue` using - `apalis-postgres` `PostgresStorage`. - - `enqueue` serializes the plan and pushes it to the PostgreSQL-backed Apalis - job table. - - serialization failures return `Err(JobDispatchError::Rejected { .. })`. - - connection or storage failures return - `Err(JobDispatchError::Unavailable { .. })`. -- Architectural boundaries: - - domain code still knows only the `RouteQueue` trait and - `JobDispatchError`; - - no inbound adapter or domain service imports Apalis or SQLx types; - - runtime request-path queue dispatch is not enabled as an accidental side - effect; - - the `TODO(#276)` markers in `route_submission` remain unchanged. -- Tests: - - new `rstest` coverage passes for unit-level adapter behaviour; - - new `rstest-bdd` coverage passes against a real embedded PostgreSQL - instance; - - existing Postgres-backed suites still pass through `make test`; - - the existing `StubRouteQueue` tests still pass. -- Documentation: - - `docs/wildside-backend-architecture.md` records the 5.2.1 scope decision - and dual-pool architecture; - - `docs/backend-roadmap.md` marks 5.2.1 done only after the gates pass. -- Gates: - - `make check-fmt` passes; - - `make lint` passes; - - `make test` passes. +Milestone 2: documentation reconciliation. -## Idempotence and recovery +Review and update only documentation that is stale or inaccurate: + +- `docs/wildside-backend-architecture.md` must explain that 5.2.1 covers the + driven queue adapter, not worker consumption or request dispatch. +- `docs/developers-guide.md` must explain adapter boundaries, dependency + visibility, and how to run the queue tests. In the approved implementation + baseline, `GenericApalisRouteQueue` is re-exported for the BDD harness + seam even though production code should prefer `ApalisRouteQueue

`. +- `docs/pg-embed-setup-unpriv-users-guide.md` must be updated only if the + embedded PostgreSQL setup expectations changed. +- `docs/users-guide.md` must be updated if it exists and user-facing server + behaviour changed. If the file is still absent and the server interface did + not change, record that no user-guide update was applicable. + +Run documentation checks after documentation edits: + +```bash +set -o pipefail +make markdownlint 2>&1 | tee /tmp/backend-5-2-1-markdownlint.out + +set -o pipefail +make nixie 2>&1 | tee /tmp/backend-5-2-1-nixie.out +``` + +Run CodeRabbit again: -All steps in this plan are designed to be re-runnable: +```bash +coderabbit review --agent +``` -- `cargo check` and `cargo test` are idempotent. -- `PostgresStorage::setup()` is idempotent (creates tables if they do not - exist). -- Diesel migrations are idempotent (tracked by migration version). -- `pg-embedded-setup-unpriv` provisions fresh template databases on each test - run. -- If a step fails halfway, re-running it from the beginning is safe. No - destructive or irreversible operations are performed. +Fix all in-scope concerns before continuing. Commit the passing milestone if +any files changed. -If the embedded PostgreSQL cluster fails to start (a known environment issue in -this container), `/dev/null` should be checked to confirm it is a character -device (`ls -l /dev/null` should show `crw-rw-rw-`). If it is a regular file, -it should be recreated with `mknod -m 666 /dev/null c 1 3`. +Milestone 3: full quality gates. -## Interfaces and dependencies +Run the required repository gates sequentially: -### Dependencies to add to `backend/Cargo.toml` +```bash +set -o pipefail +make check-fmt 2>&1 | tee /tmp/backend-5-2-1-check-fmt.out -```toml -# Queue adapter (Apalis with PostgreSQL) -apalis-core = "1.0.0-rc.7" -apalis-postgres = "1.0.0-rc.6" +set -o pipefail +make lint 2>&1 | tee /tmp/backend-5-2-1-lint.out -# SQLx for Apalis PostgreSQL pool (if not already present) -sqlx = { version = "0.8", default-features = false, features = ["postgres", "runtime-tokio-rustls"] } +set -o pipefail +make test 2>&1 | tee /tmp/backend-5-2-1-test.out ``` -The exact version of `apalis-postgres` should be verified against the latest -available release candidate at implementation time. If the version has advanced -beyond `1.0.0-rc.6`, evaluate the changelog for breaking changes and pin -accordingly. +If a gate fails, repair only in-scope defects and rerun the failing gate. After +the failing gate passes, rerun any later gates in sequence. Stop after three +repair loops for the same gate. -### Module layout after implementation +Run CodeRabbit after full gates: -```plaintext -backend/src/outbound/queue/ -├── mod.rs (module header, re-exports) -├── stub_route_queue.rs (moved from mod.rs, unchanged) -├── apalis_route_queue.rs (new: adapter struct, provider trait, error mapping) -└── test_helpers.rs (new, #[cfg(test)]: FakeQueueProvider) +```bash +coderabbit review --agent ``` -### Key types and traits +Milestone 4: roadmap closure. + +Only after Milestones 1 through 3 pass and CodeRabbit concerns are clear, +update `docs/backend-roadmap.md`: + +```markdown +- [x] 5.2.1. Implement `RouteQueue` using Apalis with PostgreSQL backend, + replacing the current stub adapter. +``` -In `backend/src/outbound/queue/apalis_route_queue.rs`, define: +Add a short execution note under the item if useful, pointing to the queue +tests and this ExecPlan. Do not mark 5.2.2 or later items done. -```rust -/// Abstracts the queue storage backend for testability. -#[async_trait] -pub(crate) trait QueueProvider: Send + Sync { - async fn push_job(&self, payload: Value) -> Result<(), JobDispatchError>; -} +Run the final required gates again if the roadmap edit triggers formatting or +lint changes: -/// Apalis-backed provider using PostgreSQL storage. -#[derive(Debug, Clone)] -pub struct ApalisPostgresProvider { /* PostgresStorage */ } +```bash +set -o pipefail +make check-fmt 2>&1 | tee /tmp/backend-5-2-1-final-check-fmt.out -/// Generic queue adapter parameterised over plan type and provider. -#[derive(Debug, Clone)] -pub struct GenericApalisRouteQueue { /* provider + PhantomData */ } +set -o pipefail +make lint 2>&1 | tee /tmp/backend-5-2-1-final-lint.out -/// Production type alias. -pub type ApalisRouteQueue

= - GenericApalisRouteQueue; +set -o pipefail +make test 2>&1 | tee /tmp/backend-5-2-1-final-test.out ``` -In `backend/src/outbound/queue/test_helpers.rs`, define: +Commit the roadmap closure separately. -```rust -/// Fake provider that records pushed payloads for assertion. -pub(crate) struct FakeQueueProvider { /* internal state */ } +Milestone 5: push and pull request. -/// Fake provider that always returns an error. -pub(crate) struct FailingQueueProvider { /* error message */ } +Push the branch and set upstream tracking: + +```bash +git push -u origin backend-5-2-1-apalis-route-queue ``` -### Test files +Create or update a draft pull request. The title must include the roadmap item +as `(backend-5.2.1)`. The body must mention this ExecPlan and include a +`## References` section with the Lody session link from: -```plaintext -backend/tests/features/route_queue_apalis.feature (Gherkin scenarios) -backend/tests/route_queue_apalis_bdd.rs (step definitions) +```bash +echo "${LODY_SESSION_ID}" ``` -## Approval / implementation gate +The draft pull request for the plan itself must say that implementation awaits +explicit approval. The implementation pull request, if later created from the +approved work, must include gate logs and CodeRabbit outcomes. + +## Validation and acceptance + +5.2.1 can be marked complete only when all of the following are true: + +- `ApalisRouteQueue

` implements `RouteQueue` using + `apalis-postgres` PostgreSQL storage. +- Queue payload serialization failures map to `JobDispatchError::Rejected`. +- PostgreSQL, SQLx, or Apalis enqueue failures map to + `JobDispatchError::Unavailable`. +- The domain and inbound layers do not import Apalis or SQLx. +- Request-path dispatch and worker consumption remain out of scope. +- Focused `rstest` unit tests pass. +- `rstest-bdd` queue scenarios pass against embedded PostgreSQL. +- Documentation accurately states the adapter's scope and operational + boundaries. +- `make check-fmt`, `make lint`, and `make test` pass. +- `coderabbit review --agent` has no unresolved in-scope concerns. +- `docs/backend-roadmap.md` marks only item 5.2.1 done. + +`proptest`, `kani`, and `verus` are not required for this milestone unless the +implementation introduces new pure invariants beyond serialization and +infrastructure error mapping. The queue adapter is primarily an external +system integration, so focused unit tests and PostgreSQL-backed behavioural +tests provide the appropriate level of rigour for 5.2.1. + +## Idempotence and recovery + +The validation commands are safe to rerun. `PostgresStorage::setup()` is +expected to be idempotent. Embedded PostgreSQL tests provision isolated +databases through the existing test harness. + +If a command is interrupted, rerun the same command with the same log path or a +new path with an `-attempt-N` suffix. If embedded PostgreSQL setup fails for an +environmental reason, record the exact log path in `Surprises & Discoveries` +and stop rather than replacing the repository's test infrastructure. + +No destructive Git commands are required. Do not use `git reset --hard` or +`git checkout --` to discard work unless the user explicitly asks for that +operation. + +## Progress + +- [x] 2026-05-21: Loaded `leta`, `rust-router`, `hexagonal-architecture`, + `execplans`, `firecrawl-mcp`, `pr-creation`, and supporting Rust skills for + planning. +- [x] 2026-05-21: Created the Leta workspace for this worktree. +- [x] 2026-05-21: Renamed the local branch to + `backend-5-2-1-apalis-route-queue`. +- [x] 2026-05-21: Used a Wyvern planning team to inspect roadmap/docs, current + queue code, and test coverage. +- [x] 2026-05-21: Used Firecrawl to verify current Apalis/PostgreSQL + documentation and queue concepts. +- [x] 2026-05-21: Drafted this approval-gated ExecPlan. +- [x] 2026-05-21: Validated the draft with `make check-fmt` + (`/tmp/check-fmt-wildside-backend-5-2-1-apalis-route-queue.out`). +- [x] 2026-05-21: Validated the draft with `make lint` + (`/tmp/lint-wildside-backend-5-2-1-apalis-route-queue.out`). +- [x] 2026-05-21: Validated the draft with `make test` + (`/tmp/test-wildside-backend-5-2-1-apalis-route-queue.out`; 1220 tests + passed, 4 skipped). +- [x] 2026-05-21: Attempted `coderabbit review --agent` twice for the plan + milestone; both attempts were blocked by a recoverable service rate limit. +- [x] 2026-05-26: Received explicit approval to implement this ExecPlan. +- [x] 2026-05-26: Confirmed branch + `backend-5-2-1-apalis-route-queue`, clean worktree, and existing Leta + workspace. +- [x] 2026-05-26: Completed baseline audit. `ApalisRouteQueue` and + `GenericApalisRouteQueue` are present under `backend/src/outbound/queue`, + current documentation describes the Apalis/PostgreSQL boundary, and + `docs/backend-roadmap.md` still lists 5.2.1 as open. +- [x] 2026-05-26: Ran targeted queue unit tests with + `cargo test -p backend outbound::queue --lib` + (`/tmp/backend-5-2-1-queue-unit.out`); 5 passed. +- [x] 2026-05-26: Ran targeted Apalis queue BDD tests with + `cargo test -p backend --test route_queue_apalis_bdd` + (`/tmp/backend-5-2-1-queue-bdd.out`); 9 passed. +- [x] 2026-05-26: Ran applicable pre-review gates: + `make check-fmt` (`/tmp/backend-5-2-1-pre-coderabbit-check-fmt.out`) and + `make markdownlint` + (`/tmp/backend-5-2-1-pre-coderabbit-markdownlint.out`); both passed. +- [x] 2026-05-26: Ran `coderabbit review --agent` after targeted adapter + verification; review completed with 0 findings. +- [x] 2026-05-26: Reconciled `docs/developers-guide.md` with the current + queue adapter API by documenting that `GenericApalisRouteQueue` is + re-exported for the BDD harness seam. +- [x] 2026-05-26: Ran documentation checks after documentation + reconciliation: `make markdownlint` (`/tmp/backend-5-2-1-markdownlint.out`) + and `make nixie` (`/tmp/backend-5-2-1-nixie.out`); both passed. +- [x] 2026-05-26: Confirmed `docs/users-guide.md` is absent and that 5.2.1 + does not change user-facing server behaviour, so no user-guide update is + applicable. +- [x] 2026-05-26: Ran `coderabbit review --agent` after documentation + reconciliation; review completed with 0 findings. +- [x] 2026-05-26: Ran full gates: `make check-fmt` + (`/tmp/backend-5-2-1-check-fmt.out`), `make lint` + (`/tmp/backend-5-2-1-lint.out`), and `make test` + (`/tmp/backend-5-2-1-test.out`); all passed. The Rust nextest portion ran + 1220 tests with 1220 passed and 4 skipped, and the frontend/workspace tests + also passed. +- [x] 2026-05-26: Ran `coderabbit review --agent` after full gates; review + completed with 0 findings. +- [x] 2026-05-26: Marked only `docs/backend-roadmap.md` item 5.2.1 done. +- [x] 2026-05-26: Ran final closure gates after the roadmap update: + `make check-fmt` (`/tmp/backend-5-2-1-final-check-fmt.out`), `make lint` + (`/tmp/backend-5-2-1-final-lint.out`), and `make test` + (`/tmp/backend-5-2-1-final-test.out`); all passed. The Rust nextest portion + ran 1220 tests with 1220 passed and 4 skipped, and the frontend/workspace + tests also passed. +- [x] 2026-05-26: Ran `coderabbit review --agent` after the final closure + commit; review completed with 0 findings. +- [x] 2026-05-26: Completed outcomes and retrospective. + +## Surprises & discoveries + +- 2026-05-21: The current base already contains + `backend/src/outbound/queue/apalis_route_queue.rs`, + `backend/tests/route_queue_apalis_bdd.rs`, and Apalis queue documentation. + The branch started with no diff from `origin/main`, while the roadmap item + still remained unchecked. This plan therefore focuses on approval-gated + validation, repair, and closure rather than assuming the adapter must be + written from scratch. + +- 2026-05-21: `docs/users-guide.md` is absent from this worktree. User-visible + behaviour does not appear to change in 5.2.1 because request-path dispatch + and workers remain future work; internal docs are the likely documentation + surface. + +- 2026-05-21: CodeRabbit review could not complete for the plan-drafting + milestone because the service returned a recoverable rate-limit error on two + attempts. No CodeRabbit concerns were produced. Approved implementation must + retry CodeRabbit before moving beyond the first implementation milestone. + +- 2026-05-26: The implementation baseline still matches the planning audit. + The adapter exists in the outbound queue module, the domain-facing + `RouteQueue` port remains the published contract, and request-path dispatch + remains out of scope. + +- 2026-05-26: `docs/developers-guide.md` incorrectly described + `GenericApalisRouteQueue` as not re-exported. The code re-exports it + from `backend/src/outbound/queue/mod.rs`, and the Apalis BDD test harness + imports that re-export directly. + +- 2026-05-26: `make nixie` runs `bun install`, which rewrote `bun.lock` to + bump the `ip-address` override from `10.1.1` to `10.2.0`. That lockfile + change was unrelated to 5.2.1 and was removed from this milestone. + +## Decision Log + +- Decision: Treat this plan as an approval-gated validation and closure plan + for 5.2.1, not as permission to continue the prior in-progress execution. + Rationale: the user explicitly stated that the plan must be approved before + implementation, and the branch currently has no diff from `origin/main`. + Date/Author: 2026-05-21 / planning agent. + +- Decision: Keep route-submission dispatch and worker consumption out of + scope. + Rationale: roadmap items 5.2.2 through 5.3.1 cover job structs, retries, + trace propagation, and worker deployment. The current route-submission TODOs + show dispatch remains deliberately deferred. + Date/Author: 2026-05-21 / planning agent. + +- Decision: Use `apalis-postgres` with PostgreSQL storage for the queue + adapter. + Rationale: `docs/backend-roadmap.md` explicitly requires Apalis with a + PostgreSQL backend. Firecrawl research confirmed Apalis PostgreSQL + documentation describes `PostgresStorage`, storage setup, standard polling, + `NOTIFY`-based storage, heartbeat support, and orphaned job re-enqueueing. + Date/Author: 2026-05-21 / planning agent. + +- Decision: Do not upgrade Apalis dependencies as part of the plan draft. + Rationale: external documentation currently shows a newer release candidate + than the repository pins, but dependency upgrades are implementation work and + need evidence from gates before they are justified. + Date/Author: 2026-05-21 / planning agent. + +- Decision: Start approved execution as a validation-and-closure activity. + Rationale: the user's 2026-05-26 instruction explicitly approves proceeding + with the planned functionality, and the baseline audit confirms that the + adapter already exists and the remaining work is to validate, repair any + defects found, reconcile documentation, and close the roadmap item. + Date/Author: 2026-05-26 / implementation agent. + +- Decision: Keep the current `GenericApalisRouteQueue` re-export and + document it as a BDD harness seam. + Rationale: changing visibility would be an API adjustment rather than a + required 5.2.1 behaviour fix. The re-export is already used by the + PostgreSQL-backed BDD test, while production code still has the clearer + `ApalisRouteQueue

` alias. + Date/Author: 2026-05-26 / implementation agent. + +## Outcomes & Retrospective + +5.2.1 is complete as a validation-and-closure milestone. The current backend +already contained the Apalis/PostgreSQL `RouteQueue` adapter, so the approved +implementation work verified that adapter against focused `rstest` unit tests, +PostgreSQL-backed `rstest-bdd` scenarios, and full repository gates rather +than rewriting working code. + +The only documentation defect found was in `docs/developers-guide.md`, which +described `GenericApalisRouteQueue` as internal even though the backend +module re-exports it for the BDD harness seam. That guide now matches the +actual public adapter surface and still directs production code to prefer the +`ApalisRouteQueue

` alias. + +No user-facing server behaviour changed, and this worktree does not contain +`docs/users-guide.md`, so no user-guide update was applicable. Route-submission +dispatch, worker consumption, retry policy, trace propagation, and route-engine +invocation remain explicitly deferred to later roadmap items. + +CodeRabbit returned 0 findings after targeted adapter verification, +documentation reconciliation, full quality gates, and final roadmap closure. +`docs/backend-roadmap.md` now marks only item 5.2.1 as done.