Ctrl K
ComunidadGuías…

docs/adr/0037-pipeline-run-integrity-at-scale.md

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# ADR-0037: Pipeline Run Integrity at Scale

## Metadata

| Field | Value |
|-------|-------|
| **Status** | Accepted |
| **Date** | 2026-08-04 |
| **Authors** | AlessioBrillo |
| **Deciders** | AlessioBrillo |
| **Supersedes** | N/A |
| **Relates to** | ADR-0003, ADR-0011, ADR-0023, ADR-0035 |
| **Project** | DOMINUS |

## Context

Pipeline runs are executed asynchronously by the `JobWorker` (ADR-0023) and
large runs (hundreds to thousands of candidates) regularly exceed the fixed
per-stage timeout. The failure mode is silent and dangerous: when a stage's
`Promise.race` timeout fires with no results, the whole run completed with an
*empty* `passed` list and no marker — so a user could believe there were no
eligible domains, when in fact the run simply did not finish.

Three compounding defects made this worse:

1. **Fixed, candidate-independent timeout**: a single `stageTimeoutMs`
   (default 5 min per stage) did not scale with input size. A slow-but-finite
   stage on a large batch was indistinguishable from a hung stage.
2. **No partial-result harvesting**: on timeout the stage result was
   discarded wholesale. Candidates already processed were lost even though
   their per-candidate results were valid.
3. **No degradation signal**: nothing on the `PipelineRunResult`, the job
   queue, the API, the CLI, or the dashboard indicated that output was
   incomplete. Callers silently consumed truncated recommendations.

Separately, the RDAP confirmation stage cross-validated availability against
WHOIS port-43. A slow WHOIS server (or a block-prone ccTLD registry) could
hang the cross-validation for every candidate — one slow external dependency
capped the throughput and reliability of the whole stage, even after ADR-0035
made RDAP authoritative.

## Decision

Fully time-box every pipeline stage; harvest partial results on expiry; and
surface degraded output end-to-end so it is never mistaken for a clean "no
recommendations" run.

### Stage budget (orchestrator)

Replace the fixed `stageTimeoutMs` with a candidate-scaled budget
(`src/pipeline/orchestrator.ts`):

- `budgetMs = min(baseMs + perCandidateMs × inputCount, capMs)`.
  Defaults: base 30 s, 200 ms per candidate, cap 1 h, all configurable
  (`STAGE_TIMEOUT_BASE_MS`, `STAGE_TIMEOUT_PER_CANDIDATE_MS`,
  `STAGE_TIMEOUT_CAP_MS`, `STAGE_TIMEOUT_GRACE_MS`).
- When the budget expires, the stage's AbortController is aborted and a
  **grace window** (default 5 s) begins. If the stage resolves within grace
  (it propagates the abort signal to its providers), the *partial* result is
  harvested and the stage is marked `timedOut` for the purposes of
  accounting. If grace also elapses, the stage returns an empty result
  immediately.
- Every budget expiry (or stage error with no partial output) pushes a
  `StageDegradation` entry: `{ stageName, reason: 'timeout' | 'error',
  processedCount, expectedCount, message? }`.

### Degradation surfaced end-to-end

- `PipelineRunResult` gains `degraded: boolean` and `degradedReasons:
  StageDegradation[]`. `degraded = true` when any stage degraded *or* any
  stage returned an error (`stageErrors`), i.e. output may be incomplete.
- The job queue result (`PipelineRunResult`), the runs repository
  (`PipelineRunResults`), and `buildResultsSummary` in the run service carry
  `degraded` / `degradedReasons` through to persistence and the API
  (`POST /api/v1/runs` and run status responses).
- The CLI (`dominus run`) prints one warning per degradation with
  `processed X/Y candidates` and exits **non-zero** on a degraded run — in
  both synchronous mode and async polling — so scripts and automation cannot
  mistake the output for a clean verdict.
- The dashboard `Runs` page shows a banner for degraded runs listing each
  stage, reason, and processed/expected counts.

### WHOIS budget (RDAP confirmation)

`RdapConfirmationStage` gains a per-domain WHOIS budget
(`RDAP_WHOIS_BUDGET_MS`, default 1000 ms, min 50, max 5000;
`whoisBudgetMs` constructor parameter). The WHOIS cross-check races against
`AbortSignal.timeout(budget)` and is ignored when it loses: RDAP remains
authoritative (ADR-0035). A disagreement still blocks the candidate — but
only when WHOIS answers within budget, keeping the cross-validation
conservative without making run throughput hostage to slow WHOIS servers.

## Consequences

### Positive

- Large runs no longer complete silently empty: either the stage finishes
  (budget scales with candidate count) or the run completes marked
  `degraded` with an explicit reason and non-zero CLI exit.
- Partial results from budget-expired stages are preserved and counted
  (`processedCount`), so a timeout loses only the unprocessed tail, not the
  work already done.
- Degraded output is visible in the API response, persisted run record,
  CLI exit code, and dashboard banner — "no recommendations" is never
  conflated with "run timed out".
- The WHOIS cross-validation can no longer stall or cap the RDAP stage;
  throughput is bounded by the authoritative path.

### Negative

- Budget is a heuristic: a finite-but-brute-slow stage may still be aborted
  if it does not respect the abort signal and exceeds grace. Operators can
  raise `STAGE_TIMEOUT_*` or `RDAP_WHOIS_BUDGET_MS` for pathological inputs.
- Stage-level accounting (`processedCount`) is derived from returned
  `passed`/`filtered` counts; stages that discard results internally appear
  with a lower `processedCount` than actually processed.
- New config surface adds operational tuning surface (mitigated by sane
  defaults and documented in `.env.example`).

### Risks and mitigation

- **Abort coherence**: harvest relies on providers honouring the abort
  signal. `TrademarkGateStage` and the DNS/RDAP stages already propagate the
  signal; the grace window absorbs non-cooperative providers up to a bound.
- **False degradation on healthy runs**: a stage that emits a recoverable
  per-candidate error and no stage-level error is not degraded; only stage
  failures that truncate output mark the run. Errors already surfaced via
  `stageErrors` are now also reflected in `degraded`.
- **Exit-code breakage**: scripts that relied on `dominus run` returning 0
  for a timed-out run will now see 1. This is the intended loud-failure
  behaviour; quiet success was the original bug.