Skip to content

Commit bb1114f

Browse files
Upload all blobs as 64k chunks (#258)
This + retry seems to get uploads pretty reliable for ghcr.io and azurecr.io, at least in my testing.
1 parent 4a974f3 commit bb1114f

File tree

2 files changed

+62
-8
lines changed

2 files changed

+62
-8
lines changed

Microsoft.NET.Build.Containers/Registry.cs

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Net.Http;
77
using System.Net.Http.Headers;
88
using System.Net.Http.Json;
9+
using System.Runtime.ExceptionServices;
910
using System.Text;
1011
using System.Text.Json.Nodes;
1112
using System.Xml.Linq;
@@ -16,6 +17,7 @@ public record struct Registry(Uri BaseUri)
1617
{
1718
private const string DockerManifestV2 = "application/vnd.docker.distribution.manifest.v2+json";
1819
private const string DockerContainerV1 = "application/vnd.docker.container.image.v1+json";
20+
private const int MaxChunkSizeBytes = 1024 * 64;
1921

2022
private string RegistryName { get; } = BaseUri.Host;
2123

@@ -132,17 +134,69 @@ private readonly async Task UploadBlob(string name, string digest, Stream conten
132134
x = new UriBuilder(new Uri(BaseUri, pushResponse.Headers.Location?.OriginalString ?? ""));
133135
}
134136

137+
Uri patchUri = x.Uri;
138+
139+
x.Query += $"&digest={Uri.EscapeDataString(digest)}";
140+
141+
Uri putUri = x.Uri;
142+
143+
// TODO: this chunking is super tiny and probably not necessary; what does the docker client do
144+
// and can we be smarter?
145+
146+
byte[] chunkBackingStore = new byte[MaxChunkSizeBytes];
147+
148+
int chunkCount = 0;
149+
int chunkStart = 0;
150+
151+
while (contents.Position < contents.Length)
152+
{
153+
int bytesRead = await contents.ReadAsync(chunkBackingStore);
154+
155+
ByteArrayContent content = new (chunkBackingStore, offset: 0, count: bytesRead);
156+
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
157+
content.Headers.ContentLength = bytesRead;
158+
159+
// manual because ACR throws an error with the .NET type {"Range":"bytes 0-84521/*","Reason":"the Content-Range header format is invalid"}
160+
// content.Headers.Add("Content-Range", $"0-{contents.Length - 1}");
161+
Debug.Assert(content.Headers.TryAddWithoutValidation("Content-Range", $"{chunkStart}-{chunkStart + bytesRead - 1}"));
162+
163+
HttpResponseMessage patchResponse = await client.PatchAsync(patchUri, content);
164+
165+
if (patchResponse.StatusCode != HttpStatusCode.Accepted)
166+
{
167+
string errorMessage = $"Failed to upload blob to {patchUri}; recieved {patchResponse.StatusCode} with detail {await patchResponse.Content.ReadAsStringAsync()}";
168+
throw new ApplicationException(errorMessage);
169+
}
170+
171+
if (patchResponse.Headers.Location is { IsAbsoluteUri: true })
172+
{
173+
x = new UriBuilder(patchResponse.Headers.Location);
174+
}
175+
else
176+
{
177+
// if we don't trim the BaseUri and relative Uri of slashes, you can get invalid urls.
178+
// Uri constructor does this on our behalf.
179+
x = new UriBuilder(new Uri(BaseUri, patchResponse.Headers.Location?.OriginalString ?? ""));
180+
}
181+
182+
patchUri = x.Uri;
183+
184+
chunkCount += 1;
185+
chunkStart += bytesRead;
186+
}
187+
188+
// PUT with digest to finalize
135189
x.Query += $"&digest={Uri.EscapeDataString(digest)}";
136190

137-
// TODO: consider chunking
138-
StreamContent content = new StreamContent(contents);
139-
content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
140-
content.Headers.ContentLength = contents.Length;
141-
HttpResponseMessage putResponse = await client.PutAsync(x.Uri, content);
191+
putUri = x.Uri;
142192

143-
string resp = await putResponse.Content.ReadAsStringAsync();
193+
HttpResponseMessage finalizeResponse = await client.PutAsync(putUri, content: null);
144194

145-
putResponse.EnsureSuccessStatusCode();
195+
if (finalizeResponse.StatusCode != HttpStatusCode.Created)
196+
{
197+
string errorMessage = $"Failed to finalize upload to {putUri}; recieved {finalizeResponse.StatusCode} with detail {await finalizeResponse.Content.ReadAsStringAsync()}";
198+
throw new ApplicationException(errorMessage);
199+
}
146200
}
147201

148202
private readonly async Task<bool> BlobAlreadyUploaded(string name, string digest, HttpClient client)

containerize/Properties/launchSettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"profiles": {
33
"containerize": {
44
"commandName": "Project",
5-
"commandLineArgs": "\"S:\\play\\container-package-test\\obj\\Release\\net7.0\\PubTmp\\Out\" --baseregistry mcr.microsoft.com --baseimagename dotnet/aspnet --baseimagetag 7.0 --outputregistry rainercontainer.azurecr.io --imagename container-package-test --workingdirectory /app --entrypoint /app/container-package-test.exe --labels org.opencontainers.image.created=2022-10-27T14:17:19.9046559Z --imagetags latest --ports 80/tcp --environmentvariables ASPNETCORE_URLS=http://+:80 \r\n"
5+
"commandLineArgs": "\"S:\\play\\container-package-test\\obj\\Release\\net7.0\\PubTmp\\Out\" --baseregistry mcr.microsoft.com --baseimagename dotnet/aspnet --baseimagetag 7.0 --outputregistry ghcr.io --imagename baronfel/container-package-test --workingdirectory /app --entrypoint /app/container-package-test.exe --labels org.opencontainers.image.created=2022-10-27T14:17:19.9046559Z --imagetags latest --ports 80/tcp --environmentvariables ASPNETCORE_URLS=http://+:80 \r\n"
66
}
77
}
88
}

0 commit comments

Comments
 (0)