Skip to content

Commit f96ff83

Browse files
authored
[automated] Merge branch 'release/9.0.3xx' => 'main' (#49389)
2 parents 80b4384 + 19caa6f commit f96ff83

File tree

16 files changed

+202
-88
lines changed

16 files changed

+202
-88
lines changed

.github/workflows/remove-lockdown-label.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,4 @@ jobs:
6868
name: 'Branch Lockdown'
6969
});
7070
console.log(`Removed Branch Lockdown label from PR #${pr.number}`);
71-
}
71+
}

NuGet.config

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<!-- Begin: Package sources from dotnet-aspire -->
77
<!-- End: Package sources from dotnet-aspire -->
88
<!-- Begin: Package sources from dotnet-runtime -->
9+
<add key="darc-int-dotnet-runtime-3875b54" value="https://pkgs.dev.azure.com/dnceng/internal/_packaging/darc-int-dotnet-runtime-3875b54e/nuget/v3/index.json" />
910
<!-- End: Package sources from dotnet-runtime -->
1011
<!--End: Package sources managed by Dependency Flow automation. Do not edit the sources above.-->
1112
<add key="dotnet6" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet6/nuget/v3/index.json" />
@@ -37,6 +38,7 @@
3738
<clear />
3839
<!--Begin: Package sources managed by Dependency Flow automation. Do not edit the sources below.-->
3940
<!-- Begin: Package sources from dotnet-runtime -->
41+
<add key="darc-int-dotnet-runtime-3875b54" value="true" />
4042
<!-- End: Package sources from dotnet-runtime -->
4143
<!--End: Package sources managed by Dependency Flow automation. Do not edit the sources above.-->
4244
</disabledPackageSources>

src/Containers/Microsoft.NET.Build.Containers/ContainerBuilder.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,11 @@ private static async Task<int> PushToLocalRegistryAsync(ILogger logger, BuiltIma
216216
await containerRegistry.LoadAsync(builtImage, sourceImageReference, destinationImageReference, cancellationToken).ConfigureAwait(false);
217217
logger.LogInformation(Strings.ContainerBuilder_ImageUploadedToLocalDaemon, destinationImageReference, containerRegistry);
218218
}
219+
catch (UnableToDownloadFromRepositoryException)
220+
{
221+
logger.LogError(Resource.FormatString(nameof(Strings.UnableToDownloadFromRepository)), sourceImageReference);
222+
return 1;
223+
}
219224
catch (Exception ex)
220225
{
221226
logger.LogError(Resource.FormatString(nameof(Strings.RegistryOutputPushFailed), ex.Message));
@@ -243,11 +248,6 @@ private static async Task<int> PushToRemoteRegistryAsync(ILogger logger, BuiltIm
243248
logger.LogError(Resource.FormatString(nameof(Strings.UnableToDownloadFromRepository)), sourceImageReference);
244249
return 1;
245250
}
246-
catch (UnableToAccessRepositoryException)
247-
{
248-
logger.LogError(Resource.FormatString(nameof(Strings.UnableToAccessRepository), destinationImageReference.Repository, destinationImageReference.RemoteRegistry!.RegistryName));
249-
return 1;
250-
}
251251
catch (Exception e)
252252
{
253253
logger.LogError(Resource.FormatString(nameof(Strings.RegistryOutputPushFailed), e.Message));

src/Containers/Microsoft.NET.Build.Containers/ImagePublisher.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ private static async Task PushToLocalRegistryAsync<T>(
107107
await loadFunc(image, sourceImageReference, destinationImageReference, cancellationToken).ConfigureAwait(false);
108108
Log.LogMessage(MessageImportance.High, Strings.ContainerBuilder_ImageUploadedToLocalDaemon, destinationImageReference, localRegistry);
109109
}
110+
catch (UnableToDownloadFromRepositoryException)
111+
{
112+
Log.LogErrorWithCodeFromResources(nameof(Strings.UnableToDownloadFromRepository), sourceImageReference);
113+
}
110114
catch (ContainerHttpException e)
111115
{
112116
Log.LogErrorFromException(e, true);

src/Containers/Microsoft.NET.Build.Containers/Registry/Registry.cs

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ internal sealed class Registry
7575
private const string DockerHubRegistry1 = "registry-1.docker.io";
7676
private const string DockerHubRegistry2 = "registry.hub.docker.com";
7777
private static readonly int s_defaultChunkSizeBytes = 1024 * 64;
78+
private const int MaxDownloadRetries = 5;
79+
private readonly Func<TimeSpan> _retryDelayProvider;
7880

7981
private readonly ILogger _logger;
8082
private readonly IRegistryAPI _registryAPI;
@@ -87,7 +89,7 @@ internal sealed class Registry
8789
/// </summary>
8890
public string RegistryName { get; }
8991

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

@@ -96,15 +98,15 @@ internal Registry(string registryName, ILogger logger, RegistryMode mode, Regist
9698
{ }
9799

98100

99-
internal Registry(Uri baseUri, ILogger logger, IRegistryAPI registryAPI, RegistrySettings? settings = null) :
101+
internal Registry(Uri baseUri, ILogger logger, IRegistryAPI registryAPI, RegistrySettings? settings = null, Func<TimeSpan>? retryDelayProvider = null) :
100102
this(baseUri, logger, new RegistryApiFactory(registryAPI), settings)
101103
{ }
102104

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

107-
private Registry(Uri baseUri, ILogger logger, RegistryApiFactory factory, RegistrySettings? settings = null)
109+
private Registry(Uri baseUri, ILogger logger, RegistryApiFactory factory, RegistrySettings? settings = null, Func<TimeSpan>? retryDelayProvider = null)
108110
{
109111
RegistryName = DeriveRegistryName(baseUri);
110112

@@ -118,6 +120,8 @@ private Registry(Uri baseUri, ILogger logger, RegistryApiFactory factory, Regist
118120
_logger = logger;
119121
_settings = settings ?? new RegistrySettings(RegistryName);
120122
_registryAPI = factory.Create(RegistryName, BaseUri, logger, _settings.IsInsecure);
123+
124+
_retryDelayProvider = retryDelayProvider ?? (() => TimeSpan.FromSeconds(1));
121125
}
122126

123127
private static string DeriveRegistryName(Uri baseUri)
@@ -404,33 +408,48 @@ public async Task<string> DownloadBlobAsync(string repository, Descriptor descri
404408
{
405409
cancellationToken.ThrowIfCancellationRequested();
406410
string localPath = ContentStore.PathForDescriptor(descriptor);
407-
411+
408412
if (File.Exists(localPath))
409413
{
410414
// Assume file is up to date and just return it
411415
return localPath;
412416
}
413-
417+
414418
string tempTarballPath = ContentStore.GetTempFile();
415-
416-
try
419+
420+
int retryCount = 0;
421+
while (retryCount < MaxDownloadRetries)
417422
{
418-
// No local copy, so download one
419-
using Stream responseStream = await _registryAPI.Blob.GetStreamAsync(repository, descriptor.Digest, cancellationToken).ConfigureAwait(false);
420-
421-
using (FileStream fs = File.Create(tempTarballPath))
423+
try
422424
{
423-
await responseStream.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
425+
// No local copy, so download one
426+
using Stream responseStream = await _registryAPI.Blob.GetStreamAsync(repository, descriptor.Digest, cancellationToken).ConfigureAwait(false);
427+
428+
using (FileStream fs = File.Create(tempTarballPath))
429+
{
430+
await responseStream.CopyToAsync(fs, cancellationToken).ConfigureAwait(false);
431+
}
432+
433+
// Break the loop if successful
434+
break;
435+
}
436+
catch (Exception ex)
437+
{
438+
retryCount++;
439+
if (retryCount >= MaxDownloadRetries)
440+
{
441+
throw new UnableToDownloadFromRepositoryException(repository);
442+
}
443+
444+
_logger.LogTrace("Download attempt {0}/{1} for repository '{2}' failed. Error: {3}", retryCount, MaxDownloadRetries, repository, ex.ToString());
445+
446+
// Wait before retrying
447+
await Task.Delay(_retryDelayProvider(), cancellationToken).ConfigureAwait(false);
424448
}
425449
}
426-
catch (Exception)
427-
{
428-
throw new UnableToDownloadFromRepositoryException(repository);
429-
}
430-
cancellationToken.ThrowIfCancellationRequested();
431-
450+
432451
File.Move(tempTarballPath, localPath, overwrite: true);
433-
452+
434453
return localPath;
435454
}
436455

test/Microsoft.NET.Build.Containers.IntegrationTests/EndToEndTests.cs

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using System.Formats.Tar;
55
using System.Runtime.CompilerServices;
66
using System.Text.Json;
7+
using FakeItEasy;
8+
using Microsoft.Build.Framework;
79
using Microsoft.DotNet.Cli.Utils;
810
using Microsoft.NET.Build.Containers.LocalDaemons;
911
using Microsoft.NET.Build.Containers.Resources;
@@ -1431,9 +1433,10 @@ static string[] DecideEntrypoint(string rid, string appName, string workingDir)
14311433
}
14321434

14331435
[DockerAvailableFact]
1434-
public async Task CheckErrorMessageWhenSourceRepositoryThrows()
1436+
public async void CheckDownloadErrorMessageWhenSourceRepositoryThrows()
14351437
{
1436-
ILogger logger = _loggerFactory.CreateLogger(nameof(CheckErrorMessageWhenSourceRepositoryThrows));
1438+
var loggerFactory = new TestLoggerFactory(_testOutput);
1439+
var logger = loggerFactory.CreateLogger(nameof(CheckDownloadErrorMessageWhenSourceRepositoryThrows));
14371440
string rid = "win-x64";
14381441
string publishDirectory = BuildLocalApp(tfm: ToolsetInfo.CurrentTargetFramework, rid: rid);
14391442

@@ -1459,24 +1462,39 @@ public async Task CheckErrorMessageWhenSourceRepositoryThrows()
14591462

14601463
// Load the image into the local registry
14611464
var sourceReference = new SourceImageReference(registry, "some_random_image", DockerRegistryManager.Net9ImageTag, null);
1462-
var destinationReference = new DestinationImageReference(registry, NewImageName(), new[] { rid });
1463-
var sawMyException = false;
1464-
try
1465-
{
1466-
await new DockerCli(_loggerFactory).LoadAsync(builtImage, sourceReference, destinationReference, default).ConfigureAwait(false);
1467-
}
1468-
catch (UnableToDownloadFromRepositoryException e)
1469-
{
1470-
sawMyException = true;
1471-
Assert.Contains("The download of the image from repository some_random_image has failed", e.ToString());
1472-
}
1473-
Assert.True(sawMyException);
1465+
string archivePath = Path.Combine(TestSettings.TestArtifactsDirectory, nameof(CheckDownloadErrorMessageWhenSourceRepositoryThrows));
1466+
var destinationReference = new DestinationImageReference(new ArchiveFileRegistry(archivePath), NewImageName(), new[] { rid });
1467+
1468+
(var taskLog, var errors) = SetupTaskLog();
1469+
var telemetry = new Telemetry(sourceReference, destinationReference, taskLog);
1470+
1471+
await ImagePublisher.PublishImageAsync(builtImage, sourceReference, destinationReference, taskLog, telemetry, CancellationToken.None)
1472+
.ConfigureAwait(false);
1473+
1474+
// Assert the error message
1475+
Assert.True(taskLog.HasLoggedErrors);
1476+
Assert.NotNull(errors);
1477+
Assert.Single(errors);
1478+
Assert.Contains("Unable to download image from the repository", errors[0]);
14741479

14751480
static string[] DecideEntrypoint(string rid, string appName, string workingDir)
14761481
{
14771482
var binary = rid.StartsWith("win", StringComparison.Ordinal) ? $"{appName}.exe" : appName;
14781483
return new[] { $"{workingDir}/{binary}" };
14791484
}
1485+
1486+
static (Microsoft.Build.Utilities.TaskLoggingHelper, List<string?> errors) SetupTaskLog()
1487+
{
1488+
// We can use any Task, we just need TaskLoggingHelper
1489+
Tasks.CreateNewImage cni = new();
1490+
List<string?> errors = new();
1491+
IBuildEngine buildEngine = A.Fake<IBuildEngine>();
1492+
A.CallTo(() => buildEngine.LogWarningEvent(A<BuildWarningEventArgs>.Ignored)).Invokes((BuildWarningEventArgs e) => errors.Add(e.Message));
1493+
A.CallTo(() => buildEngine.LogErrorEvent(A<BuildErrorEventArgs>.Ignored)).Invokes((BuildErrorEventArgs e) => errors.Add(e.Message));
1494+
A.CallTo(() => buildEngine.LogMessageEvent(A<BuildMessageEventArgs>.Ignored)).Invokes((BuildMessageEventArgs e) => errors.Add(e.Message));
1495+
cni.BuildEngine = buildEngine;
1496+
return (cni.Log, errors);
1497+
}
14801498
}
14811499

14821500
[DockerAvailableFact(checkContainerdStoreAvailability: true)]

test/Microsoft.NET.Build.Containers.UnitTests/RegistryTests.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,77 @@ public void IsRegistryInsecure(string registryName, string? insecureRegistriesEn
545545
Assert.Equal(expectedInsecure, registrySettings.IsInsecure);
546546
}
547547

548+
[Fact]
549+
public async Task DownloadBlobAsync_RetriesOnFailure()
550+
{
551+
// Arrange
552+
var logger = _loggerFactory.CreateLogger(nameof(DownloadBlobAsync_RetriesOnFailure));
553+
554+
var repoName = "testRepo";
555+
var descriptor = new Descriptor(SchemaTypes.OciLayerGzipV1, "sha256:testdigest1234", 1234);
556+
var cancellationToken = CancellationToken.None;
557+
558+
var mockRegistryAPI = new Mock<IRegistryAPI>(MockBehavior.Strict);
559+
mockRegistryAPI
560+
.SetupSequence(api => api.Blob.GetStreamAsync(repoName, descriptor.Digest, cancellationToken))
561+
.ThrowsAsync(new Exception("Simulated failure 1")) // First attempt fails
562+
.ThrowsAsync(new Exception("Simulated failure 2")) // Second attempt fails
563+
.ReturnsAsync(new MemoryStream(new byte[] { 1, 2, 3 })); // Third attempt succeeds
564+
565+
Registry registry = new(repoName, logger, mockRegistryAPI.Object, null, () => TimeSpan.Zero);
566+
567+
string? result = null;
568+
try
569+
{
570+
// Act
571+
result = await registry.DownloadBlobAsync(repoName, descriptor, cancellationToken);
572+
573+
// Assert
574+
Assert.NotNull(result);
575+
Assert.True(File.Exists(result)); // Ensure the file was successfully downloaded
576+
mockRegistryAPI.Verify(api => api.Blob.GetStreamAsync(repoName, descriptor.Digest, cancellationToken), Times.Exactly(3)); // Verify retries
577+
}
578+
finally
579+
{
580+
// Cleanup
581+
if (result != null)
582+
{
583+
File.Delete(result);
584+
}
585+
}
586+
}
587+
588+
[Fact]
589+
public async Task DownloadBlobAsync_ThrowsAfterMaxRetries()
590+
{
591+
// Arrange
592+
var logger = _loggerFactory.CreateLogger(nameof(DownloadBlobAsync_ThrowsAfterMaxRetries));
593+
594+
var repoName = "testRepo";
595+
var descriptor = new Descriptor(SchemaTypes.OciLayerGzipV1, "sha256:testdigest1234", 1234);
596+
var cancellationToken = CancellationToken.None;
597+
598+
var mockRegistryAPI = new Mock<IRegistryAPI>(MockBehavior.Strict);
599+
// Simulate 5 failures (assuming your retry logic attempts 5 times before throwing)
600+
mockRegistryAPI
601+
.SetupSequence(api => api.Blob.GetStreamAsync(repoName, descriptor.Digest, cancellationToken))
602+
.ThrowsAsync(new Exception("Simulated failure 1"))
603+
.ThrowsAsync(new Exception("Simulated failure 2"))
604+
.ThrowsAsync(new Exception("Simulated failure 3"))
605+
.ThrowsAsync(new Exception("Simulated failure 4"))
606+
.ThrowsAsync(new Exception("Simulated failure 5"));
607+
608+
Registry registry = new(repoName, logger, mockRegistryAPI.Object, null, () => TimeSpan.Zero);
609+
610+
// Act & Assert
611+
await Assert.ThrowsAsync<UnableToDownloadFromRepositoryException>(async () =>
612+
{
613+
await registry.DownloadBlobAsync(repoName, descriptor, cancellationToken);
614+
});
615+
616+
mockRegistryAPI.Verify(api => api.Blob.GetStreamAsync(repoName, descriptor.Digest, cancellationToken), Times.Exactly(5));
617+
}
618+
548619
private static NextChunkUploadInformation ChunkUploadSuccessful(Uri requestUri, Uri uploadUrl, int? contentLength, HttpStatusCode code = HttpStatusCode.Accepted)
549620
{
550621
return new(uploadUrl);

0 commit comments

Comments
 (0)