Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions GitLfsCache.Tests/Integration/LockListCachingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,43 @@
Assert.HasCount(2, (await ListLocksAsync(fixture))["locks"]!.AsArray());
}

[TestMethod]
[DataRow("/locks", "{\"path\":\"b\",\"ref\":{\"name\":\"refs/heads/main\"}}", 2, DisplayName = "Create")]
[DataRow("/locks/1/unlock", "{\"ref\":{\"name\":\"refs/heads/main\"}}", 0, DisplayName = "Unlock")]
public async Task ChangingALock_InvalidatesASnapshotListedUnderARefspec(string change, string body, int expected)
{
// git-lfs lists with ?refspec= but carries the ref of a create or unlock in the body, so the
// change never names the snapshot the listing was cached under.
await using ProxyFixture fixture = await ProxyFixture.StartAsync();
fixture.Upstream.Locks.Add("a");
const string Refspec = "?refspec=refs/heads/main";

Assert.HasCount(1, (await ListLocksAsync(fixture, Refspec))["locks"]!.AsArray());

if (expected > 1)
{
fixture.Upstream.Locks.Add("b");
}
else
{
fixture.Upstream.Locks.Clear();
}

using (HttpClient client = fixture.Client)
{
using HttpRequestMessage request = new(HttpMethod.Post, $"{LfsPath}{change}")
{
Content = new StringContent(body, Encoding.UTF8, "application/vnd.git-lfs+json"),
};

request.Headers.TryAddWithoutValidation("Authorization", Credential);
using HttpResponseMessage response = await client.SendAsync(request);

Check warning on line 248 in GitLfsCache.Tests/Integration/LockListCachingTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Consider using the overload that accepts a CancellationToken and pass 'TestContext.CancellationToken'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitLfsCache&issues=AaDnX3s1aZA5WhIAmQjJ&open=AaDnX3s1aZA5WhIAmQjJ&pullRequest=60
Assert.IsTrue(response.IsSuccessStatusCode, $"{(int)response.StatusCode}");
}

Assert.HasCount(expected, (await ListLocksAsync(fixture, Refspec))["locks"]!.AsArray());
}

[TestMethod]
public async Task LocksDisabled_RelaysExactlyAsBefore()
{
Expand Down
5 changes: 1 addition & 4 deletions GitLfsCache/Endpoints/LockRouteHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -231,10 +231,7 @@ private void InvalidateIfChanged(HttpContext context, LfsRoute route)
{
if (context.Response.StatusCode is >= 200 and < 300)
{
lockSnapshots.Invalidate(new LockSnapshotKey(
route.Upstream,
route.RepositoryPath,
context.Request.Query["refspec"].FirstOrDefault()));
lockSnapshots.Invalidate(route.Upstream, route.RepositoryPath);
}
}
}
14 changes: 11 additions & 3 deletions GitLfsCache/Locks/ILockSnapshotStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,20 @@ public interface ILockSnapshotStore
public void Publish(LockSnapshotKey key, LockSnapshot snapshot);

/// <summary>
/// Drops the snapshot for a repository, so the next read refreshes.
/// Drops every snapshot for a repository, whatever ref it was listed under, so the next read of
/// any of them refreshes.
/// </summary>
/// <remarks>
/// Called after a lock creation or release the proxy relayed successfully. Locks changed outside
/// the proxy are not seen here and are bounded only by the listing lifetime.
/// <para>
/// Every ref goes, not only the one the change named. A listing carries its ref in the query
/// string while a create or unlock carries it in the body, and a client can list under one ref
/// and lock under another, so the change cannot reliably name the snapshot it made wrong. Dropping
/// the rest only costs a refetch.
/// </para>
/// </remarks>
/// <param name="key">The repository whose snapshot is now known to be wrong.</param>
public void Invalidate(LockSnapshotKey key);
/// <param name="upstream">The upstream the repository is served from.</param>
/// <param name="repositoryPath">The repository whose snapshots are now known to be wrong.</param>
public void Invalidate(string upstream, string repositoryPath);
}
2 changes: 1 addition & 1 deletion GitLfsCache/Locks/LockFanOut.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ await Parallel.ForAsync(
// is about to look at them.
if (results.Any(result => JsonValues.Bool(result?["ok"]) == true))
{
snapshots.Invalidate(key);
snapshots.Invalidate(key.Upstream, key.RepositoryPath);
}

JsonArray array = [];
Expand Down
13 changes: 10 additions & 3 deletions GitLfsCache/Locks/LockSnapshotStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,16 @@ public void Publish(LockSnapshotKey key, LockSnapshot snapshot)
}

/// <inheritdoc />
public void Invalidate(LockSnapshotKey key)
public void Invalidate(string upstream, string repositoryPath)
{
Ensure.NotNull(key);
_snapshots.TryRemove(key, out _);
Ensure.NotNull(upstream);
Ensure.NotNull(repositoryPath);

foreach (LockSnapshotKey key in _snapshots.Keys.Where(key =>
string.Equals(key.Upstream, upstream, StringComparison.Ordinal)
&& string.Equals(key.RepositoryPath, repositoryPath, StringComparison.Ordinal)))
{
_snapshots.TryRemove(key, out _);
}
}
}
Loading