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
77 changes: 77 additions & 0 deletions GitLfsCache.Tests/Integration/ProxyFlowTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,83 @@
Assert.AreEqual("bytes=0-3", fixture.Upstream.Requests.Last().Range);
}

/// <summary>Stores an object through a whole download, then returns a fresh download href for it.</summary>
private static async Task<string> WarmAsync(ProxyFixture fixture, byte[] content, string oid)
{
fixture.Upstream.AddObject(oid, content);
JsonNode coldBatch = await PostBatchAsync(fixture, "download", oid, content.Length);
using HttpClient client = fixture.Client;
await client.GetByteArrayAsync(Relative(HrefOf(coldBatch, "download")));
Assert.IsTrue(fixture.Store.Exists("github", oid));

JsonNode warmBatch = await PostBatchAsync(fixture, "download", oid, content.Length);
return Relative(HrefOf(warmBatch, "download"));
}

[TestMethod]
[DataRow(10L, 19L, 10, 10, DisplayName = "Closed range")]
[DataRow(null, 5L, 31, 5, DisplayName = "Suffix range")]
[DataRow(30L, null, 30, 6, DisplayName = "Open range")]
public async Task Download_RangeRequestOnAHit_ReturnsPartialContentFromTheStore(
long? from,
long? to,
int expectedStart,
int expectedLength)
{
await using ProxyFixture fixture = await ProxyFixture.StartAsync();
(byte[] content, string oid) = Object("0123456789abcdefghijklmnopqrstuvwxyz");
string href = await WarmAsync(fixture, content, oid);
using HttpClient client = fixture.Client;

using HttpRequestMessage request = new(HttpMethod.Get, href);
request.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(from, to);

using HttpResponseMessage response = await client.SendAsync(request);

Assert.AreEqual(HttpStatusCode.PartialContent, response.StatusCode);
CollectionAssert.AreEqual(
content.Skip(expectedStart).Take(expectedLength).ToArray(),
await response.Content.ReadAsByteArrayAsync());
Assert.AreEqual(
$"bytes {expectedStart}-{expectedStart + expectedLength - 1}/{content.Length}",
response.Content.Headers.ContentRange?.ToString());
Assert.AreEqual(1, fixture.Upstream.FetchCount(oid), "A ranged hit must be served from the store");
}

[TestMethod]
public async Task Download_UnsatisfiableRangeOnAHit_Returns416()
{
await using ProxyFixture fixture = await ProxyFixture.StartAsync();
(byte[] content, string oid) = Object("too short for that range");
string href = await WarmAsync(fixture, content, oid);
using HttpClient client = fixture.Client;

using HttpRequestMessage request = new(HttpMethod.Get, href);

Check warning on line 357 in GitLfsCache.Tests/Integration/ProxyFlowTests.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=AaEacFfLSvxzqWj1Tec_&open=AaEacFfLSvxzqWj1Tec_&pullRequest=107
request.Headers.Range = new System.Net.Http.Headers.RangeHeaderValue(1000, 2000);

using HttpResponseMessage response = await client.SendAsync(request);

Assert.AreEqual(HttpStatusCode.RequestedRangeNotSatisfiable, response.StatusCode);

Check warning on line 362 in GitLfsCache.Tests/Integration/ProxyFlowTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitLfsCache&issues=AaEacFfLSvxzqWj1Tec-&open=AaEacFfLSvxzqWj1Tec-&pullRequest=107

Check warning on line 362 in GitLfsCache.Tests/Integration/ProxyFlowTests.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=AaEacFfLSvxzqWj1TedA&open=AaEacFfLSvxzqWj1TedA&pullRequest=107
Assert.AreEqual($"bytes */{content.Length}", response.Content.Headers.ContentRange?.ToString());
}

[TestMethod]
public async Task Download_HitWithoutARange_ReturnsTheWholeObjectAndAdvertisesRanges()
{
await using ProxyFixture fixture = await ProxyFixture.StartAsync();
(byte[] content, string oid) = Object("whole object please");
string href = await WarmAsync(fixture, content, oid);
using HttpClient client = fixture.Client;

using HttpResponseMessage response = await client.GetAsync(href);

Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
CollectionAssert.AreEqual(content, await response.Content.ReadAsByteArrayAsync());
Assert.AreEqual(content.Length, response.Content.Headers.ContentLength);
Assert.AreEqual("application/octet-stream", response.Content.Headers.ContentType?.MediaType);
CollectionAssert.Contains(response.Headers.AcceptRanges.ToList(), "bytes");

Check warning on line 380 in GitLfsCache.Tests/Integration/ProxyFlowTests.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=AaEacFfLSvxzqWj1TedB&open=AaEacFfLSvxzqWj1TedB&pullRequest=107
}

[TestMethod]
public async Task Download_ObjectUpstreamDoesNotHave_ReportsTheErrorPerObject()
{
Expand All @@ -314,13 +391,13 @@

Assert.AreEqual(404, entry["error"]!["code"]!.GetValue<int>());
Assert.IsFalse(entry.AsObject().ContainsKey("actions"));
}

Check warning on line 394 in GitLfsCache.Tests/Integration/ProxyFlowTests.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=AaEacFfLSvxzqWj1TedE&open=AaEacFfLSvxzqWj1TedE&pullRequest=107

[TestMethod]
public async Task Upload_IsRelayedUpstreamAndStoredOnTheWayThrough()

Check warning on line 397 in GitLfsCache.Tests/Integration/ProxyFlowTests.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=AaEacFfLSvxzqWj1TedF&open=AaEacFfLSvxzqWj1TedF&pullRequest=107

Check warning on line 397 in GitLfsCache.Tests/Integration/ProxyFlowTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitLfsCache&issues=AaEacFfLSvxzqWj1TedC&open=AaEacFfLSvxzqWj1TedC&pullRequest=107
{
await using ProxyFixture fixture = await ProxyFixture.StartAsync();
(byte[] content, string oid) = Object("pushed object");

Check warning on line 400 in GitLfsCache.Tests/Integration/ProxyFlowTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.Contains' instead of 'CollectionAssert.Contains'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_GitLfsCache&issues=AaEacFfLSvxzqWj1TedD&open=AaEacFfLSvxzqWj1TedD&pullRequest=107

JsonNode batch = await PostBatchAsync(fixture, "upload", oid, content.Length);
using HttpClient client = fixture.Client;
Expand Down
28 changes: 11 additions & 17 deletions GitLfsCache/Endpoints/ObjectRouteHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
/// <param name="metrics">Cache counters.</param>
/// <param name="options">The configured options.</param>
/// <param name="logger">Logger.</param>
internal sealed class ObjectRouteHandler(

Check warning on line 38 in GitLfsCache/Endpoints/ObjectRouteHandler.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Constructor has 9 parameters, which is greater than the 7 authorized.

Check warning on line 38 in GitLfsCache/Endpoints/ObjectRouteHandler.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Constructor has 9 parameters, which is greater than the 7 authorized.

Check warning on line 38 in GitLfsCache/Endpoints/ObjectRouteHandler.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Constructor has 9 parameters, which is greater than the 7 authorized.

Check warning on line 38 in GitLfsCache/Endpoints/ObjectRouteHandler.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Constructor has 9 parameters, which is greater than the 7 authorized.
IUpstreamClient upstreamClient,
IHrefTokenCodec codec,
BatchRewriter rewriter,
Expand Down Expand Up @@ -151,7 +151,7 @@
metrics.RecordHit(route.Upstream, length);
EndpointLog.ServedFromCache(logger, token.Oid, route.Upstream);

await ServeFromStoreAsync(context, cached, length, cancellationToken).ConfigureAwait(false);
await ServeFromStoreAsync(context, cached).ConfigureAwait(false);
}

return;
Expand Down Expand Up @@ -179,8 +179,11 @@
// A follower released without the object, because the leader's client went away or the
// leader stalled, queues again: one of the released followers becomes the new leader and
// the rest wait for it, rather than every one of them fetching the same object at once.
for (int attempt = 1; !ticket.IsLeader; attempt++)
int attempt = 0;

while (!ticket.IsLeader)
{
attempt++;
EndpointLog.WaitingForLeader(logger, token.Oid, route.Upstream);

bool published = await ticket
Expand All @@ -198,8 +201,7 @@
{
store.Touch(route.Upstream, token.Oid);
metrics.RecordHit(route.Upstream, nowLength);
await ServeFromStoreAsync(context, nowCached, nowLength, cancellationToken)
.ConfigureAwait(false);
await ServeFromStoreAsync(context, nowCached).ConfigureAwait(false);
}

return;
Expand Down Expand Up @@ -243,7 +245,7 @@
/// server refuses part way through surfaces as a 500 from the upstream relay rather than a 413.
/// The limit is lifted here, in the library, so every host is covered, and only for an upload
/// whose token has already been checked, so batch and lock bodies keep the server's bound.
/// </remarks>

Check warning on line 248 in GitLfsCache/Endpoints/ObjectRouteHandler.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Resource 'ticket' has already been disposed explicitly or through a using statement implicitly. Remove the redundant disposal.

Check warning on line 248 in GitLfsCache/Endpoints/ObjectRouteHandler.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Resource 'ticket' has already been disposed explicitly or through a using statement implicitly. Remove the redundant disposal.
/// <param name="context">The request context.</param>
private static void LiftRequestBodyLimit(HttpContext context)
{
Expand Down Expand Up @@ -489,19 +491,11 @@
return true;
}

private static async Task ServeFromStoreAsync(
HttpContext context,
Stream cached,
long length,
CancellationToken cancellationToken)
{
context.Response.ContentType = OctetStream;
context.Response.ContentLength = length;

await StreamTee
.CopyAsync(cached, context.Response.Body, null, null, cancellationToken)
.ConfigureAwait(false);
}
// ASP.NET Core's range processing answers a Range with 206 and Content-Range, an unsatisfiable
// one with 416, and advertises Accept-Ranges: bytes. That is what lets git-lfs resume an
// interrupted download of a cached object instead of starting it over.
private static Task ServeFromStoreAsync(HttpContext context, Stream cached) =>
Results.Stream(cached, OctetStream, enableRangeProcessing: true).ExecuteAsync(context);

private static void CopyTransferHeaders(HttpResponseMessage response, HttpContext context)
{
Expand Down
Loading