Implement Valkey target-lease backend with atomic fencing scripts - #95
Conversation
- source/leases/valkey-store.lisp: Valkey-backed lease store with ACL/password handling, TLS/plain support, interrupted-acquire idempotency - source/leases/valkey-scripts.lisp: server-side Lua scripts for atomic lease ops - source/leases/package.lisp: export valkey-store symbols - source/starintel-gserver.asd: register valkey-store/valkey-scripts modules - t/valkey-lease-integration-test.lisp: 12 Valkey lease integration tests (TLS + plain) - starintel-gserver-integration-tests.asd, t/run-integration-tests.lisp: wire valkey suite - docker/valkey-entrypoint.sh, docker-compose.yml: Valkey service with ACL/password/TLS setup - flake.nix, nix/images.nix: Valkey image + devshell - qlfile, qlfile.lock: valkey client dependency - scripts/stack-test.sh: Valkey readiness + cleanup - DOCKER.md, docs/configuration.org, docs/lease-store-usage.org, README.org: docs - tests/test_operational_salvage_contract.py: salvage contract coverage - .gitignore: generated test artifacts Baseline: VALKEY-LEASE 12/12, COUCHDB-VIEW 7/7, HTTP-API 28/28.
lost-rob0t
left a comment
There was a problem hiding this comment.
Blocking review findings before this draft is marked ready. CI is green, but the production adapter still diverges from the normative lease/security contract in several places: the Valkey ACL grants unrestricted command/key access; acquisition conflicts lose the protocol's retryable semantics; an existing lease key with no TTL can be deleted and replaced instead of failing closed; operation identifiers are only checked for non-emptiness rather than bounded size; and the integration test labeled as the backend-neutral contract is a separate happy-path subset instead of executing the same contract assertions used by the memory backend. Please fix the inline findings, add regression coverage for each, and rerun unit, integration, and stack CI.
Restrict the Valkey service credential to a least-privilege ACL scoped to the StarIntel lease namespace with only the adapter and Lua-script commands granted; deny unrelated keys and administrative commands. Add an ACL regression test proving lease ops still work, unrelated key access is rejected, and an out-of-surface command is rejected. Centralize lease-outcome retryability in the protocol layer (retryable-lease-outcome-code-p) and have both the memory and Valkey adapters derive the retryable flag from it, so acquisition contention is retryable in both backends. The shared contract asserts equivalent conflict outcomes carry the same retryable flag. Fail closed when an active Valkey lease key has no TTL: the acquire script now distinguishes missing, expiring, expired, and no-TTL states, returns the stable backend-unavailable result for a no-TTL active key, and never deletes, replaces, or allocates a new fencing token over the corrupt record. Add a real-Valkey regression test covering the full fail-closed sequence. Bound externally supplied identifiers (request, owner principal/client/ credential/service, execution, job, trace, lease, and revoke reason) at the protocol layer using UTF-8 byte limits, and validate metadata as a bounded JSON object. Both adapters consult the shared validators before any backend work. Add boundary tests for max accepted, over-max rejected, multibyte UTF-8, malformed metadata shape, and oversized metadata. Run the identical backend-neutral lease contract against the memory and Valkey stores via a shared fixture/assertion suite covering acquisition, contention, retryability, request-id idempotency, changed-input conflict, renewal, wrong owner, stale token, release, stale release after successor, inspect/get, list/filter, revoke, health, deadline, close, and record serialization. Keep Valkey-only coverage for real concurrency, TLS, ACL, pool exhaustion, reconnect/backoff, response interruption, authoritative TTL, and cluster-safe key layout. Validation: star-unit-tests (13 suites, all green), star-integration-tests (VALKEY-LEASE-INTEGRATION-TESTS 14/14), container stack (scripts/stack-test.sh passed), operational salvage (12/12), and the canonical schema lock.
lost-rob0t
left a comment
There was a problem hiding this comment.
Second-pass review after commit 76581d2. The first round materially improved the adapter and all current CI workflows are green, but the no-TTL/corrupt-state handling is still incomplete and the identifier-bound fix does not cover list filters. Do not merge yet. In particular: (1) the acquire idempotency fast path can still return an active :acquired result after the live lease key has lost its TTL; (2) renew does not verify that the current lease is still unexpired before extending it, and the get/fenced-commit paths likewise do not treat a no-TTL active key as corrupt; (3) list-leases still accepts unbounded owner filter input (and target/program filtering is not using the shared UTF-8 byte validator); (4) the PR body is stale and still reports 12 Valkey integration tests even though the fix commit reports 14. Fix the authority-bearing corrupt-state paths, add regressions, complete shared input bounds, and update the PR test summary.
… scripts Fix idempotent acquire replay over corrupt/no-TTL state: the idempotency fast path now validates the current authority-bearing live key (TTL and logical expiry via server TIME) before returning an active :acquired lease. A no-TTL active key fails closed with :backend-unavailable; a logically expired key returns the historical record with state=expired; a successor key returns expired. No fencing token is allocated during replay. Fix renewal to never revive an expired/corrupt lease: after ownership matches, the script checks PTTL==-1 (fail closed, no TTL reattachment) and now >= expires_at (expired, delete surviving key, do not revive) before computing the new expiry. Maximum lifetime is not a substitute for current expiry validation. Apply corrupt-state rules to get-lease and fenced commits: get-lease returns :backend-unavailable for a no-TTL key and :expired for a logically expired active-shaped record, never :found. fenced-set checks PTTL==-1 and returns :backend-unavailable so a corrupt lease cannot authorize a commit. Release and revoke are terminal invalidation operations and intentionally proceed on a no-TTL key to clean up corrupt state; this is documented and tested as cleanup, not authority continuation. Revisit acquire expiry determination: replace the PTTL>0 / PTTL==0 branch with authoritative server TIME and the record's expires_at. A surviving key with PTTL==0 but now<expires_at is still active (conflict); a key with PTTL>0 but now>=expires_at is expired (reclaim). PTTL==-1 always fails closed. No GET-then-SET race introduced; all logic stays in the atomic Lua script. Bound list-leases filters: add valid-lease-filter-p and valid-lease-component-filter-p to the protocol layer. Both memory and Valkey backends validate owner-principal-id, target-id, and program-id before any backend work (before SCAN/memory scan). Oversized or malformed filters return :invalid-request, not :backend-unavailable or an unhandled error. nil remains valid for an omitted filter. Resolve bundled ACL vs custom key-prefix (Option B): the bundled Valkey image is default-prefix-only (~starintel:target-lease:v1:*). Custom :key-prefix requires a separately configured Valkey ACL. Updated docs/lease-store-usage.org with a new section documenting this constraint and changed the scoped example to use the default prefix. Validation: star-unit-tests (LEASE-STORE-CONTRACT-TESTS 8/8), star-integration-tests (VALKEY-LEASE-INTEGRATION-TESTS 20/20), container stack (passed), operational salvage (12/12), schema lock (passed).
lost-rob0t
left a comment
There was a problem hiding this comment.
Second review of a4fd56d. The previous corrupt-state findings are substantially addressed and CI is green, but three correctness/contract gaps remain before merge: list-leases still bypasses active/expiry/TTL validation; corrupt backend records can escape the typed adapter boundary as raw Lisp errors during result deserialization; and canonical component filters are still character-bounded rather than UTF-8-byte-bounded. See inline comments. These need regression coverage against real Valkey where applicable.
…n, and UTF-8 byte bound for identity components list-leases now applies the same corrupt-state rules as get-lease: a new +valkey-list-active-script+ Lua script does SCAN + GET + PTTL + TIME in one round trip per page, skipping no-TTL keys (corrupt) and logically expired keys (now >= expires_at). Corrupt JSON is caught with pcall. Filtering (owner/target/program) moves into the script using pre-normalized values. No-TTL and expired keys can no longer appear in the active lease list. valkey-script-outcome now wraps deserialize-lease-record in a handler-case. A malformed or contract-invalid backend record (bad version, canonical identity mismatch, unknown state) returns the typed :backend-unavailable result instead of signaling a raw Lisp error through the adapter boundary. normalize-identity-component now enforces UTF-8 byte length via utf-8-byte-length and +lease-identifier-max-bytes+ instead of character count. A 256-character multibyte value can no longer bypass the 256-byte protocol bound. Tests now cover the multibyte case for target-id and program-id filters, not just owner-principal-id. Validation: LEASE-STORE-CONTRACT-TESTS 8/8, VALKEY-LEASE-INTEGRATION-TESTS 21/21, container stack passed, operational salvage 12/12, schema lock passed.
lost-rob0t
left a comment
There was a problem hiding this comment.
Second-pass follow-up on 9fe1b69: the three reported fixes are present, but the new list implementation introduces a cluster-incompatible EVAL pattern, and the corrupt-record adapter guard still lacks the requested real-Valkey regression. Do not merge until the cluster/list issue is corrected and CI is fully green.
…on test
list-leases no longer uses a multi-key Lua script (EVAL ... 0 with
dynamically discovered keys). In Valkey cluster mode, scripts that access
keys not declared as input keys and spanning multiple hash slots are
forbidden. Different leases deliberately use different {digest} hash tags,
so the list script could span slots and fail in cluster deployments.
Revert to cluster-safe Lisp-side SCAN + GET + PTTL + TIME, with each
command targeting one key. The corrupt-state guards (PTTL == -1 excluded,
now >= expires_at excluded, corrupt JSON skipped via handler-case) are
preserved in the Lisp path. The +valkey-list-active-script+ Lua script is
removed.
Add real-Valkey regression test for corrupt-record handling: injects a
syntactically valid but contract-invalid record (tampered lock_key) and
verifies get-lease returns :backend-unavailable (not a raw Lisp error) and
list-leases skips the corrupt record without signaling.
Validation: LEASE-STORE-CONTRACT-TESTS 8/8,
VALKEY-LEASE-INTEGRATION-TESTS 22/22, container stack passed,
operational salvage 12/12, schema lock passed.
lost-rob0t
left a comment
There was a problem hiding this comment.
Two issues remain on the latest head. The invalid EVAL ... 0 list script is gone, but the replacement performs authority validation across separate GET/PTTL/TIME calls, so it can return a lease that disappeared or was replaced between calls; it also treats PTTL=-2 (missing key) as acceptable. The corrupt-record regression also does not actually exercise valkey-script-outcome's deserialization guard because tampering lock_key is rejected inside Lua before the encoded record reaches the Lisp deserializer. Please fix the inline findings and rerun the real-Valkey suite.
Preserve the existing #95 lease implementation while incorporating the reconciled master history, including persistent human users and current auth/runtime behavior. Resolve README, Compose, stack, and ASDF overlap semantically; keep Valkey behind the generic lease-store protocol.
Bring #95 fully current with master after the branch-policy merge without altering the existing Valkey implementation history.
|
Final acceptance re-audit at head Current-head required GitHub Actions are green:
Exact workflow commands exercised at this head:
Exact recorded suite counts for this PR head:
The final two stale review threads were re-audited against the actual current head and resolved only after verifying their implementations and regression coverage:
No runtime work from PR #79 is mixed into this PR. |
Summary
Implements the first deployable KV lease backend (Valkey) behind the backend-neutral lease-store protocol landed in #94 / #93. Closes #31.
What changed
source/leases/valkey-store.lisp— Valkey-backed lease store: atomic acquire/renew/release via Lua scripts, monotonic fencing counter, TTL stored server-side, request-id idempotency cache, connection pooling with deadlines and bounded reconnect/backoff, ACL/password auth, TLS support, cluster-safe namespaced keys, health/cleanup closures, redacted observability hooks.valkey-script-outcomewraps deserialization in a handler-case so corrupt records return:backend-unavailableinstead of signaling.list-leasesuses cluster-safe Lisp-side SCAN+GET+PTTL+TIME (no multi-key Lua script).source/leases/valkey-scripts.lisp— Server-side Lua scripts for atomic lease operations (acquire, renew, release, get, fenced-set) with compare-and-swap semantics, authoritative server-time expiry, and fail-closed corrupt-state handling (no-TTL active keys, logically expired surviving keys, idempotent replay validation). NoGET-then-SET races. No multi-key list script.source/leases/protocol.lisp— Centralized retryability mapping, bounded identifier/metadata validators (UTF-8 byte limits), filter validators, and byte-boundednormalize-identity-component.source/leases/memory-store.lisp— Uses shared validators and centralized retryability.source/leases/package.lisp— Export protocol symbols.source/starintel-gserver.asd— Registervalkey-store/valkey-scriptsmodules.t/lease-store-contract-test.lisp— Shared backend-neutral contract suite (runs against memory and Valkey) + identifier/metadata/filter boundary tests (including multibyte UTF-8 for all filter types).t/valkey-lease-integration-test.lisp— 22 integration tests against a real Valkey service (plain + TLS) including corrupt-state regressions for acquire/renew/get/commit/list and a corrupt-record regression.starintel-gserver-integration-tests.asd,t/run-integration-tests.lisp— Wire the valkey suite into the integration runner.docker/valkey-entrypoint.sh,docker-compose.yml— Valkey service with least-privilege ACL/password/TLS setup via Docker secrets.flake.nix,nix/images.nix— Valkey package/image + devshell; integration runner boots ephemeral plain+TLS Valkey instances with per-run certs/ACLs.qlfile,qlfile.lock— Valkey client dependency.scripts/stack-test.sh— Valkey readiness check and cleanup.tests/test_operational_salvage_contract.py— Salvage contract coverage.DOCKER.md,docs/configuration.org,docs/lease-store-usage.org,README.org,docs/index.org— Documentation.Test results
Container stack: passed. Operational salvage: 12/12. Schema lock: verified.
All generated certs, ACL files, and passwords are ephemeral (mktemp dirs, cleaned on exit). No secrets committed.