@checkstack/healthcheck-backend 1.19.0 → 1.20.1

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