Skip to content

Gmail reconcile lease: timestamp-derived token can collide #168

Description

@zaridan

GmailWatchStateStore.claimReconcileLease returns the rendered claimed_until::text as its lease token. Two claims that land within one clock tick mint the same token, so a stale holder's token compares equal to the live holder's.

Evidence this is real, not theoretical

HT-101 (PR #165) hit exactly this. Its IMAP lease was written to mirror the Gmail one statement-for-statement, and a test forcing a same-tick reclaim produced two identical tokens. The IMAP side was fixed there by adding a per-claim lease_token uuid (gen_random_uuid()), keeping claimed_until for expiry and the token for ownership.

Notably, src/store/imap-watch-state.test.ts already carried a waitForClockTick() helper — the original author knew the collision was possible and worked around it in the tests rather than fixing the production code.

Why this is lower severity than the IMAP case was

Gmail's token guards only releaseReconcileLease. It never fences a cursor advance. Worst case is a stale holder clearing a live successor's lease, allowing a redundant concurrent reconcile — wasteful, and the ingest ledger still dedupes. The IMAP case was worse because the same token fenced the cursor write, so a collision could let a stale holder escape a UIDVALIDITY-reset pause and corrupt the cursor.

Acceptance

  • gmail_watch_state gains a per-claim lease_token uuid; claimReconcileLease returns it, releaseReconcileLease matches on it.
  • A test forces a same-tick reclaim and asserts the two tokens differ.
  • Any waitForClockTick-style workaround that existed only to dodge the collision is removed, not kept.

Unlike migration 027, gmail_watch_state is live in production, so this needs a real additive migration — not an in-place amendment.

Found while fixing the IMAP equivalent in PR #165, 2026-07-31.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions