fix(backend): IME archiver accepts an existing archive entry before walking the hosts

A version whose installer was already in the archive could only turn
Archived through the 409 on the write-once upload, which needs a
distribution host that still serves the build. For a version no host
serves any more the row stayed Failed:VersionMismatch next to its own
blob and re-walked every host once a day.

The archiver now checks the archive first: an entry whose ProductVersion
is the observed version ends the job as Archived with the hash and size
of the archived bytes and no download; a missing provenance sidecar is
written without a source URL. An entry holding another build, or an
unreadable one, is not accepted and the walk runs as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-09-21 20:58:49 +02:00
parent 5433bff3db
commit 12af1d6c76
2 changed files with 168 additions and 23 deletions
@@ -441,6 +441,96 @@ public class ImeMsiArchiverTests
Assert.Equal(0, archiver.ProvenanceCalls);
}
// =========================================================================
// Archive already holds the version before the walk (operator backfill of a
// build no host serves any more). The 409 path can never be reached for it —
// without the pre-walk check the row stays Failed:VersionMismatch forever.
// =========================================================================
[Fact]
public async Task ArchiveAsync_VersionAlreadyInArchive_NoHostServesIt_ArchivedWithoutDownload()
{
var archivedPayload = new byte[1024];
new Random(11).NextBytes(archivedPayload);
var expectedSha = Convert.ToHexString(SHA256.HashData(archivedPayload)).ToLowerInvariant();
var archiver = new RecordingArchiver
{
ArchivedMsiExists = true,
ArchivedPayload = archivedPayload,
DefaultProductVersion = "9.9.9.0", // every host has moved on
};
archiver.ProductVersionByUrl[RecordingArchiver.ArchivedOrigin] = Version;
var result = await archiver.ArchiveAsync(Envelope());
Assert.True(result.Success);
Assert.Equal(ImeMsiArchiver.Statuses.Archived, result.Status);
Assert.False(result.Retryable);
Assert.Equal($"{Version}/IntuneWindowsAgent.msi", result.BlobPath);
// The row must describe the archived bytes; the source URL of a backfill is unknown.
Assert.Equal(expectedSha, result.Sha256);
Assert.Equal(archivedPayload.Length, result.SizeBytes);
Assert.Null(result.SourceUrl);
Assert.Equal(0, archiver.DownloadCalls);
Assert.Equal(0, archiver.UploadCalls);
Assert.Equal(0, archiver.ProvenanceCalls); // sidecar came with the backfill
}
[Fact]
public async Task ArchiveAsync_VersionAlreadyInArchive_MissingSidecar_IsWrittenWithoutSourceUrl()
{
var archiver = new RecordingArchiver { ArchivedMsiExists = true, ProvenanceExists = false };
var result = await archiver.ArchiveAsync(Envelope());
Assert.True(result.Success);
Assert.Equal(0, archiver.DownloadCalls);
Assert.Equal(1, archiver.ProvenanceCalls);
Assert.Equal($"{Version}/provenance.json", archiver.ProvenanceLastPath);
using var doc = System.Text.Json.JsonDocument.Parse(archiver.ProvenanceLastJson);
Assert.Equal(System.Text.Json.JsonValueKind.Null, doc.RootElement.GetProperty("url").ValueKind);
// A null source must not compare equal to a null event URL.
Assert.False(doc.RootElement.GetProperty("urlFromEvent").GetBoolean());
Assert.Equal(0, doc.RootElement.GetProperty("candidates").GetArrayLength());
}
[Theory]
[InlineData("1.103.101.0")] // another ring's build filed under this version
[InlineData(null)] // not a readable package
public async Task ArchiveAsync_ArchiveEntryIsNotTheObservedVersion_NotAccepted_WalkRuns(string? archivedProductVersion)
{
var archiver = new RecordingArchiver
{
ArchivedMsiExists = true,
DefaultProductVersion = "9.9.9.0",
};
archiver.ProductVersionByUrl[RecordingArchiver.ArchivedOrigin] = archivedProductVersion;
var result = await archiver.ArchiveAsync(Envelope());
Assert.False(result.Success);
Assert.Equal(ImeMsiArchiver.Statuses.FailedVersionMismatch, result.Status);
Assert.Equal(ImeMsiArchiver.BuildCandidateUrls(EventUrl).Count, archiver.DownloadCalls);
Assert.Equal(0, archiver.ProvenanceCalls);
}
[Fact]
public async Task ArchiveAsync_ArchiveCheckFails_RetryableError_NoDownload()
{
var archiver = new RecordingArchiver
{
ArchivedMsiExists = true,
ArchivedOpenException = new RequestFailedException(500, "read failed"),
};
var result = await archiver.ArchiveAsync(Envelope());
Assert.False(result.Success);
Assert.Equal(ImeMsiArchiver.Statuses.FailedError, result.Status);
Assert.True(result.Retryable);
Assert.Equal(0, archiver.DownloadCalls);
}
// =========================================================================
// Provenance-write gap (daily review 2026-08-18): MSI upload is write-once,
// so a first attempt that dies between the MSI and the provenance upload
@@ -660,6 +750,10 @@ public class ImeMsiArchiverTests
public Exception? UploadException { get; set; }
public Exception? ProvenanceException { get; set; }
public bool ProvenanceExists { get; set; } = true;
/// <summary>Whether the archive already holds an installer for the version BEFORE the walk (operator backfill).</summary>
public bool ArchivedMsiExists { get; set; }
/// <summary>Key into <see cref="ProductVersionByUrl"/> for the archived bytes — they have no URL of their own.</summary>
public const string ArchivedOrigin = "archive://existing";
public byte[]? ArchivedPayload { get; set; }
public Exception? ArchivedOpenException { get; set; }
@@ -722,12 +816,15 @@ public class ImeMsiArchiverTests
return Task.CompletedTask;
}
protected override Task<bool> ProvenanceExistsAsync(string blobPath, CancellationToken cancellationToken)
=> Task.FromResult(ProvenanceExists);
protected override Task<bool> ArchiveBlobExistsAsync(string blobPath, CancellationToken cancellationToken)
=> Task.FromResult(blobPath.EndsWith("provenance.json", StringComparison.Ordinal)
? ProvenanceExists
: ArchivedMsiExists);
protected override Task<Stream> OpenArchivedMsiAsync(string blobPath, CancellationToken cancellationToken)
{
if (ArchivedOpenException is not null) throw ArchivedOpenException;
_currentUrl = ArchivedOrigin;
return Task.FromResult<Stream>(new MemoryStream(ArchivedPayload ?? Payload));
}
}
@@ -202,7 +202,7 @@ namespace AutopilotMonitor.Functions.Services.Ime
/// not permanently rejected as a bad version string), and the last archive activity is
/// older than <see cref="RequeueBackoff"/> — or there never was any (versions sighted
/// before the archiver existed, or archived by hand by an operator; the
/// 409 path then heals the row to Archived).
/// pre-walk archive check then turns the row to Archived without a download).
/// </summary>
public static bool ShouldRequeueOnSighting(
ImeVersionSighting? sighting, string? msiDownloadUrl, string? msiMatchedBy, DateTime nowUtc)
@@ -246,6 +246,13 @@ namespace AutopilotMonitor.Functions.Services.Ime
try
{
// The archive may already hold this version (operator backfill, earlier attempt).
// This must be checked BEFORE the walk: the 409 below is only reachable when a
// host still serves the build, which is never true for a version Microsoft has
// moved past — the row would stay Failed:VersionMismatch next to its own blob.
var existing = await TryCompleteFromArchiveAsync(envelope, version, blobPath, cancellationToken);
if (existing is not null) return existing;
var adminConfig = await GetAdminConfigurationAsync();
var maxSizeBytes = (long)adminConfig.MaxImeMsiDownloadSizeMB * 1024 * 1024;
@@ -385,7 +392,7 @@ namespace AutopilotMonitor.Functions.Services.Ime
ImeMsiArchiveEnvelope envelope, string version, string blobPath, string sourceUrl,
IReadOnlyList<CandidateAttempt> attempts, CancellationToken cancellationToken)
{
if (await ProvenanceExistsAsync($"{version}/provenance.json", cancellationToken))
if (await ArchiveBlobExistsAsync($"{version}/provenance.json", cancellationToken))
{
_logger.LogInformation(
"ImeMsiArchiver: blob {BlobPath} already exists — treating re-delivery as archived",
@@ -394,21 +401,7 @@ namespace AutopilotMonitor.Functions.Services.Ime
blobPath, null, null, sourceUrl);
}
string sha256;
long totalBytes;
string? archivedProductVersion;
using (var archived = await OpenArchivedMsiAsync(blobPath, cancellationToken))
using (var spool = CreateSpool())
{
using (var hashing = new HashingCappedReadStream(archived, maxBytes: 0))
{
await hashing.CopyToAsync(spool, cancellationToken);
sha256 = hashing.Sha256Hex;
totalBytes = hashing.TotalBytes;
}
spool.Position = 0;
archivedProductVersion = ReadProductVersion(spool);
}
var (sha256, totalBytes, archivedProductVersion) = await InspectArchivedMsiAsync(blobPath, cancellationToken);
await WriteProvenanceAsync(envelope, version, sourceUrl, sha256, totalBytes, archivedProductVersion, attempts, cancellationToken);
@@ -419,8 +412,62 @@ namespace AutopilotMonitor.Functions.Services.Ime
blobPath, sha256, totalBytes, sourceUrl);
}
/// <summary>
/// Pre-walk check: when the archive already holds an installer for this version AND
/// its ProductVersion is the observed one, the job is done without touching the CDN.
/// Hash and size come from the archived bytes so the row describes the blob; a missing
/// sidecar is written (no source URL — the bytes did not come from this attempt).
/// An entry holding another build is NOT accepted: the folder invariant is "the bytes
/// are that version", so the walk runs and the row keeps telling the truth. Returns
/// null when there is no verified entry. Failures propagate (retryable), like the heal.
/// </summary>
private async Task<ImeMsiArchiveResult?> TryCompleteFromArchiveAsync(
ImeMsiArchiveEnvelope envelope, string version, string blobPath, CancellationToken cancellationToken)
{
if (!await ArchiveBlobExistsAsync(blobPath, cancellationToken)) return null;
var (sha256, totalBytes, archivedProductVersion) = await InspectArchivedMsiAsync(blobPath, cancellationToken);
if (!VersionsMatch(version, archivedProductVersion))
{
_logger.LogWarning(
"ImeMsiArchiver: archive entry {BlobPath} holds ProductVersion {ProductVersion} — not the observed {Version}; not accepted, walking the hosts",
blobPath, archivedProductVersion ?? "(unreadable)", version);
return null;
}
if (!await ArchiveBlobExistsAsync($"{version}/provenance.json", cancellationToken))
{
await WriteProvenanceAsync(envelope, version, sourceUrl: null, sha256, totalBytes, archivedProductVersion,
Array.Empty<CandidateAttempt>(), cancellationToken);
}
_logger.LogInformation(
"ImeMsiArchiver: {Version} is already archived ({SizeBytes} bytes, sha256 {Sha256}) — no download",
version, totalBytes, sha256);
return new ImeMsiArchiveResult(true, Statuses.Archived, Retryable: false,
blobPath, sha256, totalBytes, SourceUrl: null);
}
/// <summary>Hash, size and ProductVersion of the installer that sits in the archive.</summary>
private async Task<(string Sha256, long TotalBytes, string? ProductVersion)> InspectArchivedMsiAsync(
string blobPath, CancellationToken cancellationToken)
{
using var archived = await OpenArchivedMsiAsync(blobPath, cancellationToken);
using var spool = CreateSpool();
string sha256;
long totalBytes;
using (var hashing = new HashingCappedReadStream(archived, maxBytes: 0))
{
await hashing.CopyToAsync(spool, cancellationToken);
sha256 = hashing.Sha256Hex;
totalBytes = hashing.TotalBytes;
}
spool.Position = 0;
return (sha256, totalBytes, ReadProductVersion(spool));
}
private async Task WriteProvenanceAsync(
ImeMsiArchiveEnvelope envelope, string version, string sourceUrl,
ImeMsiArchiveEnvelope envelope, string version, string? sourceUrl,
string sha256, long totalBytes, string? productVersion,
IReadOnlyList<CandidateAttempt> attempts, CancellationToken cancellationToken)
{
@@ -429,7 +476,8 @@ namespace AutopilotMonitor.Functions.Services.Ime
version,
productVersion,
url = sourceUrl,
urlFromEvent = string.Equals(sourceUrl, envelope.MsiDownloadUrl, StringComparison.OrdinalIgnoreCase),
urlFromEvent = sourceUrl is not null
&& string.Equals(sourceUrl, envelope.MsiDownloadUrl, StringComparison.OrdinalIgnoreCase),
msiMatchedBy = envelope.MsiMatchedBy,
sha256,
msiBytes = totalBytes,
@@ -497,8 +545,8 @@ namespace AutopilotMonitor.Functions.Services.Ime
}, cancellationToken);
}
/// <summary>True when the provenance sidecar already exists in the archive container.</summary>
protected virtual async Task<bool> ProvenanceExistsAsync(string blobPath, CancellationToken cancellationToken)
/// <summary>True when the blob (installer or provenance sidecar) already exists in the archive container.</summary>
protected virtual async Task<bool> ArchiveBlobExistsAsync(string blobPath, CancellationToken cancellationToken)
{
var containerClient = _blobStorage.GetContainerClient(Constants.BlobContainers.ImeArchive);
return (await containerClient.GetBlobClient(blobPath).ExistsAsync(cancellationToken)).Value;