fix(deployment): pending-updates tick uses a slim instance projection, hash-only flatten, one untracked mismatch snapshot read and a per-tick secret lookup memo (review 2026-10-21 04/P2)
This commit is contained in:
+47
-21
@@ -87,7 +87,7 @@ public class PendingUpdatesEvaluatorTests
|
||||
NullLogger<DeploymentService>.Instance);
|
||||
|
||||
_evaluator = new PendingUpdatesEvaluator(
|
||||
_siteRepo, _templateRepo, _artifactService, _deploymentService,
|
||||
_siteRepo, _deploymentRepo, _artifactService, _deploymentService,
|
||||
NullLogger<PendingUpdatesEvaluator>.Instance);
|
||||
}
|
||||
|
||||
@@ -97,9 +97,9 @@ public class PendingUpdatesEvaluatorTests
|
||||
private void ArrangeSites(params Site[] sites) =>
|
||||
_siteRepo.GetAllSitesAsync(Arg.Any<CancellationToken>()).Returns(sites);
|
||||
|
||||
/// <summary>Stubs the instance list read.</summary>
|
||||
/// <summary>Stubs the slim deployed-instance list read.</summary>
|
||||
private void ArrangeInstances(params Instance[] instances) =>
|
||||
_templateRepo.GetAllInstancesAsync(Arg.Any<CancellationToken>()).Returns(instances);
|
||||
_deploymentRepo.GetDeployedInstanceSummariesAsync(Arg.Any<CancellationToken>()).Returns(instances);
|
||||
|
||||
/// <summary>Builds an instance in the given lifecycle state at the given site.</summary>
|
||||
private static Instance MakeInstance(int id, int siteId, InstanceState state) =>
|
||||
@@ -114,30 +114,35 @@ public class PendingUpdatesEvaluatorTests
|
||||
|
||||
/// <summary>Stubs a successful flatten of <paramref name="instanceId"/> at the given hash.</summary>
|
||||
private void ArrangeFlatten(int instanceId, FlattenedConfiguration config, string hash) =>
|
||||
_pipeline.FlattenAndValidateAsync(
|
||||
instanceId, Arg.Any<CancellationToken>(), Arg.Any<bool>(), Arg.Any<FlattenSession?>())
|
||||
.Returns(Result<FlatteningPipelineResult>.Success(
|
||||
new FlatteningPipelineResult(config, hash, ValidationResult.Success())));
|
||||
_pipeline.ComputeRevisionHashAsync(instanceId, Arg.Any<FlattenSession?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Result<string>.Success(hash));
|
||||
|
||||
/// <summary>Stubs a FAILED flatten of <paramref name="instanceId"/> — the "unknown" leg.</summary>
|
||||
private void ArrangeFlattenFailure(int instanceId) =>
|
||||
_pipeline.FlattenAndValidateAsync(
|
||||
instanceId, Arg.Any<CancellationToken>(), Arg.Any<bool>(), Arg.Any<FlattenSession?>())
|
||||
.Returns(Result<FlatteningPipelineResult>.Failure("template missing"));
|
||||
_pipeline.ComputeRevisionHashAsync(instanceId, Arg.Any<FlattenSession?>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Result<string>.Failure("template missing"));
|
||||
|
||||
/// <summary>Stubs the (InstanceId, RevisionHash) projection read.</summary>
|
||||
private void ArrangeDeployedHashes(params (int InstanceId, string Hash)[] rows) =>
|
||||
_deploymentRepo.GetDeployedRevisionHashesAsync(Arg.Any<CancellationToken>())
|
||||
.Returns(rows.ToDictionary(r => r.InstanceId, r => r.Hash));
|
||||
|
||||
/// <summary>Snapshots served by the batched, untracked snapshot read.</summary>
|
||||
private readonly Dictionary<int, DeployedConfigSnapshot> _snapshots = new();
|
||||
|
||||
/// <summary>Stubs the fat snapshot read for <paramref name="instanceId"/>.</summary>
|
||||
private void ArrangeSnapshot(int instanceId, FlattenedConfiguration config, string frozenHash) =>
|
||||
_deploymentRepo.GetDeployedSnapshotByInstanceIdAsync(instanceId, Arg.Any<CancellationToken>())
|
||||
.Returns(new DeployedConfigSnapshot("dep1", frozenHash, JsonSerializer.Serialize(config))
|
||||
{
|
||||
InstanceId = instanceId,
|
||||
DeployedAt = DateTimeOffset.UtcNow
|
||||
});
|
||||
private void ArrangeSnapshot(int instanceId, FlattenedConfiguration config, string frozenHash)
|
||||
{
|
||||
_snapshots[instanceId] = new DeployedConfigSnapshot("dep1", frozenHash, JsonSerializer.Serialize(config))
|
||||
{
|
||||
InstanceId = instanceId,
|
||||
DeployedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
_deploymentRepo.GetDeployedSnapshotsByInstanceIdsAsync(Arg.Any<IReadOnlyCollection<int>>(), Arg.Any<CancellationToken>())
|
||||
.Returns(ci => (IReadOnlyDictionary<int, DeployedConfigSnapshot>)ci.ArgAt<IReadOnlyCollection<int>>(0)
|
||||
.Where(_snapshots.ContainsKey)
|
||||
.ToDictionary(id => id, id => _snapshots[id]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asks the artifact service what the site's hash WOULD be right now — the value a
|
||||
@@ -282,10 +287,10 @@ public class PendingUpdatesEvaluatorTests
|
||||
|
||||
// A NotDeployed instance has nothing to be stale against; flattening it would
|
||||
// be pure cost on every TTL expiry, so it must never reach the batch.
|
||||
await _pipeline.Received(1).FlattenAndValidateAsync(
|
||||
1, Arg.Any<CancellationToken>(), Arg.Any<bool>(), Arg.Any<FlattenSession?>());
|
||||
await _pipeline.DidNotReceive().FlattenAndValidateAsync(
|
||||
2, Arg.Any<CancellationToken>(), Arg.Any<bool>(), Arg.Any<FlattenSession?>());
|
||||
await _pipeline.Received(1).ComputeRevisionHashAsync(
|
||||
1, Arg.Any<FlattenSession?>(), Arg.Any<CancellationToken>());
|
||||
await _pipeline.DidNotReceive().ComputeRevisionHashAsync(
|
||||
2, Arg.Any<FlattenSession?>(), Arg.Any<CancellationToken>());
|
||||
Assert.Equal(1, snapshot.Sites.Single().DeployedInstanceCount);
|
||||
}
|
||||
|
||||
@@ -338,6 +343,27 @@ public class PendingUpdatesEvaluatorTests
|
||||
Assert.False(snapshot.HasPendingUpdates);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// review 2026-10-21 04/P2: the instance list is the slim, untracked deployed-instance
|
||||
/// projection — never the full instance graph with all four override collections.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_ReadsTheSlimInstanceProjection_NotTheFullInstanceGraph()
|
||||
{
|
||||
ArrangeSites(new Site("Alpha", "site-a") { Id = 1 });
|
||||
ArrangeInstances(MakeInstance(1, 1, InstanceState.Enabled));
|
||||
var config = ScalarConfig("5");
|
||||
var hash = _hasher.ComputeHash(config);
|
||||
ArrangeDeployedHashes((1, hash));
|
||||
ArrangeFlatten(1, config, hash);
|
||||
|
||||
await _evaluator.EvaluateAsync();
|
||||
|
||||
await _deploymentRepo.Received(1).GetDeployedInstanceSummariesAsync(Arg.Any<CancellationToken>());
|
||||
await _templateRepo.DidNotReceive().GetAllInstancesAsync(Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
// ── ordering, ComputedAt, ForSites ───────────────────────────────────────
|
||||
|
||||
/// <summary>Verifies that sites are ordered by name, ignoring case.</summary>
|
||||
|
||||
Reference in New Issue
Block a user