fix: poll follower closeout after refresh

This commit is contained in:
Codex
2026-07-03 19:58:22 +00:00
parent 20a61b47e1
commit b3c039c8ca
2 changed files with 28 additions and 2 deletions
@@ -60,7 +60,7 @@ The automatic controller loop is non-blocking, so closeout acceleration cannot l
The same rule applies to git-mirror post-flush. If native status shows runtime/Argo are aligned but GitOps mirror is still pending flush, the automatic controller loop must run the bounded target-side git-mirror flush instead of leaving a follower in `ClosingOut` until a manual wait/closeout path is used.
After an automatic closeout accelerator runs, the same reconcile must do one bounded native status re-read and write the resulting state when it is already aligned. Do not defer the final `Noop` write to the next controller loop; loop interval plus another status-read can add enough idle time to exceed the 120s end-to-end budget even when PipelineRun, Argo and runtime are already ready. The re-read timeout must come from YAML follower budgets.
After an automatic closeout accelerator runs, the same reconcile must do a bounded native status re-read/poll and write the resulting state when it is already aligned. Do not defer the final `Noop` write to the next controller loop; loop interval plus another status-read can add enough idle time to exceed the 120s end-to-end budget even when PipelineRun, Argo and runtime are already ready. The re-read timeout must come from YAML follower budgets, and the short poll interval must come from YAML controller budgets. A single immediate re-read is insufficient when Argo accepts refresh first and updates operation/runtime state a few seconds later.
Stage timing rows must not label optional gates as `not-ready` when they are not part of that follower's closeout contract. For sentinel-like followers without a GitOps branch flush gate, git-mirror source snapshot readiness should render as source-ready/ready, while missing GitOps `githubInSync` remains `-`/not-applicable instead of a failure-looking state.
+27 -1
View File
@@ -875,7 +875,7 @@ async function decideAndMaybeTrigger(
else if (flush !== null) automaticCloseoutAccelerated = true;
}
if (automaticCloseoutAccelerated && observedSha !== null) {
const reread = await readAdapterStatus(registry, follower, { ...options, timeoutSeconds: follower.budgets.statusSeconds });
const reread = await readAdapterStatusAfterCloseoutAcceleration(registry, follower, observedSha, options);
if (!reread.ok) {
warnings.push(`post-closeout status re-read failed: ${redactText(tailText(reread.message, 300))}`);
} else if (reread.observedSha === observedSha) {
@@ -955,6 +955,32 @@ function shouldRefreshAutomaticCloseout(
return !argoReady || !runtimeAligned || live.targetSha !== observedSha;
}
async function readAdapterStatusAfterCloseoutAcceleration(
registry: BranchFollowerRegistry,
follower: FollowerSpec,
observedSha: string,
options: ParsedOptions,
): Promise<AdapterSummary> {
const timeoutSeconds = follower.budgets.statusSeconds;
const startedAt = Date.now();
const deadline = startedAt + Math.max(1, timeoutSeconds) * 1000;
let latest = await readAdapterStatus(registry, follower, { ...options, timeoutSeconds: Math.min(timeoutSeconds, remainingSeconds(startedAt, timeoutSeconds)) });
while (
latest.ok
&& latest.observedSha === observedSha
&& latest.aligned !== true
&& Date.now() < deadline
) {
const remaining = Math.max(0, deadline - Date.now());
const pollSeconds = Math.min(registry.controller.budgets.nativePollIntervalSeconds, Math.ceil(remaining / 1000));
if (pollSeconds <= 0) break;
runCommand(["sleep", String(pollSeconds)], repoRoot, { timeoutMs: (pollSeconds + 1) * 1000 });
if (Date.now() >= deadline) break;
latest = await readAdapterStatus(registry, follower, { ...options, timeoutSeconds: remainingSeconds(startedAt, timeoutSeconds) });
}
return latest;
}
function shouldFlushAutomaticCloseout(
follower: FollowerSpec,
observedSha: string | null,