Skip to content

Evict stale and excess lock snapshots [minor] - #100

Merged
matt-edmondson merged 1 commit into
mainfrom
claude/issue-50-evict-lock-snapshots
Oct 8, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
claude/issue-50-evict-lock-snapshots

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #50

Summary

LockSnapshotStore kept one snapshot per (upstream, repository, ref) for as long as the process ran. The ref comes from the client's ?refspec=, so every branch anyone had listed kept a full lock list in memory, and a client could grow the store without limit.

The store now evicts on every Publish. Publishing is the only thing that grows it, and it follows a full upstream walk, so the O(n) scan costs little by comparison:

  1. It drops snapshots older than Locks:ListTtl. These would be refetched before being served anyway.
  2. It then drops the oldest remaining snapshots, by TakenAt, until at most Locks:MaxSnapshots are left. This is a new option, default 1000, and the validator rejects values of zero or less.
  3. It never evicts the snapshot it has just published, even when that snapshot is already old because the walk took a long time.

Removal checks the value as well as the key, so a snapshot republished under the same key during the scan is not lost. Only eviction is serialised. Read and Invalidate still take no lock, and their behaviour is unchanged. In particular, LockFanOut can still resolve lock ids from a stale snapshot until the next publish, so a large batch unlock does not start doing more upstream walks. Worst-case memory is now MaxSnapshots × MaxSnapshotLocks.

TimeProvider is now passed into the store, which DI already registers. The README settings table documents Locks:MaxSnapshots.

Out of scope: normalising the refspec or repository path in the key. Whether a path is case-sensitive depends on the forge, so this is not a small change. It is also tied to the refspec-keyed design that #46 and #47 cover. The cap bounds memory either way.

How it was tested

New tests in GitLfsCache.Tests/Locks/LockSnapshotStoreTests.cs use FakeTimeProvider:

  • Publish_AfterManyRefsHaveOutlivedTheListTtl_DropsEveryStaleSnapshot: publishes 200 distinct refspecs, advances time by ListTtl, then publishes once more. Afterwards the store holds only that last snapshot.
  • Publish_BeyondMaxSnapshots_EvictsTheOldestFirst: with a cap of 3, publishes 5 snapshots. The 2 oldest are evicted.
  • Publish_ASnapshotThatIsAlreadyOld_KeepsIt, Publish_RepublishingOneKey_HoldsOneSnapshot and Invalidate_DropsEveryRefOfTheRepositoryOnly check the guard and the existing behaviour.
  • A validator test for MaxSnapshots <= 0.

Revert proof: with eviction short-circuited in LockSnapshotStore, the two eviction tests fail (352/354 pass). With the fix restored, all 354 pass.

Full suite: dotnet build finished with 0 warnings and 0 errors, and dotnet test passed 354 of 354.

🤖 Generated with Claude Code

https://claude.ai/code/session_01K6bDUGMFsAQews3TnA2jXr


Generated by Claude Code

LockSnapshotStore held one snapshot per (upstream, repository, ref) for
the life of the process, and the ref comes from the client's ?refspec=,
so every branch ever listed kept a full lock list in memory and a client
could grow the store without limit.

Each publish now drops snapshots older than Locks:ListTtl, which would
be refetched before being served anyway, then the oldest survivors until
at most the new Locks:MaxSnapshots (default 1000) remain. The snapshot
just published is never evicted. Read and Invalidate are unchanged.

Fixes #50

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01K6bDUGMFsAQews3TnA2jXr
@sonarqubecloud

sonarqubecloud Bot commented Oct 8, 2026

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit 750d714 into main Oct 8, 2026
14 checks passed
@matt-edmondson
matt-edmondson deleted the claude/issue-50-evict-lock-snapshots branch October 8, 2026 04:52
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.

Lock-list snapshots are never evicted: every distinct (repository, refspec) keeps a full lock list in memory for the life of the process

2 participants