Skip to content

Implement Valkey target-lease backend with atomic fencing scripts - #95

Merged
lost-rob0t merged 14 commits into
masterfrom
agent/issue-31-valkey-lease-backend
Aug 16, 2026
Merged

Implement Valkey target-lease backend with atomic fencing scripts#95
lost-rob0t merged 14 commits into
masterfrom
agent/issue-31-valkey-lease-backend

Conversation

@lost-rob0t

@lost-rob0t lost-rob0t commented Aug 7, 2026

Copy link
Copy Markdown
Owner

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-outcome wraps deserialization in a handler-case so corrupt records return :backend-unavailable instead of signaling. list-leases uses 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). No GET-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-bounded normalize-identity-component.
  • source/leases/memory-store.lisp — Uses shared validators and centralized retryability.
  • source/leases/package.lisp — Export protocol symbols.
  • source/starintel-gserver.asd — Register valkey-store / valkey-scripts modules.
  • 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

SUITE LEASE-STORE-CONTRACT-TESTS  discovered=8  executed=8  passed=8  failed=0 skipped=0
SUITE VALKEY-LEASE-INTEGRATION-TESTS discovered=22 executed=22 passed=22 failed=0 skipped=0
SUITE COUCHDB-VIEW-INTEGRATION-TESTS discovered=7  executed=7  passed=7  failed=0 skipped=0
SUITE HTTP-API-TESTS                  discovered=28 executed=28 passed=28 failed=0 skipped=0

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.

- 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 lost-rob0t left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread docker/valkey-entrypoint.sh Outdated
Comment thread source/leases/valkey-store.lisp Outdated
Comment thread source/leases/valkey-scripts.lisp
Comment thread source/leases/valkey-store.lisp Outdated
Comment thread t/valkey-lease-integration-test.lisp Outdated
@lost-rob0t
lost-rob0t marked this pull request as ready for review August 8, 2026 00:13
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 lost-rob0t left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread source/leases/valkey-scripts.lisp
Comment thread source/leases/valkey-scripts.lisp
Comment thread docker/valkey-entrypoint.sh
… 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 lost-rob0t left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread source/leases/valkey-store.lisp Outdated
Comment thread source/leases/valkey-store.lisp Outdated
Comment thread source/leases/protocol.lisp
…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 lost-rob0t left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread source/leases/valkey-store.lisp Outdated
Comment thread source/leases/valkey-store.lisp
…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 lost-rob0t left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread source/leases/valkey-store.lisp
Comment thread t/valkey-lease-integration-test.lisp
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.

Copy link
Copy Markdown
Owner Author

Final acceptance re-audit at head 652b148c233646af4f2e4b46dd6289ae73ef3c26 against canonical master base badc960ac1f1222b3acbc5bc30c90c012d0f594c.

Current-head required GitHub Actions are green:

  • Smoke Tests run 31952574794: unit-tests success and integration-tests success.
  • Container Stack run 31952574829: stack job success.
  • Operational salvage run 31952574756: success.
  • Canonical schema lock run 31952574782: success.

Exact workflow commands exercised at this head:

  • nix flake check --show-trace
  • nix build .#default --no-link --print-build-logs
  • nix run .#star-unit-tests
  • nix run .#star-integration-tests
  • ./scripts/stack-test.sh
  • python -m unittest discover -s tests -p 'test_*.py' -v
  • git diff --check

nix run .#star-integration-tests boots real ephemeral plain and TLS Valkey services with authentication/ACLs, real CouchDB and RabbitMQ are provided by the integration job, and the required-suite runner fails on zero discovered tests, zero executed tests, partial execution, any failure, or any skip.

Exact recorded suite counts for this PR head:

  • LEASE-STORE-CONTRACT-TESTS: discovered 8, executed 8, passed 8, failed 0, skipped 0.
  • VALKEY-LEASE-INTEGRATION-TESTS: discovered 22, executed 22, passed 22, failed 0, skipped 0.
  • COUCHDB-VIEW-INTEGRATION-TESTS: discovered 7, executed 7, passed 7, failed 0, skipped 0.
  • HTTP-API-TESTS: discovered 28, executed 28, passed 28, failed 0, skipped 0.
  • Operational salvage: 12/12.
  • Container stack: passed.
  • Schema lock: verified.

The final two stale review threads were re-audited against the actual current head and resolved only after verifying their implementations and regression coverage:

  • per-key list authority validation is atomic via a one-key KEYS[1] Lua script; release/reacquire and missing-key PTTL=-2 regressions are in t/valkey-lease-review-regression-test.lisp;
  • unsupported record_version now exercises the Lisp deserialization guard and returns typed :backend-unavailable.

No runtime work from PR #79 is mixed into this PR. dev is historical only and is 0 commits ahead / 29 behind current master.

@lost-rob0t
lost-rob0t merged commit 7834d66 into master Aug 16, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P0-23] Implement the initial Valkey target-lease backend with atomic fencing scripts

1 participant