koala73/worldmonitor · error · Error

ECCC_COUNT_DRIFT

ECCC_COUNT_DRIFT

Error message

ECCC_COUNT_DRIFT

What it means

Within one status collection ('issued' or 'continued'), the loop records numberMatched from the first page and requires every subsequent page to report the identical value. If a later page's numberMatched differs from the previously seen value, the collection changed mid-pagination, offsets become meaningless, and the loop throws Error('ECCC_COUNT_DRIFT') to avoid publishing a mixed-in-time, possibly inconsistent alert set.

Solutions

  1. Retry the whole fetchEcccAlertFeatures() call after a short delay; drift is transient during active weather and a fresh run starts with a new baseline numberMatched.
  2. Check whether the ECCC API offers a snapshot/transaction timestamp parameter to pin the collection during pagination; if so, include it in ECCC_ALERTS_URLS.
  3. If drift is frequent, reduce wall-clock time per fetch (increase concurrency or page size where permitted) so pagination completes within one collection snapshot.
  4. Verify the upstream API's numberMatched semantics haven't changed (e.g. becoming per-page instead of per-collection) and update the validator if so.
Defensive patterns

Strategy: retry

Try / catch

async function fetchEcccWithRetry(opts, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fetchEcccAlertFeatures(opts);
    } catch (err) {
      if (!String(err.message).includes('ECCC_COUNT_DRIFT') || i === attempts - 1) throw err;
      await new Promise((r) => setTimeout(r, 1000 * (i + 1)));
    }
  }
}

Prevention

When it happens

Trigger: A second or later page in scripts/_weather-alert-select.mjs:777 returns a data.numberMatched value different from the value captured on the first page of the same status — i.e. alerts were created, resolved, or expired between page requests while the sequential paging loop was running.

Common situations: Fetching during rapidly evolving severe weather where warnings are issued/expired every few seconds; slow fetchFn (network latency) widening the window for the live collection to mutate; an upstream API bug where numberMatched fluctuates or is computed inconsistently per page; load balancers serving different backend snapshots.

Related errors


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15). Data as JSON: /api/errors/255a1370aab60aaf. Report an issue: GitHub.

Appendix: source

Thrown at scripts/_weather-alert-select.mjs:777

    let matched;
    try {
      do {
        if (pages >= ECCC_MAX_PAGES) throw new Error('ECCC_PAGE_LIMIT');
        if (byteBudget.remaining <= 0) throw new Error('ECCC_AGGREGATE_TOO_LARGE');
        const url = new URL(ECCC_ALERTS_URLS[index]);
        url.searchParams.set('offset', String(statusFeatures.length));
        pages += 1;
        const data = await fetchApprovedWeatherJson(url.toString(), {
          allowedHosts: [ECCC_HOST], maxBytes, fetchFn, userAgent, byteBudget,
        });
        const page = requireAlertFeatures(data);
        if (data.type !== 'FeatureCollection'
          || !Number.isSafeInteger(data.numberMatched) || data.numberMatched < 0
          || !Number.isSafeInteger(data.numberReturned) || data.numberReturned !== page.length
          || page.length > ECCC_PAGE_SIZE) {
          throw new Error('ECCC_MALFORMED_PAGE');
        }
        if (matched !== undefined && data.numberMatched !== matched) throw new Error('ECCC_COUNT_DRIFT');
        matched = data.numberMatched;
        if (statusFeatures.length + page.length > matched
          || (page.length === 0 && statusFeatures.length < matched)) {
          throw new Error('ECCC_PAGE_PROGRESS');
        }
        for (const feature of page) {
          if (typeof feature?.id !== 'string' || !feature.id.trim()) throw new Error('ECCC_INVALID_ID');
          if (seenIds.has(feature.id)) throw new Error('ECCC_DUPLICATE_ID');
          seenIds.add(feature.id);
        }
        statusFeatures.push(...page);
      } while (statusFeatures.length < matched);
      features.push(...statusFeatures);
    } catch (err) {
      failures.push(err);
      failedStatuses.push(status);
    }
  }

View on GitHub (pinned to 7d06c8633d)