Skip to content

feat(distributed): expose the lease token so a write can be conditional on it - #2

Closed
abnegate wants to merge 1 commit into
mainfrom
dat-2084-lease-token
Closed

feat(distributed): expose the lease token so a write can be conditional on it#2
abnegate wants to merge 1 commit into
mainfrom
dat-2084-lease-token

Conversation

@abnegate

@abnegate abnegate commented Aug 5, 2026

Copy link
Copy Markdown
Member

What

Distributed::token() returns the value this lock wrote to the key, or null when it holds no lease.

Why

refresh() proves ownership at the instant it returns. That is the wrong instant for a caller whose write commits later: between the successful refresh and the write, the lease can lapse and a successor can take the key, and nothing about the write itself says which lease it belongs to.

Handing back the token lets the caller close that gap. It stores the token alongside whatever it recorded while holding the lease, and refuses a later write whose stored token no longer matches — the write becomes conditional on the lease rather than on a check that merely preceded it.

The caller here is Appwrite Cloud's dedicated-databases worker (DAT-2084), where a lease that lapsed during a ~5 minute PostgreSQL cutover let the finalization writes land on top of a successor's outcome.

Because the token is the literal value on the key, it doubles as a diagnostic: an operator can GET the lock key and compare it against the record to see whether the recorded owner is still the live holder.

Tests

Three added to DistributedTest, all against a real Redis:

  • the token is the value on the key, and is cleared on release
  • each acquisition mints its own token, so work recorded under a lapsed lease cannot pass for work under its successor
  • a failed acquire issues no token, and the holder's token is what the key holds

Verified red against the plausible wrong implementation (returning a freshly generated token rather than the stored one): 2 of the 3 fail.

…al on it

refresh() proves ownership at the instant it returns, which is the wrong
instant for a caller whose write commits later. Handing back the value on the
key lets that caller store the token with whatever it recorded under the lease
and refuse a stale write whose token no longer matches, and lets an operator
compare a record against the live holder directly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Thanks for contributing! This repository is a read-only mirror; development for this library happens in packages/lock in the utopia-php monorepo. Please open this pull request there instead.

@github-actions github-actions Bot closed this Aug 5, 2026
@greptile-apps

greptile-apps Bot commented Aug 5, 2026

Copy link
Copy Markdown

Greptile Summary

The PR exposes each distributed-lock acquisition token so downstream writes can be fenced against the lease identity.

  • Adds Distributed::token() as a public accessor for the Redis lock value.
  • Adds Redis-backed tests for token identity, uniqueness across acquisitions, failed acquisition, and release cleanup.

Confidence Score: 4/5

The PR appears safe to merge, with a non-blocking API-documentation issue around stale tokens after lease loss.

The accessor correctly exposes the stored fencing value, but its nullability documentation promises current lease state even though Redis expiry and failed refresh leave that value cached.

Files Needing Attention: src/Distributed.php

Important Files Changed

Filename Overview
src/Distributed.php Adds the token accessor, but its documentation conflates cached acquisition identity with current lease ownership.
tests/DistributedTest.php Adds focused coverage for token identity, renewal across acquisitions, failed acquisition, and explicit release.

Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
### Issue 1
src/Distributed.php:143
**Clarify cached token semantics**

After the Redis TTL expires or `refresh()` fails, `token()` continues returning the cached acquisition value even though the instance no longer holds the lease. Describing `null` as meaning that the lock holds no lease conflates explicit release with current ownership and can lead callers to treat a stale token as proof of a live lease.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "feat(distributed): expose the lease toke..." | Re-trigger Greptile

Comment thread src/Distributed.php
}

/**
* The value this lock wrote to the key, or null when it holds no lease.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Clarify cached token semantics

After the Redis TTL expires or refresh() fails, token() continues returning the cached acquisition value even though the instance no longer holds the lease. Describing null as meaning that the lock holds no lease conflates explicit release with current ownership and can lead callers to treat a stale token as proof of a live lease.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/Distributed.php
Line: 143

Comment:
**Clarify cached token semantics**

After the Redis TTL expires or `refresh()` fails, `token()` continues returning the cached acquisition value even though the instance no longer holds the lease. Describing `null` as meaning that the lock holds no lease conflates explicit release with current ownership and can lead callers to treat a stale token as proof of a live lease.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex

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.

1 participant