fix(reconcile): batch startup reconcile into fixed round trips, bound central work by QueryTimeout, retry failed site passes with back-off (review 2026-10-21 03/P-1)

This commit is contained in:
Joseph Doherty
2026-10-22 06:25:57 -04:00
parent d6a9a79c27
commit 7c83a4aa94
9 changed files with 638 additions and 130 deletions
@@ -341,6 +341,37 @@ public interface IDeploymentManagerRepository
string configurationJson, string token, DateTimeOffset createdAtUtc, DateTimeOffset expiresAtUtc,
CancellationToken cancellationToken = default);
/// <summary>
/// Reads the deployed config snapshots of <paramref name="instanceIds"/> in ONE untracked
/// query — the startup-reconcile gap read. Only the gap's ids should be passed: this is the
/// read that materialises each snapshot's <c>ConfigurationJson</c>.
/// </summary>
/// <param name="instanceIds">Instances whose snapshots to read; duplicates are harmless.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>Instance id → snapshot, omitting instances with no snapshot (deleted race).</returns>
Task<IReadOnlyDictionary<int, DeployedConfigSnapshot>> GetDeployedSnapshotsByInstanceIdsAsync(
IReadOnlyCollection<int> instanceIds, CancellationToken cancellationToken = default);
/// <summary>
/// Batch form of <see cref="StagePendingIfAbsentAsync"/> for startup reconcile: expired
/// pending rows for the candidates' instances are deleted, then every candidate whose
/// instance has no LIVE pending row is inserted, all in one <c>SaveChanges</c>. A live row
/// (an in-flight deploy, or the peer node's concurrent reconcile) is never superseded.
/// Self-contained: commits its own writes.
/// </summary>
/// <param name="candidates">One new pending row per instance (later duplicates for the same instance are ignored).</param>
/// <param name="nowUtc">"Now": rows with <c>ExpiresAtUtc &lt;= nowUtc</c> are expired.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>
/// Instance id → the instance's live pending row after the call: the candidate itself
/// (reference-equal) when it was staged, otherwise the pre-existing live row, returned
/// WITHOUT its <c>ConfigurationJson</c> (empty string). An instance whose candidate could not
/// be staged and has no live row (e.g. the instance was deleted mid-reconcile) is omitted.
/// </returns>
Task<IReadOnlyDictionary<int, PendingDeployment>> StagePendingBatchIfAbsentAsync(
IReadOnlyList<PendingDeployment> candidates, DateTimeOffset nowUtc,
CancellationToken cancellationToken = default);
// Instance lookups for deployment pipeline
/// <summary>
/// Gets an instance by ID.