Skip to content

Commit 222d06f

Browse files
Elon Muskclaude
andauthored
fix(runtime): scope the background drift check to the datasource it was armed for (#10961) (#11167)
scheduleDriftChecks arms one setInterval per datasource, and every tick called validateAll() — every federated object on every federated datasource, each validation driving a live introspect(datasource) — then kept only the rows matching the armed name. The events were right; the work was the whole farm, unattended, on every interval, forever. Measured at the branch point against the real ExternalDatasourceService over a recording introspector: one tick armed for wh_a introspected ['wh_a','wh_b','wh_c']; two armed timers introspected six remotes per cycle; three ticks of one timer introspected nine. Each tick now calls the scoped validateDatasource(datasource) added for #10537. It is deliberately not on IExternalDatasourceService, so the plugin probes for it — and a registered service without it is declined, never served by the fan-out. Declined QUIETLY: unlike the REST route's 503, a background timer has no caller waiting, so the honest degradation is to skip the work and log the reason once per tick (warn — functional, not durability). Events, payloads and the never-throws contract are unchanged. Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 338044f commit 222d06f

4 files changed

Lines changed: 542 additions & 25 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
"@objectstack/runtime": minor
3+
---
4+
5+
fix(runtime): the background drift check introspects the datasource it was armed for (#10961)
6+
7+
`ExternalValidationPlugin.scheduleDriftChecks` arms one `setInterval` **per
8+
datasource** — one timer for each datasource declaring
9+
`external.validation.checkIntervalMs`. Every tick asked the `external-datasource`
10+
service for `validateAll()` — every federated object on every federated
11+
datasource, each validation driving a live `introspect(datasource)` remote-schema
12+
read — and then kept only the rows whose `datasource` matched the one that timer
13+
was armed for. The emitted events were right; the **work** was the whole farm.
14+
15+
This is the periodic twin of the request-gate defect fixed in
16+
`POST /datasources/:name/external/validate`, and worse in the one way that
17+
matters: a request gate has a caller waiting on the answer and watching the
18+
latency, while this is **unattended**. The fan-out repeated on every interval of
19+
every armed datasource, forever, with nobody reading the rows it discarded.
20+
21+
Measured at the branch point against the real `ExternalDatasourceService` over a
22+
recording introspector, on a fixture with three federated datasources: a single
23+
tick armed for `wh_a` introspected `['wh_a', 'wh_b', 'wh_c']`; two armed timers
24+
introspected **six** remotes in one cycle where two were asked for; three ticks
25+
of one timer introspected nine. A timer armed for a name nothing is bound to
26+
still dialled all three, to produce the empty result it already had.
27+
28+
Each tick now calls the scoped `validateDatasource(datasource)` — the twin
29+
composed service-side from the same primitives (`listObjects` → filter →
30+
`validateObject`) with the same federation predicate, so it returns row-for-row
31+
what the post-filter kept. **No change to what is emitted**: the same
32+
`external.schema.drift` event per drifted object, the same payload, the same
33+
`onMismatch` policy handling, and the same "never throws" contract for a
34+
fire-and-forget timer. Same tick, `['wh_a']`; two armed timers, two remotes; an
35+
unbound name, none.
36+
37+
Unchanged: `validateAll()` itself, and the **boot** gate
38+
(`ExternalValidationPlugin.runValidation`), whose subject genuinely is every
39+
federated object in the environment.
40+
41+
## What you may newly see, and why this is a `minor`
42+
43+
`validateDatasource` is deliberately **not** on `IExternalDatasourceService`
44+
the contract offers `validateObject(objectName)` and `validateAll()`, and adding
45+
a per-datasource spelling to it is a spec-surface decision to take on its own
46+
terms. So the plugin **probes** for the scoped spelling, and a registered
47+
service that does not have it is **declined** rather than served by the fan-out:
48+
a silent fallback would leave the old unattended sweep reachable on a path no
49+
test drives, in exactly the deployments nobody is looking at.
50+
51+
If you register your own `external-datasource` service — a
52+
contract-conforming implementation has no reason to carry `validateDatasource`
53+
today — background drift checking for that host now stops, where it previously
54+
worked via the sweep. That narrowing, not the scoping fix, is what earns a
55+
`minor`: it is visible to embedders and it is quiet by design.
56+
57+
**Quiet, deliberately.** The REST route answers `503` for the same absence; a
58+
background timer must not. There is no caller waiting on this check, so "loud"
59+
would mean spraying errors at nobody or manufacturing 5xx noise from a check
60+
nothing requested. The honest degradation for an unattended checker is to not do
61+
the thing and record why: no events, no throw, one `warn` naming the consequence
62+
(drift on that datasource is not being watched) and the fix — register a service
63+
that can validate a single datasource, or drop `external.validation.checkIntervalMs`
64+
so nothing is armed. `warn`, not `error`: nothing claimed to have been persisted,
65+
so this is a functional degradation, not a durability one.
66+
67+
The probe re-runs on every tick and its verdict is cached nowhere, so a service
68+
registered after the timers were armed starts being checked on the next interval.

0 commit comments

Comments
 (0)