@checkstack/healthcheck-backend 1.17.0 → 1.19.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 +559 -0
- package/package.json +32 -29
- package/src/adaptive-timeout.test.ts +91 -0
- package/src/adaptive-timeout.ts +75 -0
- package/src/ai/system-signals-contributor.test.ts +2 -0
- package/src/automations.test.ts +47 -0
- package/src/automations.ts +19 -3
- package/src/health-notification-content.test.ts +89 -0
- package/src/health-notification-content.ts +138 -0
- package/src/healthcheck-gitops-kinds.test.ts +34 -2
- package/src/healthcheck-gitops-kinds.ts +17 -13
- package/src/index.ts +58 -6
- package/src/migration-chain-contract.test.ts +7 -1
- package/src/notification-policy.test.ts +19 -0
- package/src/notification-policy.ts +26 -0
- package/src/queue-executor.test.ts +391 -338
- package/src/queue-executor.ts +426 -362
- package/src/realtime-aggregation.ts +9 -2
- package/src/rollup-consumer.test.ts +191 -0
- package/src/rollup-consumer.ts +160 -0
- package/src/router.ts +46 -13
- package/src/schedule-jitter.test.ts +69 -0
- package/src/schedule-jitter.ts +50 -0
- package/src/schedule-reconciler.it.test.ts +453 -0
- package/src/schedule-reconciler.test.ts +418 -0
- package/src/schedule-reconciler.ts +304 -0
- package/src/service-batching.test.ts +106 -0
- package/src/service-bulk-counts.it.test.ts +144 -0
- package/src/service-bulk-run-stats.it.test.ts +197 -0
- package/src/service-ordering.test.ts +10 -2
- package/src/service-paused-filter.test.ts +27 -7
- package/src/service-rollup-worst-wins.test.ts +221 -124
- package/src/service.ts +557 -266
- package/src/slow-check-admission.test.ts +184 -0
- package/src/slow-check-admission.ts +101 -0
- package/src/slow-check-classifier.test.ts +155 -0
- package/src/slow-check-classifier.ts +137 -0
- package/src/slow-check-config.ts +102 -0
- package/src/status-page/rollup.test.ts +40 -0
- package/src/status-page/rollup.ts +27 -0
- package/src/status-page/widgets.test.ts +303 -0
- package/src/status-page/widgets.ts +155 -39
- package/src/suspect-lane.test.ts +50 -0
- package/src/suspect-lane.ts +61 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { SuspectLane } from "./suspect-lane";
|
|
3
|
+
|
|
4
|
+
describe("SuspectLane", () => {
|
|
5
|
+
test("admits up to capacity, then denies with lane_full", () => {
|
|
6
|
+
const lane = new SuspectLane(2);
|
|
7
|
+
expect(lane.tryAdmit("a")).toEqual({ admitted: true });
|
|
8
|
+
expect(lane.tryAdmit("b")).toEqual({ admitted: true });
|
|
9
|
+
expect(lane.active).toBe(2);
|
|
10
|
+
expect(lane.tryAdmit("c")).toEqual({ admitted: false, reason: "lane_full" });
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("single-flight: a second admit of the same key is denied with in_flight", () => {
|
|
14
|
+
const lane = new SuspectLane(4);
|
|
15
|
+
expect(lane.tryAdmit("k")).toEqual({ admitted: true });
|
|
16
|
+
expect(lane.tryAdmit("k")).toEqual({ admitted: false, reason: "in_flight" });
|
|
17
|
+
expect(lane.active).toBe(1); // the duplicate did not consume a slot
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("in_flight is checked before capacity", () => {
|
|
21
|
+
const lane = new SuspectLane(1);
|
|
22
|
+
expect(lane.tryAdmit("k")).toEqual({ admitted: true });
|
|
23
|
+
// lane is full AND k is in-flight; in_flight wins (more specific reason).
|
|
24
|
+
expect(lane.tryAdmit("k")).toEqual({ admitted: false, reason: "in_flight" });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("release frees the slot and clears single-flight", () => {
|
|
28
|
+
const lane = new SuspectLane(1);
|
|
29
|
+
expect(lane.tryAdmit("k")).toEqual({ admitted: true });
|
|
30
|
+
expect(lane.tryAdmit("k")).toEqual({ admitted: false, reason: "in_flight" });
|
|
31
|
+
lane.release("k");
|
|
32
|
+
expect(lane.active).toBe(0);
|
|
33
|
+
expect(lane.tryAdmit("k")).toEqual({ admitted: true }); // re-admittable
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("release is idempotent for unknown/duplicate keys", () => {
|
|
37
|
+
const lane = new SuspectLane(2);
|
|
38
|
+
lane.tryAdmit("a");
|
|
39
|
+
lane.release("a");
|
|
40
|
+
lane.release("a"); // no-op
|
|
41
|
+
lane.release("never-admitted"); // no-op
|
|
42
|
+
expect(lane.active).toBe(0);
|
|
43
|
+
expect(lane.tryAdmit("a")).toEqual({ admitted: true });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("rejects a non-positive capacity", () => {
|
|
47
|
+
expect(() => new SuspectLane(0)).toThrow();
|
|
48
|
+
expect(() => new SuspectLane(-1)).toThrow();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pod-local bulkhead for slot-hogging ("suspect") health-check jobs.
|
|
3
|
+
*
|
|
4
|
+
* A correlated outage can turn hundreds of checks into slot-hoggers at once. The
|
|
5
|
+
* work-conserving bulkhead caps how many suspect jobs may HOLD a worker slot
|
|
6
|
+
* concurrently on this pod, so healthy checks keep draining. It is deliberately
|
|
7
|
+
* NON-BLOCKING: a suspect job that cannot be admitted is skipped for this tick
|
|
8
|
+
* (freeing its worker slot immediately) rather than queued behind the lane -
|
|
9
|
+
* queueing would just move the starvation back into the shared worker pool.
|
|
10
|
+
*
|
|
11
|
+
* Two admission gates, combined:
|
|
12
|
+
* - single-flight: at most ONE in-flight run per `(configId, systemId)` job
|
|
13
|
+
* key, so a recurring fire that arrives while the previous slow run is still
|
|
14
|
+
* executing is dropped instead of stacking up (no buildup).
|
|
15
|
+
* - capacity: at most `capacity` suspect jobs holding a slot at once.
|
|
16
|
+
*
|
|
17
|
+
* This is pod-local infrastructure, exactly like the queue's own concurrency
|
|
18
|
+
* semaphore: total suspect concurrency across the cluster is `capacity x pods`,
|
|
19
|
+
* scaling the same way queue concurrency already does. The CLASSIFICATION that
|
|
20
|
+
* decides which jobs are suspect derives from durable run history (globally
|
|
21
|
+
* consistent); only this admission bookkeeping is per-pod.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export type AdmissionDenial = "in_flight" | "lane_full";
|
|
25
|
+
|
|
26
|
+
export type AdmissionResult =
|
|
27
|
+
| { admitted: true }
|
|
28
|
+
| { admitted: false; reason: AdmissionDenial };
|
|
29
|
+
|
|
30
|
+
export class SuspectLane {
|
|
31
|
+
private held = 0;
|
|
32
|
+
private readonly inFlight = new Set<string>();
|
|
33
|
+
|
|
34
|
+
constructor(private readonly capacity: number) {
|
|
35
|
+
if (!Number.isInteger(capacity) || capacity < 1) {
|
|
36
|
+
throw new Error(`SuspectLane capacity must be a positive integer, got ${capacity}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Try to admit a suspect job. On `admitted: true` the caller MUST call
|
|
42
|
+
* `release(key)` exactly once (in a `finally`) when the run completes.
|
|
43
|
+
*/
|
|
44
|
+
tryAdmit(key: string): AdmissionResult {
|
|
45
|
+
if (this.inFlight.has(key)) return { admitted: false, reason: "in_flight" };
|
|
46
|
+
if (this.held >= this.capacity) return { admitted: false, reason: "lane_full" };
|
|
47
|
+
this.held++;
|
|
48
|
+
this.inFlight.add(key);
|
|
49
|
+
return { admitted: true };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Release a previously-admitted key. Idempotent for an unknown key. */
|
|
53
|
+
release(key: string): void {
|
|
54
|
+
if (this.inFlight.delete(key)) this.held--;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Number of suspect jobs currently holding a slot (for metrics/tests). */
|
|
58
|
+
get active(): number {
|
|
59
|
+
return this.held;
|
|
60
|
+
}
|
|
61
|
+
}
|