|
| 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