@checkstack/healthcheck-backend 1.19.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +190 -0
- package/drizzle/0019_chemical_frightful_four.sql +8 -0
- package/drizzle/0020_certain_mordo.sql +2 -0
- package/drizzle/meta/0019_snapshot.json +661 -0
- package/drizzle/meta/0020_snapshot.json +711 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +21 -21
- package/src/ai/system-signals-contributor.test.ts +33 -9
- package/src/ai/system-signals-contributor.ts +38 -16
- package/src/cache-test-stub.ts +26 -0
- package/src/cache.test.ts +291 -0
- package/src/cache.ts +204 -34
- package/src/health-notification-content.test.ts +22 -0
- package/src/health-notification-content.ts +7 -0
- package/src/healthcheck-gitops-kinds.test.ts +14 -0
- package/src/healthcheck-gitops-kinds.ts +27 -0
- package/src/index.ts +31 -12
- package/src/queue-executor.test.ts +13 -26
- package/src/queue-executor.ts +94 -44
- package/src/retention-job.ts +8 -0
- package/src/rollup-consumer.test.ts +19 -8
- package/src/router-config-secrets.test.ts +2 -7
- package/src/router-create-and-assign.test.ts +2 -7
- package/src/router-pause-recompute.test.ts +2 -7
- package/src/router.test.ts +3 -8
- package/src/router.ts +7 -15
- package/src/schema.ts +74 -31
- package/src/service.ts +45 -102
- package/src/status-fingerprint.test.ts +92 -0
- package/src/status-fingerprint.ts +66 -0
- package/src/status-page/widgets.test.ts +84 -0
- package/src/status-page/widgets.ts +87 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,195 @@
|
|
|
1
1
|
# @checkstack/healthcheck-backend
|
|
2
2
|
|
|
3
|
+
## 1.20.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- bd41130: perf(healthcheck): stop recomputing the full system rollup on every check run
|
|
8
|
+
|
|
9
|
+
The queue run executor captured the system-wide rollup health
|
|
10
|
+
(`getSystemHealthStatus(systemId)`) at the start of EVERY check tick - a
|
|
11
|
+
worst-wins aggregate that fans out an N+1 of windowed `health_check_runs` reads
|
|
12
|
+
across every check × environment of the system. That value was only ever
|
|
13
|
+
consumed on the rare catastrophic-failure path (a job that throws before running
|
|
14
|
+
any probe); the normal success/failure paths record their transition from the
|
|
15
|
+
per-environment pre-read and never touched it. Under load this was one of the
|
|
16
|
+
heaviest repeated reads on the hot path.
|
|
17
|
+
|
|
18
|
+
The rollup pre-status is now computed lazily, only inside the catastrophic-
|
|
19
|
+
failure branch that actually uses it. Behavior is unchanged - the catastrophic
|
|
20
|
+
path reads the same pre-tick rollup (it is reached only when the run threw before
|
|
21
|
+
inserting anything, so nothing changed in between) - but every normal check tick
|
|
22
|
+
no longer pays for a full rollup recompute it discards.
|
|
23
|
+
|
|
24
|
+
- bd41130: perf(healthcheck): add system-leading aggregate and config-reverse indexes
|
|
25
|
+
|
|
26
|
+
Add two Postgres indexes (migration 0020) to serve reads that the existing
|
|
27
|
+
keys cannot cover:
|
|
28
|
+
|
|
29
|
+
- `health_check_aggregates_system_bucket_idx` on
|
|
30
|
+
`(system_id, bucket_size, bucket_start)`. The health-state read omits
|
|
31
|
+
`configuration_id`, so the leading-`configuration_id` unique index could
|
|
32
|
+
not be used and the query scanned the aggregates table. This index leads
|
|
33
|
+
with `system_id` so those reads use an index instead.
|
|
34
|
+
- `system_health_checks_config_enabled_idx` on `(configuration_id, enabled)`.
|
|
35
|
+
The reverse lookup in `getSystemIdsForConfiguration` (config-change
|
|
36
|
+
recompute) filters by `configuration_id`, but the primary key leads with
|
|
37
|
+
`system_id` and could not serve it. This index makes the config-scoped
|
|
38
|
+
lookup an index scan.
|
|
39
|
+
|
|
40
|
+
- bd41130: perf(healthcheck): add the missing composite indexes on health_check_runs
|
|
41
|
+
|
|
42
|
+
The status read path reads the last N runs for a (system, check[, environment])
|
|
43
|
+
slice ordered by `timestamp DESC` on every status read AND on every check
|
|
44
|
+
execution, but `health_check_runs` had NO secondary indexes - only its primary
|
|
45
|
+
key. Every such read was a full sequential scan of the (multi-million-row) table
|
|
46
|
+
plus an in-memory sort, so point reads averaged 50-320 ms and dominated total DB
|
|
47
|
+
time. Two composite indexes now back these access patterns:
|
|
48
|
+
|
|
49
|
+
- `health_check_runs_check_recent_idx` (system_id, configuration_id, timestamp) -
|
|
50
|
+
the cross-environment newest-run reads and the retention `DELETE`.
|
|
51
|
+
- `health_check_runs_slice_recent_idx` (system_id, configuration_id,
|
|
52
|
+
environment_id, timestamp) - the env-scoped slice reads, the per-check
|
|
53
|
+
DISTINCT-environment discovery, and the per-env last-healthy `max(timestamp)`
|
|
54
|
+
group-by.
|
|
55
|
+
|
|
56
|
+
Both turn full-table seq-scans into index range scans (Postgres scans the btree
|
|
57
|
+
backward for the `DESC` order).
|
|
58
|
+
|
|
59
|
+
> [!IMPORTANT]
|
|
60
|
+
> Deploy note: the migration builds the indexes with a plain (non-CONCURRENT)
|
|
61
|
+
> `CREATE INDEX`, which briefly locks writes to `health_check_runs` while each
|
|
62
|
+
> index builds (the migrator runs every migration in one transaction, so
|
|
63
|
+
> `CREATE INDEX CONCURRENTLY` is not possible through it). On a very large table
|
|
64
|
+
> you can build them `CONCURRENTLY` by hand (same names) before deploying; the
|
|
65
|
+
> migration uses `IF NOT EXISTS`, so it then no-ops.
|
|
66
|
+
|
|
67
|
+
- bd41130: perf(healthcheck): cache system health status on the shared distributed cache with per-check-vector invalidation
|
|
68
|
+
|
|
69
|
+
The per-system derived health status (`getSystemHealthStatus`) is an N+1 over
|
|
70
|
+
`health_check_runs` across every check × environment, and it backs the highest
|
|
71
|
+
call-count read paths: the dashboard badges, the bulk status endpoint, the
|
|
72
|
+
per-(system, check, environment) matrix the dependency map and status-page
|
|
73
|
+
widgets consume, and the AI system-signals scan. It was only cached for the
|
|
74
|
+
single/bulk rollup, was invalidated UNCONDITIONALLY on every check run (so a
|
|
75
|
+
steady-state healthy system evicted its own cache every tick), the matrix
|
|
76
|
+
endpoint was not cached at all, and the AI signals scan bypassed the cache with
|
|
77
|
+
its own uncached N+1.
|
|
78
|
+
|
|
79
|
+
All four reads now go through a single `HealthCheckCache` facade - built on the
|
|
80
|
+
**platform `CacheManager`** - that is the ONE sanctioned reader AND invalidator
|
|
81
|
+
of a system's status:
|
|
82
|
+
|
|
83
|
+
- **Reads** (`read` / `readBulk` / `readMatrix`) are served read-through, keyed
|
|
84
|
+
per `(system, environment)`, holding the RAW (pre-incident-override) status;
|
|
85
|
+
the router folds incident overrides downstream, so an incident change never
|
|
86
|
+
touches this cache. The matrix reuses the same per-environment entries the
|
|
87
|
+
badge path warms. The AI signals contributor now scans candidate systems from
|
|
88
|
+
the durable table and resolves their statuses through `readBulk`, reusing the
|
|
89
|
+
warm cache instead of a fresh N+1.
|
|
90
|
+
- **Invalidation is change-gated on the per-check status VECTOR**, not the run:
|
|
91
|
+
`reconcile(previous, next)` evicts only when a check actually flipped status
|
|
92
|
+
(or its slice-failure composition changed) - a `statusFingerprint` invariant
|
|
93
|
+
to the volatile `evaluatedAt` / `lastRunAt` / `runsConsidered`. A run that
|
|
94
|
+
leaves the vector unchanged keeps the cache warm. This also catches a per-check
|
|
95
|
+
flip that leaves the rollup enum unchanged (which the reactive `health` entity
|
|
96
|
+
view would miss). A per-environment run that changes its slice evicts BOTH its
|
|
97
|
+
env key AND the system rollup key (the slice feeds the worst-wins rollup), so a
|
|
98
|
+
simultaneous slice swap - one env recovering as another fails, which the
|
|
99
|
+
rollup's own fingerprint is blind to - still refreshes the rollup. Sibling
|
|
100
|
+
environment keys stay warm.
|
|
101
|
+
|
|
102
|
+
Cross-pod coherence comes from the SHARED cache backend, not from an application
|
|
103
|
+
broadcast: with a distributed provider (Redis) an eviction is a `delete` every
|
|
104
|
+
pod sees immediately. On the default in-memory backend the cache is per-pod and
|
|
105
|
+
therefore single-instance-only (the Infrastructure Cache UI now warns about
|
|
106
|
+
this). The cached value is a derivation of the shared `health_check_runs` tables,
|
|
107
|
+
so a miss recomputes the same answer on every pod; the 15s TTL is only a
|
|
108
|
+
natural-refresh safety net.
|
|
109
|
+
|
|
110
|
+
Enforced by design, not convention:
|
|
111
|
+
|
|
112
|
+
- Every status-mutating writer invalidates through the facade: the run executor,
|
|
113
|
+
the router config/assignment/satellite handlers, the system/satellite lifecycle
|
|
114
|
+
hooks, AND the GitOps apply path (create/update/delete/associate/disassociate),
|
|
115
|
+
which writes configs directly on the service rather than through the router and
|
|
116
|
+
would otherwise have stranded a stale status until the TTL.
|
|
117
|
+
- A `checkstack/no-direct-system-status-read` lint rule (error) forbids raw
|
|
118
|
+
`service.getSystemHealthStatus(...)` reads anywhere except the cache facade and
|
|
119
|
+
the executor / entity-compute paths that must read live to detect a transition.
|
|
120
|
+
- A `checkstack/no-direct-health-run-insert` lint rule (error) forbids raw
|
|
121
|
+
`insert(healthCheckRuns)` outside the executor / service run writers.
|
|
122
|
+
|
|
123
|
+
The executor's per-run change-gate reads its pre-run baseline INSIDE the
|
|
124
|
+
per-(system, environment) advisory-lock critical section (not before the probe),
|
|
125
|
+
so a concurrent same-slice run cannot commit between the baseline read and the
|
|
126
|
+
insert and cause the gate to miss a real transition.
|
|
127
|
+
|
|
128
|
+
Behavior is unchanged for readers (same values, strictly fresher than the prior
|
|
129
|
+
15s-stale-on-quiet-systems behavior). The `getSystemHealthStatus` /
|
|
130
|
+
`getBulkSystemHealthStatus` / `getBulkSystemHealthMatrix` RPC contracts are
|
|
131
|
+
untouched, so cross-plugin callers (dependency, SLO, status-page) need no change.
|
|
132
|
+
|
|
133
|
+
- bd41130: fix(status-page): scope email subscriptions to published environments and author-selected systems
|
|
134
|
+
|
|
135
|
+
Two correctness fixes to status-page email subscriptions:
|
|
136
|
+
|
|
137
|
+
- **Health notifications now respect the page's published environments.** A
|
|
138
|
+
per-environment health transition carries the environment it happened in
|
|
139
|
+
(`originEnvironmentId`, threaded through `notifyForSubscription` ->
|
|
140
|
+
`NotificationAudienceEvent` -> the status-page fan-out). A page that publishes
|
|
141
|
+
a specific environment set is now skipped for a change in an environment it
|
|
142
|
+
does not publish - so a `development` failure never emails a prod-only page's
|
|
143
|
+
subscribers, even for a system that is also shown in prod. Pages publishing all
|
|
144
|
+
environments, and env-less sources (incident, maintenance, whole-system health
|
|
145
|
+
rollup), are unaffected.
|
|
146
|
+
- **Notifications are scoped per category to the widgets the author placed.** The
|
|
147
|
+
send-time fan-out now surfaces a notification only through widgets of its own
|
|
148
|
+
category: a health status change reaches a page only through a HEALTH widget
|
|
149
|
+
(`banner` / `systemHealth` / `groupStatus` / `uptime`, which now implement
|
|
150
|
+
`resolveScopedSystems` and declare `subscriptionCategory: "health"`), an
|
|
151
|
+
incident only through an incident widget, and so on. A page that lists a
|
|
152
|
+
system's incidents but never its health no longer emails health subscribers
|
|
153
|
+
about it, and a health-only page now correctly surfaces its systems for
|
|
154
|
+
subscription. Health widgets also participate in the public subscribe picker.
|
|
155
|
+
|
|
156
|
+
BREAKING CHANGE: on a page publishing a specific environment set, health
|
|
157
|
+
subscribers now only receive changes that occurred in a published environment
|
|
158
|
+
(previously any environment of a surfaced system triggered a notification), and a
|
|
159
|
+
notification is surfaced only by a widget of its own category (previously any
|
|
160
|
+
scoping widget on the page could surface any category). Legacy subscribers (NULL
|
|
161
|
+
categories) and all-environment pages are unchanged; no data migration is needed.
|
|
162
|
+
|
|
163
|
+
### Patch Changes
|
|
164
|
+
|
|
165
|
+
- Updated dependencies [bd41130]
|
|
166
|
+
- Updated dependencies [bd41130]
|
|
167
|
+
- Updated dependencies [bd41130]
|
|
168
|
+
- Updated dependencies [bd41130]
|
|
169
|
+
- Updated dependencies [bd41130]
|
|
170
|
+
- Updated dependencies [bd41130]
|
|
171
|
+
- Updated dependencies [bd41130]
|
|
172
|
+
- Updated dependencies [bd41130]
|
|
173
|
+
- @checkstack/backend-api@0.32.0
|
|
174
|
+
- @checkstack/cache-utils@0.3.0
|
|
175
|
+
- @checkstack/catalog-backend@1.8.0
|
|
176
|
+
- @checkstack/ai-backend@0.10.11
|
|
177
|
+
- @checkstack/incident-backend@1.13.0
|
|
178
|
+
- @checkstack/notification-common@1.7.0
|
|
179
|
+
- @checkstack/status-page-backend@0.6.0
|
|
180
|
+
- @checkstack/automation-backend@0.11.2
|
|
181
|
+
- @checkstack/command-backend@0.2.23
|
|
182
|
+
- @checkstack/gitops-backend@0.5.23
|
|
183
|
+
- @checkstack/satellite-backend@0.8.5
|
|
184
|
+
- @checkstack/script-packages-backend@0.4.2
|
|
185
|
+
- @checkstack/secrets-backend@0.3.5
|
|
186
|
+
- @checkstack/catalog-common@2.7.1
|
|
187
|
+
- @checkstack/sdk@0.128.1
|
|
188
|
+
- @checkstack/healthcheck-common@1.16.1
|
|
189
|
+
- @checkstack/incident-common@1.10.1
|
|
190
|
+
- @checkstack/maintenance-common@1.10.1
|
|
191
|
+
- @checkstack/status-page-common@0.6.1
|
|
192
|
+
|
|
3
193
|
## 1.19.0
|
|
4
194
|
|
|
5
195
|
### Minor Changes
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- NOTE: plain (non-CONCURRENT) CREATE INDEX takes a SHARE lock that blocks
|
|
2
|
+
-- writes to health_check_runs while each index builds. The migrator wraps every
|
|
3
|
+
-- migration in one transaction, so CREATE INDEX CONCURRENTLY (which cannot run
|
|
4
|
+
-- inside a transaction) is not possible here. On a very large table you can
|
|
5
|
+
-- pre-build these CONCURRENTLY by hand (outside the migrator, same names) before
|
|
6
|
+
-- deploying; the IF NOT EXISTS below then makes this migration a no-op.
|
|
7
|
+
CREATE INDEX IF NOT EXISTS "health_check_runs_check_recent_idx" ON "health_check_runs" USING btree ("system_id","configuration_id","timestamp");--> statement-breakpoint
|
|
8
|
+
CREATE INDEX IF NOT EXISTS "health_check_runs_slice_recent_idx" ON "health_check_runs" USING btree ("system_id","configuration_id","environment_id","timestamp");
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
CREATE INDEX "health_check_aggregates_system_bucket_idx" ON "health_check_aggregates" USING btree ("system_id","bucket_size","bucket_start");--> statement-breakpoint
|
|
2
|
+
CREATE INDEX "system_health_checks_config_enabled_idx" ON "system_health_checks" USING btree ("configuration_id","enabled");
|