Skip to content

[release/8.0.4xx] Containers - Retry on download blob #48987

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: release/8.0.4xx
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,11 @@ private static async Task<int> PushToRemoteRegistryAsync(ILogger logger, BuiltIm
cancellationToken)).ConfigureAwait(false);
logger.LogInformation(Strings.ContainerBuilder_ImageUploadedToRegistry, destinationImageReference, destinationImageReference.RemoteRegistry.RegistryName);
}
catch (UnableToDownloadFromRepositoryException)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect to see this updated in PushToLocalRegistryAsync as well, correct? That's at the root of the stack in the linked issue.

{
logger.LogError(Resource.FormatString(nameof(Strings.UnableToDownloadFromRepository)), sourceImageReference);
return 1;
}
catch (UnableToAccessRepositoryException)
{
logger.LogError(Resource.FormatString(nameof(Strings.UnableToAccessRepository), destinationImageReference.Repository, destinationImageReference.RemoteRegistry!.RegistryName));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

namespace Microsoft.NET.Build.Containers;

internal sealed class UnableToDownloadFromRepositoryException : Exception
{
public UnableToDownloadFromRepositoryException(string repository)
: base($"The download of the image from repository { repository } has failed.")
{
}
}
54 changes: 40 additions & 14 deletions src/Containers/Microsoft.NET.Build.Containers/Registry/Registry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ internal sealed class Registry
private const string DockerHubRegistry1 = "registry-1.docker.io";
private const string DockerHubRegistry2 = "registry.hub.docker.com";
private static readonly int s_defaultChunkSizeBytes = 1024 * 64;
private const int MaxDownloadRetries = 5;
private readonly Func<TimeSpan> _retryDelayProvider;

private readonly ILogger _logger;
private readonly IRegistryAPI _registryAPI;
Expand All @@ -86,7 +88,7 @@ internal sealed class Registry
/// </summary>
public string RegistryName { get; }

internal Registry(string registryName, ILogger logger, IRegistryAPI registryAPI, RegistrySettings? settings = null) :
internal Registry(string registryName, ILogger logger, IRegistryAPI registryAPI, RegistrySettings? settings = null, Func<TimeSpan>? retryDelayProvider = null) :
this(new Uri($"https://{registryName}"), logger, registryAPI, settings)
{ }

Expand All @@ -95,15 +97,15 @@ internal Registry(string registryName, ILogger logger, RegistryMode mode, Regist
{ }


internal Registry(Uri baseUri, ILogger logger, IRegistryAPI registryAPI, RegistrySettings? settings = null) :
internal Registry(Uri baseUri, ILogger logger, IRegistryAPI registryAPI, RegistrySettings? settings = null, Func<TimeSpan>? retryDelayProvider = null) :
this(baseUri, logger, new RegistryApiFactory(registryAPI), settings)
{ }

internal Registry(Uri baseUri, ILogger logger, RegistryMode mode, RegistrySettings? settings = null) :
this(baseUri, logger, new RegistryApiFactory(mode), settings)
{ }

private Registry(Uri baseUri, ILogger logger, RegistryApiFactory factory, RegistrySettings? settings = null)
private Registry(Uri baseUri, ILogger logger, RegistryApiFactory factory, RegistrySettings? settings = null, Func<TimeSpan>? retryDelayProvider = null)
{
RegistryName = DeriveRegistryName(baseUri);

Expand All @@ -117,6 +119,8 @@ private Registry(Uri baseUri, ILogger logger, RegistryApiFactory factory, Regist
_logger = logger;
_settings = settings ?? new RegistrySettings(RegistryName);
_registryAPI = factory.Create(RegistryName, BaseUri, logger, _settings.IsInsecure);

_retryDelayProvider = retryDelayProvider ?? (() => TimeSpan.FromSeconds(1));
}

private static string DeriveRegistryName(Uri baseUri)
Expand Down Expand Up @@ -401,26 +405,48 @@ public async Task<string> DownloadBlobAsync(string repository, Descriptor descri
{
cancellationToken.ThrowIfCancellationRequested();
string localPath = ContentStore.PathForDescriptor(descriptor);

if (File.Exists(localPath))
{
// Assume file is up to date and just return it
return localPath;
}

// No local copy, so download one
using Stream responseStream = await _registryAPI.Blob.GetStreamAsync(repository, descriptor.Digest, cancellationToken).ConfigureAwait(false);


string tempTarballPath = ContentStore.GetTempFile();
using (FileStream fs = File.Create(tempTarballPath))

int retryCount = 0;
while (retryCount < MaxDownloadRetries)
{
await responseStream.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
try
{
// No local copy, so download one
using Stream responseStream = await _registryAPI.Blob.GetStreamAsync(repository, descriptor.Digest, cancellationToken).ConfigureAwait(false);

using (FileStream fs = File.Create(tempTarballPath))
{
await responseStream.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
}

// Break the loop if successful
break;
}
catch (Exception ex)
{
retryCount++;
if (retryCount >= MaxDownloadRetries)
{
throw new UnableToDownloadFromRepositoryException(repository);
}

_logger.LogTrace("Download attempt {0}/{1} for repository '{2}' failed. Error: {3}", retryCount, MaxDownloadRetries, repository, ex.ToString());

// Wait before retrying
await Task.Delay(_retryDelayProvider(), cancellationToken).ConfigureAwait(false);
}
}

cancellationToken.ThrowIfCancellationRequested();


File.Move(tempTarballPath, localPath, overwrite: true);

return localPath;
}

Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,10 @@
<value>CONTAINER1015: Unable to access the repository '{0}' at tag '{1}' in the registry '{2}'. Please confirm that this name and tag are present in the registry.</value>
<comment>{StrBegin="CONTAINER1015: "}</comment>
</data>
<data name="UnableToDownloadFromRepository" xml:space="preserve">
<value>CONTAINER1018: Unable to download image from the repository '{0}'.</value>
<comment>{StrBegins="CONTAINER1018:" }</comment>
</data>
<data name="UnableToAccessRepository" xml:space="preserve">
<value>CONTAINER1016: Unable to access the repository '{0}' in the registry '{1}'. Please confirm your credentials are correct and that you have access to this repository and registry.</value>
<comment>{StrBegin="CONTAINER1016:" }</comment>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading