Skip to content

Rebased source receipt remains oldest in LRU order and is evicted by the next receipt #557

Description

@morluto

Summary

Storage::persist_rebased_receipt_at says it touches the source receipt so that the source is not prematurely evicted by LRU, but the update refreshes only last_access_unix_millis and expires_unix_millis. It does not assign a new access_sequence.

Eviction is ordered exclusively by (access_sequence, id). At the 128-receipt quota, a successful rebase can therefore leave the refreshed source as the oldest LRU entry, and the very next receipt creation deterministically deletes it even though its TTL was just extended.

Affected revision: 85dd1b1b918e863f5535333cf435a5ac6163c30e.

Evidence

src/receipt.rs defines:

pub(crate) const MAX_RECEIPTS: usize = 128;
pub(crate) const RECEIPT_TTL_MILLIS: i64 = 24 * 60 * 60 * 1_000;

New/touched ordinary receipts obtain a monotonically increasing sequence through next_receipt_access_sequence.

src/storage/receipts.rs::evict_oldest_receipt_except selects:

SELECT id
FROM retrieval_receipts
WHERE access_sequence > 0
ORDER BY access_sequence, id
LIMIT 1

During rebase, the source is protected from quota eviction inside the current transaction. The function then contains this comment and update:

// Touch the source receipt so it is not prematurely evicted by LRU.
tx.execute(
    "UPDATE retrieval_receipts
     SET last_access_unix_millis = ?1,
         expires_unix_millis = ?2
     WHERE id = ?3",
    params![now_unix_millis, source_expires_unix_millis, source_row_id],
)?;

Unlike the ordinary receipt touch path, this update does not modify access_sequence.

Formal counterexample

Let N = MAX_RECEIPTS = 128.

  1. Create source receipt S first. Its LRU sequence is 1.
  2. Create N - 1 additional receipts. The database is full and their sequences are 2..N.
  3. Rebase S.
    • The quota loop must make room for the rebased receipt.
    • It calls evict_oldest_receipt_except(..., Some(S)), so sequence 2 is deleted rather than S.
    • The source timestamps are refreshed, but S.access_sequence remains 1.
    • The new rebased receipt is inserted with sequence N + 1.
  4. Create one more ordinary receipt.
    • The database is again at N receipts.
    • Creation calls evict_oldest_receipt_except(..., None).
    • The minimum (access_sequence, id) is now S with sequence 1.
    • S is deleted.

Thus the source can be evicted on the first quota-producing operation after the code has explicitly “touched” it and extended its expiry by 24 hours.

This is deterministic; no race or clock anomaly is required.

Existing test gap

rebase_quota_eviction_never_selects_the_source_receipt verifies only that the source survives the rebase transaction itself. It does not create another receipt after the rebase. The sequence above therefore passes the current test while violating the stated LRU intent immediately afterward.

Impact

  • A source receipt exposed as source_receipt_id can become unreadable immediately after a successful rebase under normal quota pressure.
  • Follow-up audit, resource-read, comparison, or another rebase flow can receive UnknownReceipt despite the source having just been refreshed.
  • The persisted expiry timestamp claims the source is live for another TTL while the independent LRU ordering still treats it as the oldest entry.
  • The behavior becomes load-dependent: low-volume repositories preserve the source, while busy multi-agent repositories lose it at the first subsequent receipt allocation.

Suggested acceptance criteria

  • A successful rebase refreshes the source's LRU position consistently with its refreshed access/expiry timestamps.
  • At MAX_RECEIPTS, execute the exact sequence above and assert that:
    • the source survives the rebase;
    • one subsequent receipt allocation evicts the next genuinely least-recently-used receipt, not the source;
    • both the source and newly rebased receipt remain readable afterward.
  • Preserve monotonic, overflow-checked access-sequence accounting and the current transaction/quota bounds.
  • Add a clock-controlled regression test so the result does not depend on wall time.

Non-goals

  • Do not increase the receipt quota or TTL as a workaround.
  • Do not retain all historical receipts indefinitely.
  • Do not weaken quota enforcement or the guarantee that the rebase source cannot be evicted during the rebase transaction itself.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions