fix(confirmation): exclude NO_DATA from lookback to prevent stuck confirmation under neutral-dense history (#756)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Raj Nandan Sharma
2026-06-13 19:48:35 +05:30
parent 17e3fa6d77
commit 0940c8d01e
2 changed files with 8 additions and 5 deletions
+3 -2
View File
@@ -333,8 +333,8 @@ export class MonitoringRepository extends BaseRepository {
* Recent samples the Confirmation Threshold resolver needs, newest first: scheduled-check
* observations (REALTIME/TIMEOUT/ERROR) plus incident/maintenance overlays. MANUAL pushes
* and DEFAULT fill are excluded — they stay transparent to the counter. Returns `type` so
* the resolver can stop at overlay rows (freeze); observations whose status is NO_DATA are
* skipped as neutral by the resolver.
* the resolver can stop at overlay rows (freeze). Observations whose status is NO_DATA are
* excluded entirely (neutral — they neither advance nor reset the count and must not consume lookback slots).
*/
async getRecentSamplesForConfirmation(
monitor_tag: string,
@@ -346,6 +346,7 @@ export class MonitoringRepository extends BaseRepository {
.where("monitor_tag", monitor_tag)
.where("timestamp", "<", beforeTs)
.whereIn("type", [...OBSERVED_CHECK_TYPES, ...OVERLAY_TYPES])
.whereNot("status", GC.NO_DATA)
.orderBy("timestamp", "desc")
.limit(limit);
}
@@ -32,8 +32,9 @@ export interface ConfirmationDeps {
// Overlay sample types that freeze the count (must match OVERLAY_TYPES in the monitoring repository).
const OVERLAY_TYPES: string[] = [GC.INCIDENT, GC.MAINTENANCE];
// Extra lookback rows beyond the threshold, to tolerate neutral (NO_DATA) rows that are skipped
// rather than counted. Pathologically neutral-dense histories may delay confirmation by a check.
// Extra lookback rows beyond the threshold: headroom for the anchor row and any interleaved
// overlay rows. NO_DATA observations are excluded by the query (neutral), so they never
// consume slots — the buffer does not need to scale with NO_DATA density.
const LOOKBACK_BUFFER = 10;
/**
@@ -90,7 +91,7 @@ export async function resolveConfirmedStatus(
for (const row of recent) {
if (row.type !== null && OVERLAY_TYPES.indexOf(row.type) !== -1) break; // freeze boundary
const rawSide = sideOf(row.raw_status);
if (rawSide === null) continue; // NO_DATA: neutral — neither advance nor reset
if (rawSide === null) continue; // NO_DATA: neutral (excluded by the query; this is a defensive guard)
if (rawSide === observedSide && sideOf(row.status) === confirmedSide) {
pendingRun++;
pendingTimestamps.push(row.timestamp);
@@ -119,5 +120,6 @@ function confirmedSideStatus(
if (row.type !== null && OVERLAY_TYPES.indexOf(row.type) !== -1) continue;
if (sideOf(row.status) === confirmedSide && row.status) return row.status;
}
// Defensive fallback (unreachable when an anchor was found above); side is correct, severity may coarsen.
return confirmedSide === "UP" ? GC.UP : GC.DOWN;
}