Files
alkstore/tasks/pg-fix-scheduler-error-classification.md
T

132 lines
6.5 KiB
Markdown

---
id: pg-fix-scheduler-error-classification
name: Classify run_schedules' renew-failure arms on pg (the sqlite twin, review 004 item 9)
status: completed
depends_on: []
scope: narrow
risk: low
impact: component
level: implementation
tags: [review-004-fixes, postgres-engine, scheduler]
---
## Description
The pg twin of `sqlite-fix-scheduler-error-classification` (completed),
recorded in that task's Notes as an unresolved follow-up:
`alkstore-postgres/src/scheduler.rs:682` carries the identical
`.unwrap_or(false)` flattening the sqlite task fixed — a mid-run
`lease.renew(LEADER_TTL_S)` failure (including the closed-store
`Database` error) is mapped to `false` and misreported as
`Error::LeadershipLost`, misdirecting the documented respawn recipe at
a dead store.
The classification decision of record is already landed and
engine-neutral — the sqlite task's dated (2026-10-11) ADR-009 §1 /
core-contract.md §Errors annotation pins the arm set for the family —
so this task implements pg to match it, not re-decides:
- `renew` → `Ok(false)` — the genuine loss arm (expired and
re-acquired elsewhere; exclusion lapses silently at TTL expiry,
ADR-008 §7) — returns `Err(LeadershipLost)`, unchanged,
return-before-tick. The respawn recipe matches on this arm only.
- `renew` → `Err(e)` — the machinery never reached the lock
semantics; not a loss, never `LeadershipLost`. The `Err` propagates
unchanged: the closed-store arm arrives pre-classified (pg's
post-close `renew` fails closed with `Database`, pinned by pg
lock_tests), and any other storage failure carries the driver's
source chain.
No new variant minted — ADR-008 §5's act-differently rule holds
(`Closed` stays the wake-receiver variant; closed store is `Database`
engine-wide, the pg posture already).
**Tests need the harness server** (the reason the sqlite task recorded
rather than landed this): model the two pins on the sqlite task's —
a mid-run close exits with the closed `Database` classification, never
`LeadershipLost`; a mid-run lease loss still returns `LeadershipLost`
(the respawn arm unchanged).
## Acceptance Criteria
- [ ] The pg renew arm implements the landed arm set: `Ok(false)` →
`LeadershipLost` unchanged; `Err(e)` propagates unchanged (no
flattening) — no re-deciding; any discrepancy with the sqlite
task's decisions of record recorded, not deviated around
- [ ] Both arms pinned by tests against the harness server (the
closed-store arm and the genuine-loss arm, modeled on the
sqlite task's two pins)
- [ ] The ADR-009 §1 / core-contract annotations verified to read
engine-neutral (they were landed as family-wide; cite them —
only pg-specific doc text needs touching, e.g.
engine-postgres.md's scheduler row if it describes the arm)
- [ ] `cargo test -p alkstore-postgres` (harness server), clippy
`-D warnings`, fmt clean; workspace gates green server-less
## References
- tasks/sqlite-fix-scheduler-error-classification.md (the twin — the
arm set's decisions of record, the two pin shapes, the recorded
follow-up this task discharges)
- docs/reviews/004-pre-release-general-review.md §"Findings recorded,
not fixed" item 9 (the original finding — recorded the sqlite site
only; the pg twin surfaced at implementation)
- docs/architecture/decisions/009-*.md §1 (the dated classification
annotation of record), core-contract.md §Errors
- alkstore-postgres/src/scheduler.rs:675-690 (the flattening site)
## Notes
- **No discrepancy with the decisions of record**: the sqlite task's
arm set implemented verbatim — `Ok(false)` keeps
`Err(LeadershipLost)` return-before-tick (the respawn arm),
`Err(e)` propagates unchanged (no new variant, no re-deciding; the
`Closed`-stays-wake-receiver posture untouched).
- **Harness server (was the recorded blocker)**: the `pglo-poc`
container was up on :15432; the standard env
(`ALKSTORE_PG_HOST/PORT/USER/PASSWORD/DB=…/15432/postgres/poc/blobs`)
ran the whole pg suite live — nothing was held back to the
server-less skip posture.
- **Engine note (how the closed arm arrives)**: pg's closed-store
classification is *proactive* — `PgLockHandle::renew` checks the
store's shared `closed` flag before any SQL (locked.rs closed-sign
first, the sqlite twin's shape; pinned by pg lock_tests) — so the
test drives it through the fixture store's own `EngineCtx`
(`run_schedules` runs on `fx.store.engine_ctx()`, sharing the
closed flag and pool; `close()` then flips the runner's flag), not
through a second store instance the way the sqlite pin's
same-file DB works. The genuine-loss arm drives through the
runner-store pattern (second `open_store` on the same dsn/schema)
with an admin-client `DELETE` of the lock row — the twin's rogue
deletion shape.
- **Doc alignment**: the pg scheduler's module doc got the arm-set
sentence (the pg-specific text this criterion calls out) — the
SQLite twin's wording (`Ok(false)` = the loss / `Err(e)` =
opaque-propagate, review 004 item 9). ADR-009 §1 and
core-contract.md §Errors verified engine-neutral as landed
(2026-10-11, dated annotation; no engine names) — cited, not
touched; engine-postgres.md's scheduler row doesn't describe the
renew arm, so it needs nothing.
## Summary
The pg leader loop's top-of-iteration renew no longer flattens a
failed renewal into `LeadershipLost` (`scheduler.rs` — the
`unwrap_or(false)` the sqlite task recorded): `Ok(false)` keeps the
`Err(LeadershipLost)` respawn arm unchanged, and a failed renewal
propagates its opaque `Database` classification unchanged (the
closed-store arm arrives pre-classified — the engine-wide "the store
is closed" source, the proactive closed-sign posture) — respawning a
leader against a dead store is no longer the pg outcome either; the
sqlite/pg twins now classify identically. Two acceptance tests pin
the arms against the harness server, modeled on the sqlite pair:
`closed_store_run_reports_the_closed_classification_not_leadership_
lost` (the runner runs on the fixture's own EngineCtx and the store
closes mid-run — the exit is the closed `Database` classification,
never `LeadershipLost`) and `mid_run_lease_loss_still_returns_
leadership_lost` (a deleted lease row mid-run still returns the
loss, the respawn arm unchanged). Verified: full
`cargo test -p alkstore-postgres` green on the live harness server
(133 lib + 25 contract suite + 9 schema tests), workspace gates green
server-less, clippy `-D warnings` clean, `cargo fmt --check` clean,
doc gate clean, fuzz corpus replay (6/6) green.