@camstack/system 1.2.445 → 1.2.447
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/dist/{agent-https-CiOW0-sj.js → agent-https-5DYAOJMO.js} +2 -2
- package/dist/{agent-https-D4Q8iFNd.mjs → agent-https-BWUEf6ev.mjs} +2 -2
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +86 -6
- package/dist/builtins/alerts/alerts.addon.mjs +86 -6
- package/dist/builtins/alerts/critical-push.d.ts +4 -0
- package/dist/builtins/alerts/emit-alert-policy.d.ts +28 -0
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +2 -2
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +2 -2
- package/dist/builtins/calendars/calendars.addon.js +1 -1
- package/dist/builtins/calendars/calendars.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/composer/composer.addon.js +2 -2
- package/dist/builtins/composer/composer.addon.mjs +2 -2
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
- package/dist/builtins/device-manager/device-manager.addon.js +1 -1
- package/dist/builtins/device-manager/device-manager.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/keychain/index.js +4 -4
- package/dist/builtins/keychain/index.mjs +4 -4
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +3 -3
- package/dist/builtins/local-auth/local-auth.addon.mjs +3 -3
- package/dist/builtins/local-network/local-network.addon.js +2 -2
- package/dist/builtins/local-network/local-network.addon.mjs +2 -2
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/container-memory-ring.d.ts +18 -0
- package/dist/builtins/native-metrics/load-series-config.d.ts +38 -1
- package/dist/builtins/native-metrics/load-series-reader.d.ts +3 -3
- package/dist/builtins/native-metrics/load-series-types.d.ts +14 -0
- package/dist/builtins/native-metrics/native-metrics.addon.d.ts +5 -7
- package/dist/builtins/native-metrics/native-metrics.addon.js +220 -974
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +220 -974
- package/dist/builtins/native-metrics/node-load-ring.d.ts +3 -5
- package/dist/builtins/native-metrics/node-metrics-history-recorder.d.ts +70 -0
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/record-entries/index.js +1 -1
- package/dist/builtins/record-entries/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/index.js +1 -1
- package/dist/builtins/sqlite-storage/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +2 -2
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +2 -2
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +3 -3
- package/dist/builtins/system-config/system-config.addon.mjs +3 -3
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{composition-sources-BMt5cFYb.mjs → composition-sources-BmC5jAUv.mjs} +1 -1
- package/dist/{composition-sources-D3fi9Xns.js → composition-sources-BwIIltnZ.js} +1 -1
- package/dist/{constants-BMHCxRzb.mjs → constants-6XHGmEUF.mjs} +1 -1
- package/dist/{constants-Br879Axi.js → constants-DA_tjzIk.js} +1 -1
- package/dist/{device-ca-T8osSMos.mjs → device-ca-Bh_ClxMO.mjs} +1 -1
- package/dist/{device-ca-CnJ_va7i.js → device-ca-CnbrgfmW.js} +1 -1
- package/dist/{dist-DgOG8flk.js → dist-BkNfEJKa.js} +292 -11
- package/dist/{dist-BNunRyXT.mjs → dist-CkZIrsAP.mjs} +292 -11
- package/dist/file-host-worker.js +1 -1
- package/dist/file-host-worker.mjs +1 -1
- package/dist/{hub-mdns-ciao-D16oRQ9K.mjs → hub-mdns-ciao-CoyIUWHC.mjs} +2 -2
- package/dist/{hub-mdns-ciao-BIkTHq-n.js → hub-mdns-ciao-m5LIr4oA.js} +2 -2
- package/dist/index.js +9 -8
- package/dist/index.mjs +9 -8
- package/dist/{phase6-declare-gate-tkKOEitw.mjs → phase6-declare-gate-BDWZuB2n.mjs} +1 -1
- package/dist/{phase6-declare-gate-Ci1-8uUn.js → phase6-declare-gate-Bl7KocEs.js} +1 -1
- package/dist/{restore-archive-DjJnCgap.js → restore-archive-B57xI5Ue.js} +1 -1
- package/dist/{restore-archive-DR9MtO7J.mjs → restore-archive-CMr4V7Fi.mjs} +1 -1
- package/dist/{wal-maintenance-CfvgtQ7C.mjs → wal-maintenance-Bqm4zrNA.mjs} +0 -0
- package/dist/{wal-maintenance-CODGqhKY.js → wal-maintenance-CuG1DUca.js} +0 -0
- package/package.json +1 -1
- package/dist/builtins/native-metrics/container-memory-series.d.ts +0 -260
- package/dist/builtins/native-metrics/load-series-store.d.ts +0 -293
package/package.json
CHANGED
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Retention for the container-memory reading — the same two tiers, the same
|
|
3
|
-
* bounds and the same reader as the per-process load series, because it is the
|
|
4
|
-
* same question asked at the other end.
|
|
5
|
-
*
|
|
6
|
-
* ## Why it is retained at all
|
|
7
|
-
*
|
|
8
|
-
* A single hand reading of `memory.stat` says what the container holds RIGHT
|
|
9
|
-
* NOW and nothing about direction. That is exactly the failure this exists to
|
|
10
|
-
* fix: on 2026-08-29 six of the hub's twenty-three gigabytes had no owner among
|
|
11
|
-
* the processes, and there was no way to say whether page cache was steady or
|
|
12
|
-
* climbing — so the difference was read as a leak. A number you cannot trend is
|
|
13
|
-
* a number you will misread.
|
|
14
|
-
*
|
|
15
|
-
* ## Why it is not the same TABLE
|
|
16
|
-
*
|
|
17
|
-
* `metrics:node-load-samples` is one row PER PROCESS per sample. A
|
|
18
|
-
* container-memory reading is one row per NODE per sample — 76x fewer rows on
|
|
19
|
-
* the live fleet. Folding it into that table would mean either repeating the
|
|
20
|
-
* same ten numbers on every process row of a sample, or inventing a synthetic
|
|
21
|
-
* process that is not a process. Both are lies with a storage cost. It is its
|
|
22
|
-
* own collection, keyed the same way, pruned by the same knobs and read in the
|
|
23
|
-
* same call.
|
|
24
|
-
*
|
|
25
|
-
* ## Why it is not its own RING
|
|
26
|
-
*
|
|
27
|
-
* {@link NodeLoadRing} is already a fixed-capacity partitioned ring with
|
|
28
|
-
* per-node capacity, a fleet-wide row budget, an idle-partition sweep and a
|
|
29
|
-
* monotonic idempotence gate — all of it measured and tested. It is generic
|
|
30
|
-
* over its payload and reads only the node id and the timestamp off an
|
|
31
|
-
* envelope. A second ring implementation would be a second set of bounds to
|
|
32
|
-
* get wrong. {@link ContainerMemoryRing} is a naming adapter over it, nothing
|
|
33
|
-
* more: one reading per sample, so one "row" per sample.
|
|
34
|
-
*/
|
|
35
|
-
import type { ContainerMemoryPoint, ContainerMemorySnapshot, IScopedLogger, SettingsStoreClient } from "@camstack/types";
|
|
36
|
-
import type { LoadSeriesRetention } from "./load-series-store.js";
|
|
37
|
-
/**
|
|
38
|
-
* @durable class=audit owner=metrics-native
|
|
39
|
-
* write="one row per NODE per sample, appended by the HUB's `native-metrics`
|
|
40
|
-
* off its subscription to `metrics.node-processes-snapshot` — the same
|
|
41
|
-
* subscription, the same accepted-sample gate and the same timestamps as
|
|
42
|
-
* `metrics:node-load-samples`, so a memory row and the process rows of the
|
|
43
|
-
* same instant share an `atMs`. A node with no cgroup writes NOTHING; it
|
|
44
|
-
* never writes a zeroed row."
|
|
45
|
-
* retention="The operator's `loadSeriesRetentionHours` by age and
|
|
46
|
-
* CONTAINER_MEMORY_MAX_ROWS by count, swept in the same pass as the process
|
|
47
|
-
* table and through the same bounded page. Losing the table loses the cold
|
|
48
|
-
* window only: the ring keeps serving the last hour."
|
|
49
|
-
*/
|
|
50
|
-
export declare const CONTAINER_MEMORY_COLLECTION = "metrics:node-memory-samples";
|
|
51
|
-
/**
|
|
52
|
-
* Per-node ceiling on the HOT ring, in samples.
|
|
53
|
-
*
|
|
54
|
-
* Deliberately the same 180 as the process ring — one hour at the 20 s cadence,
|
|
55
|
-
* six at the 10 s default's own reckoning — because the two series are read
|
|
56
|
-
* together and a memory window shorter than the process window would silently
|
|
57
|
-
* truncate the comparison the pair exists to support.
|
|
58
|
-
*/
|
|
59
|
-
export declare const CONTAINER_MEMORY_SAMPLES_PER_NODE = 180;
|
|
60
|
-
/**
|
|
61
|
-
* Fleet ceiling on the hot ring, in READINGS.
|
|
62
|
-
*
|
|
63
|
-
* One reading per node per sample, so this is simply the per-node ceiling times
|
|
64
|
-
* the partition ceiling the process ring already declares (16). 2 880 readings
|
|
65
|
-
* of ten numbers each is well under a megabyte — the bound exists so a runaway
|
|
66
|
-
* node id space costs a fixed amount, not because the number is large.
|
|
67
|
-
*/
|
|
68
|
-
export declare const CONTAINER_MEMORY_MAX_TOTAL_ROWS: number;
|
|
69
|
-
/**
|
|
70
|
-
* Hard ceiling on the durable table, in ROWS. The guarantee behind the
|
|
71
|
-
* retention intention, exactly as `DEFAULT_MAX_ROWS` is for the process table.
|
|
72
|
-
*
|
|
73
|
-
* | cadence | retention | rows (3 nodes) |
|
|
74
|
-
* | --- | --- | --- |
|
|
75
|
-
* | 10 s | 6 h (default) | 6 480 |
|
|
76
|
-
* | 5 s | 6 h | 12 960 |
|
|
77
|
-
* | 5 s | 72 h, 16 nodes | 829 440 — capped here |
|
|
78
|
-
*
|
|
79
|
-
* 50 000 rows is 7.7x the shipped default. This row is strictly narrower than
|
|
80
|
-
* a `metrics:node-load-samples` row — fewer columns, one index, every value an
|
|
81
|
-
* integer varint — and that one was MEASURED at 87 B/row, so 50 000 rows is
|
|
82
|
-
* **under 4.2 MiB** with the real figure lower still. It is not a knob: a
|
|
83
|
-
* guarantee that moves with the hand that raised the intention is not one.
|
|
84
|
-
*/
|
|
85
|
-
export declare const CONTAINER_MEMORY_MAX_ROWS = 5e4;
|
|
86
|
-
/** Rows one prune pass may remove. Bounded, never a full scan. */
|
|
87
|
-
export declare const CONTAINER_MEMORY_PRUNE_PAGE_ROWS = 2e4;
|
|
88
|
-
export declare const CONTAINER_MEMORY_COLUMNS: readonly [{
|
|
89
|
-
readonly name: "id";
|
|
90
|
-
readonly type: "INTEGER";
|
|
91
|
-
readonly primaryKey: true;
|
|
92
|
-
readonly notNull: true;
|
|
93
|
-
}, {
|
|
94
|
-
readonly name: "nodeId";
|
|
95
|
-
readonly type: "TEXT";
|
|
96
|
-
readonly notNull: true;
|
|
97
|
-
}, {
|
|
98
|
-
readonly name: "atMs";
|
|
99
|
-
readonly type: "INTEGER";
|
|
100
|
-
readonly notNull: true;
|
|
101
|
-
}, {
|
|
102
|
-
readonly name: "source";
|
|
103
|
-
readonly type: "TEXT";
|
|
104
|
-
readonly notNull: true;
|
|
105
|
-
}, {
|
|
106
|
-
readonly name: "currentMib";
|
|
107
|
-
readonly type: "INTEGER";
|
|
108
|
-
readonly notNull: true;
|
|
109
|
-
}, {
|
|
110
|
-
readonly name: "limitMib";
|
|
111
|
-
readonly type: "INTEGER";
|
|
112
|
-
}, {
|
|
113
|
-
readonly name: "anonMib";
|
|
114
|
-
readonly type: "INTEGER";
|
|
115
|
-
}, {
|
|
116
|
-
readonly name: "fileMib";
|
|
117
|
-
readonly type: "INTEGER";
|
|
118
|
-
}, {
|
|
119
|
-
readonly name: "shmemMib";
|
|
120
|
-
readonly type: "INTEGER";
|
|
121
|
-
}, {
|
|
122
|
-
readonly name: "slabMib";
|
|
123
|
-
readonly type: "INTEGER";
|
|
124
|
-
}, {
|
|
125
|
-
readonly name: "gpuShmemMib";
|
|
126
|
-
readonly type: "INTEGER";
|
|
127
|
-
}];
|
|
128
|
-
export declare const CONTAINER_MEMORY_INDEXES: readonly [{
|
|
129
|
-
readonly name: "idx_node_memory_node_at";
|
|
130
|
-
readonly columns: readonly ["nodeId", "atMs"];
|
|
131
|
-
}];
|
|
132
|
-
/** One stored row, in the units the table stores them in. */
|
|
133
|
-
export interface ContainerMemoryRow {
|
|
134
|
-
readonly nodeId: string;
|
|
135
|
-
readonly atMs: number;
|
|
136
|
-
readonly source: string;
|
|
137
|
-
readonly currentMib: number;
|
|
138
|
-
readonly limitMib: number | null;
|
|
139
|
-
readonly anonMib: number | null;
|
|
140
|
-
readonly fileMib: number | null;
|
|
141
|
-
readonly shmemMib: number | null;
|
|
142
|
-
readonly slabMib: number | null;
|
|
143
|
-
readonly gpuShmemMib: number | null;
|
|
144
|
-
}
|
|
145
|
-
/** The ONE encode. */
|
|
146
|
-
export declare function snapshotToRow(nodeId: string, atMs: number, snapshot: ContainerMemorySnapshot): ContainerMemoryRow;
|
|
147
|
-
/**
|
|
148
|
-
* The ONE decode. A row whose `source` is not a hierarchy this build knows is
|
|
149
|
-
* skipped rather than coerced — a stored string that no longer means anything
|
|
150
|
-
* must not be charted as though it did.
|
|
151
|
-
*/
|
|
152
|
-
export declare function rowToPoint(row: ContainerMemoryRow): ContainerMemoryPoint | null;
|
|
153
|
-
/** Structural validation on read. A malformed row is skipped, never charted. */
|
|
154
|
-
export declare function recordToMemoryRow(data: Record<string, unknown>): ContainerMemoryRow | null;
|
|
155
|
-
export interface ContainerMemoryRingOptions {
|
|
156
|
-
readonly samplesPerNode?: number;
|
|
157
|
-
/**
|
|
158
|
-
* Injectable clock, forwarded to the ring's idle-partition sweep. Tests
|
|
159
|
-
* drive eviction without sleeping for an hour; production uses `Date.now`.
|
|
160
|
-
*/
|
|
161
|
-
readonly now?: () => number;
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* The HOT tier — a naming adapter over {@link NodeLoadRing}, not a second ring.
|
|
165
|
-
*
|
|
166
|
-
* One reading per sample, so the ring's "rows" and its samples are the same
|
|
167
|
-
* count and its fleet budget is a sample budget. Every bound, the idle sweep
|
|
168
|
-
* and the monotonic replay gate come from there unchanged.
|
|
169
|
-
*/
|
|
170
|
-
export declare class ContainerMemoryRing {
|
|
171
|
-
private readonly ring;
|
|
172
|
-
constructor(options?: ContainerMemoryRingOptions);
|
|
173
|
-
/** Retain one reading. `false` = a timestamp this node already delivered. */
|
|
174
|
-
record(nodeId: string, atMs: number, snapshot: ContainerMemorySnapshot): boolean;
|
|
175
|
-
/**
|
|
176
|
-
* Read one node's retained readings, oldest-first. `sinceMs` is EXCLUSIVE,
|
|
177
|
-
* matching every other reader in this subsystem.
|
|
178
|
-
*
|
|
179
|
-
* A node nobody has recorded answers EMPTY — which is what "this node has no
|
|
180
|
-
* cgroup" looks like, and it must stay distinguishable from a zero.
|
|
181
|
-
*/
|
|
182
|
-
read(nodeId: string, sinceMs?: number): readonly ContainerMemoryPoint[];
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Merge the two tiers and reduce, oldest-first.
|
|
186
|
-
*
|
|
187
|
-
* Dedupe is on `atMs` with the HOT copy winning, exactly as the process reader
|
|
188
|
-
* does: the ring holds the reading as it arrived, the table holds it after the
|
|
189
|
-
* MiB encode.
|
|
190
|
-
*
|
|
191
|
-
* Reduction keeps, for each bucket, the reading with the largest
|
|
192
|
-
* `currentBytes` — WHOLE, every field from that one sample. A per-field
|
|
193
|
-
* maximum would synthesise a row whose parts never coexisted (an `anon` peak
|
|
194
|
-
* and a `file` peak minutes apart, summed into a total that never happened),
|
|
195
|
-
* and a mean would smear away the spike this series exists to find. `atMs`
|
|
196
|
-
* becomes the bucket START so the points lie on the same x-axis as the folded
|
|
197
|
-
* process series, which is the only reason both travel in one answer.
|
|
198
|
-
*
|
|
199
|
-
* An empty bucket stays ABSENT — never zero, never carried forward.
|
|
200
|
-
*/
|
|
201
|
-
export declare function reduceContainerMemory(points: readonly ContainerMemoryPoint[], bucketMs: number, origin: number): readonly ContainerMemoryPoint[];
|
|
202
|
-
/** Merge cold + hot on `atMs`, hot wins. Exported because dedup is the
|
|
203
|
-
* property worth testing on its own. */
|
|
204
|
-
export declare function mergeContainerMemory(cold: readonly ContainerMemoryPoint[], hot: readonly ContainerMemoryPoint[]): readonly ContainerMemoryPoint[];
|
|
205
|
-
export interface ContainerMemoryStoreDeps {
|
|
206
|
-
readonly store: SettingsStoreClient;
|
|
207
|
-
readonly logger: IScopedLogger;
|
|
208
|
-
readonly now?: () => number;
|
|
209
|
-
readonly prunePageRows?: number;
|
|
210
|
-
}
|
|
211
|
-
/** What one prune pass did. Returned so a test can assert the bound held. */
|
|
212
|
-
export interface ContainerMemoryPruneOutcome {
|
|
213
|
-
readonly deletedByAge: number;
|
|
214
|
-
readonly deletedByCap: number;
|
|
215
|
-
readonly rowsExamined: number;
|
|
216
|
-
readonly capBit: boolean;
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* The COLD tier. Append-only, bounded, and never the authority on anything the
|
|
220
|
-
* hot ring can answer.
|
|
221
|
-
*
|
|
222
|
-
* Every method is best-effort and says so in the log rather than throwing at
|
|
223
|
-
* the emitter: a storage hiccup must cost a gap in a chart, never a dropped
|
|
224
|
-
* snapshot on the bus.
|
|
225
|
-
*/
|
|
226
|
-
export declare class ContainerMemoryStore {
|
|
227
|
-
private declared;
|
|
228
|
-
private readonly store;
|
|
229
|
-
private readonly logger;
|
|
230
|
-
private readonly nowFn;
|
|
231
|
-
private readonly prunePageRows;
|
|
232
|
-
constructor(deps: ContainerMemoryStoreDeps);
|
|
233
|
-
/** Idempotently declare the collection. `false` when the store refused. */
|
|
234
|
-
declare(): Promise<boolean>;
|
|
235
|
-
/** Append ONE reading. */
|
|
236
|
-
append(nodeId: string, atMs: number, snapshot: ContainerMemorySnapshot): Promise<number>;
|
|
237
|
-
/** Read one node's cold readings, oldest-first. `sinceMs` is EXCLUSIVE. */
|
|
238
|
-
read(nodeId: string, sinceMs: number, limitRows: number): Promise<readonly ContainerMemoryPoint[]>;
|
|
239
|
-
/**
|
|
240
|
-
* Enforce BOTH bounds, oldest-first, through a bounded page each.
|
|
241
|
-
*
|
|
242
|
-
* Not rate-limited here: it is driven by the process store's own prune
|
|
243
|
-
* cadence, so it runs at most once a minute by construction and a second
|
|
244
|
-
* timer would be a second thing to get wrong.
|
|
245
|
-
*/
|
|
246
|
-
prune(nodeIds: readonly string[], retention: LoadSeriesRetention): Promise<ContainerMemoryPruneOutcome | null>;
|
|
247
|
-
/** Total rows, or `null` when the store could not answer. */
|
|
248
|
-
count(): Promise<number | null>;
|
|
249
|
-
/**
|
|
250
|
-
* Delete this node's oldest rows, at most one page's worth.
|
|
251
|
-
*
|
|
252
|
-
* `cutoff === null` means "the oldest rows, whatever their age" — the
|
|
253
|
-
* row-cap path, which is bounded by `limitRows` instead. Either way the page
|
|
254
|
-
* is READ FIRST, ordered by the one index, and its last `atMs` becomes the
|
|
255
|
-
* effective cutoff for a single bounded delete. An unbounded `deleteWhere`
|
|
256
|
-
* would be one statement and one stalling transaction the first time a
|
|
257
|
-
* retention is lowered.
|
|
258
|
-
*/
|
|
259
|
-
private deleteOldestPage;
|
|
260
|
-
}
|
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Durable retention for the per-node load series — the COLD window behind the
|
|
3
|
-
* in-memory ring's hot one.
|
|
4
|
-
*
|
|
5
|
-
* ## One table, and what that decides
|
|
6
|
-
*
|
|
7
|
-
* One row per process per sample, in `metrics:node-load-samples`, declared the
|
|
8
|
-
* way every other typed collection in this repo is declared: explicit columns,
|
|
9
|
-
* explicit indexes, read by `where`/`whereBetween`/`orderBy`/`limit`. There is
|
|
10
|
-
* no dimension table, no per-addon pre-aggregate and no bucket tier. The
|
|
11
|
-
* consequence is that the row count is DETERMINISTIC and large, and the honest
|
|
12
|
-
* thing to do with a large number is to publish it rather than to design it
|
|
13
|
-
* away — see `docs/architecture/load-ledger.md` § "2026-08-27 (night) — the load
|
|
14
|
-
* series reaches disk, and what it costs".
|
|
15
|
-
*
|
|
16
|
-
* ## Measured cost, not estimated
|
|
17
|
-
*
|
|
18
|
-
* `scripts/measure-load-series-cost.mts` writes the real payload shape through
|
|
19
|
-
* the real `SqliteSettingsBackend` and the real `declareCollection` DDL, then
|
|
20
|
-
* reads `page_count × page_size` off the resulting file. The answer is
|
|
21
|
-
* **{@link MEASURED_BYTES_PER_ROW} bytes per row**, flat from 164 k rows to
|
|
22
|
-
* 3.9 M rows — table plus its one index, after a truncating WAL checkpoint.
|
|
23
|
-
*
|
|
24
|
-
* That number is why the columns look the way they do. Four decisions, each
|
|
25
|
-
* paid for in bytes measured on the same harness:
|
|
26
|
-
*
|
|
27
|
-
* | shape | B/row |
|
|
28
|
-
* | --- | --- |
|
|
29
|
-
* | naive: `TEXT` id, 2 indexes, every `NodeProcess` field | 196.0 |
|
|
30
|
-
* | one index, drop `threadCount` + `uptimeSec` | 172.8 |
|
|
31
|
-
* | + base36 in the composite id | 159.6 |
|
|
32
|
-
* | + CPU as integer tenths, RSS as integer MiB | 140.3 |
|
|
33
|
-
* | + `INTEGER` rowid-alias key (this file) | **86.9** |
|
|
34
|
-
*
|
|
35
|
-
* - **`command` is not stored.** It is the fattest field in a `NodeProcess` and
|
|
36
|
-
* it is the same ~100-character string on every runner — the runner id
|
|
37
|
-
* travels in the environment, not in argv, so `ps` reports one identical
|
|
38
|
-
* command line for all of them. `addonId` is the short identifier that
|
|
39
|
-
* already exists, and nothing that reads this series reads `command`.
|
|
40
|
-
* - **CPU is stored as integer tenths.** Losslessly: `ps pcpu` is rounded to
|
|
41
|
-
* one decimal by `runPs`, and the thread sampler rounds to one decimal too
|
|
42
|
-
* (`Math.round(x * 1000) / 10`). A SQLite `REAL` is 8 bytes always; the same
|
|
43
|
-
* value as an integer varint is 1-2.
|
|
44
|
-
* - **RSS is stored in whole MiB.** A byte-exact residency on a value in the
|
|
45
|
-
* hundreds of megabytes is precision no chart can draw, and it costs 3 bytes
|
|
46
|
-
* a row.
|
|
47
|
-
* - **The key is an `INTEGER` rowid alias.** A composite `TEXT` key cost ~40
|
|
48
|
-
* B/row — the string in the row plus its own B-tree — for an idempotence
|
|
49
|
-
* this table gets for free somewhere better: {@link NodeLoadRing.record}
|
|
50
|
-
* already refuses a sample at or before the newest it holds, and only an
|
|
51
|
-
* ACCEPTED sample is written here. The gate is one monotonic comparison in
|
|
52
|
-
* memory instead of a unique index on two million rows.
|
|
53
|
-
*
|
|
54
|
-
* ## Retention is an intention; the row cap is the guarantee
|
|
55
|
-
*
|
|
56
|
-
* {@link LoadSeriesRetention.retentionHours} is what the operator asked to
|
|
57
|
-
* keep. {@link DEFAULT_MAX_ROWS} is what the table is allowed to become. They
|
|
58
|
-
* are different promises on purpose: an operator who raises retention to three
|
|
59
|
-
* days must not be able to take this table to two million rows with nothing
|
|
60
|
-
* stopping it. When the cap bites, the OLDEST rows go and a line says so.
|
|
61
|
-
*/
|
|
62
|
-
import type { IScopedLogger, SettingsStoreClient } from "@camstack/types";
|
|
63
|
-
/**
|
|
64
|
-
* @durable class=audit owner=metrics-native
|
|
65
|
-
* write="one row per process per sample, appended in ONE transaction per
|
|
66
|
-
* sample (`settingsStore.insertMany`) by the HUB's `native-metrics`, off its
|
|
67
|
-
* subscription to `metrics.node-processes-snapshot`. Emission is
|
|
68
|
-
* fixed-cadence, so the write rate is deterministic: nodes x processes /
|
|
69
|
-
* cadence, 7.6 rows/s on the live fleet at the 10 s default. Only a sample
|
|
70
|
-
* the in-memory ring ACCEPTED is written, which is what makes the append
|
|
71
|
-
* idempotent without a unique key."
|
|
72
|
-
* retention="TWO bounds, swept together at most once a minute. By AGE, the
|
|
73
|
-
* operator's `loadSeriesRetentionHours` (default 6, the intention); by ROWS,
|
|
74
|
-
* DEFAULT_MAX_ROWS (the guarantee, which bites regardless of the age knob).
|
|
75
|
-
* Both prune oldest-first through a bounded page — never a full scan — and
|
|
76
|
-
* the row cap logs when it bites. Losing the table loses the cold window
|
|
77
|
-
* only: the ring keeps serving the last hour and the charts keep drawing."
|
|
78
|
-
*/
|
|
79
|
-
export declare const LOAD_SERIES_COLLECTION = "metrics:node-load-samples";
|
|
80
|
-
/**
|
|
81
|
-
* Bytes one stored row costs on disk, MEASURED — table plus index, after a
|
|
82
|
-
* truncating checkpoint. Used to project the cost of a configuration for the
|
|
83
|
-
* operator BEFORE they apply it, so the knob is honest.
|
|
84
|
-
*/
|
|
85
|
-
export declare const MEASURED_BYTES_PER_ROW = 87;
|
|
86
|
-
/**
|
|
87
|
-
* Sampling cadence bounds, in seconds. Outside this range a cadence is
|
|
88
|
-
* REFUSED — see {@link resolveLoadSeriesConfig}.
|
|
89
|
-
*/
|
|
90
|
-
export declare const MIN_CADENCE_SEC = 5;
|
|
91
|
-
export declare const MAX_CADENCE_SEC = 60;
|
|
92
|
-
export declare const DEFAULT_CADENCE_SEC = 10;
|
|
93
|
-
/** Retention the operator asked for, in hours. Six is the shipped default. */
|
|
94
|
-
export declare const DEFAULT_RETENTION_HOURS = 6;
|
|
95
|
-
export declare const MIN_RETENTION_HOURS = 1;
|
|
96
|
-
export declare const MAX_RETENTION_HOURS = 72;
|
|
97
|
-
/**
|
|
98
|
-
* Hard ceiling on the table, in ROWS.
|
|
99
|
-
*
|
|
100
|
-
* The guarantee behind the retention knob. Sized against the shipped default
|
|
101
|
-
* and against the worst configuration the knob can express:
|
|
102
|
-
*
|
|
103
|
-
* | cadence | retention | rows (3 nodes, 76 processes) |
|
|
104
|
-
* | --- | --- | --- |
|
|
105
|
-
* | 10 s | 6 h (default) | 164 160 |
|
|
106
|
-
* | 5 s | 6 h | 328 320 |
|
|
107
|
-
* | 5 s | 72 h | 3 939 840 — capped here |
|
|
108
|
-
*
|
|
109
|
-
* 500 000 rows x the measured 87 B = **41.5 MiB**, roughly 3x what the shipped
|
|
110
|
-
* default needs and far below the 2 M rows a three-day retention would reach.
|
|
111
|
-
* It is deliberately NOT a knob: the point of a guarantee is that it is not
|
|
112
|
-
* subject to the same hand that raised the intention. This is the discipline
|
|
113
|
-
* the in-memory ring already adopted (`DEFAULT_MAX_TOTAL_PROCESS_ROWS`) and
|
|
114
|
-
* the one the RTP pre-roll rings still lack — a maximum per instance and no
|
|
115
|
-
* budget at all for the whole, which reached 898 MB across 47 rings
|
|
116
|
-
* (`docs/architecture/load-ledger.md` #5).
|
|
117
|
-
*/
|
|
118
|
-
export declare const DEFAULT_MAX_ROWS = 5e5;
|
|
119
|
-
/**
|
|
120
|
-
* Rows a single prune pass may remove.
|
|
121
|
-
*
|
|
122
|
-
* The prune is BOUNDED, never a full scan. It reads at most this many keys
|
|
123
|
-
* oldest-first through `idx_load_samples_node_at`, takes the last one's `atMs`
|
|
124
|
-
* as the effective cutoff and issues ONE `deleteWhere` up to it. A backlog
|
|
125
|
-
* larger than a page is worked off across passes rather than in one stall —
|
|
126
|
-
* which is the defect corrected in the recorder the same week, where
|
|
127
|
-
* `accountingForLocations` visited 6.7 M rows twice a minute.
|
|
128
|
-
*/
|
|
129
|
-
export declare const PRUNE_PAGE_ROWS = 2e4;
|
|
130
|
-
/** How often a prune pass may run. A bound, not a deadline. */
|
|
131
|
-
export declare const PRUNE_INTERVAL_MS = 6e4;
|
|
132
|
-
export declare const LOAD_SERIES_COLUMNS: readonly [{
|
|
133
|
-
readonly name: "id";
|
|
134
|
-
readonly type: "INTEGER";
|
|
135
|
-
readonly primaryKey: true;
|
|
136
|
-
readonly notNull: true;
|
|
137
|
-
}, {
|
|
138
|
-
readonly name: "nodeId";
|
|
139
|
-
readonly type: "TEXT";
|
|
140
|
-
readonly notNull: true;
|
|
141
|
-
}, {
|
|
142
|
-
readonly name: "atMs";
|
|
143
|
-
readonly type: "INTEGER";
|
|
144
|
-
readonly notNull: true;
|
|
145
|
-
}, {
|
|
146
|
-
readonly name: "pid";
|
|
147
|
-
readonly type: "INTEGER";
|
|
148
|
-
readonly notNull: true;
|
|
149
|
-
}, {
|
|
150
|
-
readonly name: "addonId";
|
|
151
|
-
readonly type: "TEXT";
|
|
152
|
-
}, {
|
|
153
|
-
readonly name: "classification";
|
|
154
|
-
readonly type: "TEXT";
|
|
155
|
-
readonly notNull: true;
|
|
156
|
-
}, {
|
|
157
|
-
readonly name: "cpuDeci";
|
|
158
|
-
readonly type: "INTEGER";
|
|
159
|
-
readonly notNull: true;
|
|
160
|
-
}, {
|
|
161
|
-
readonly name: "rssMib";
|
|
162
|
-
readonly type: "INTEGER";
|
|
163
|
-
readonly notNull: true;
|
|
164
|
-
}, {
|
|
165
|
-
readonly name: "cpuMainDeci";
|
|
166
|
-
readonly type: "INTEGER";
|
|
167
|
-
}, {
|
|
168
|
-
readonly name: "cpuGcDeci";
|
|
169
|
-
readonly type: "INTEGER";
|
|
170
|
-
}];
|
|
171
|
-
export declare const LOAD_SERIES_INDEXES: readonly [{
|
|
172
|
-
readonly name: "idx_load_samples_node_at";
|
|
173
|
-
readonly columns: readonly ["nodeId", "atMs"];
|
|
174
|
-
}];
|
|
175
|
-
/** One stored process row, in the units the table stores them in. */
|
|
176
|
-
export interface LoadSeriesRow {
|
|
177
|
-
readonly nodeId: string;
|
|
178
|
-
readonly atMs: number;
|
|
179
|
-
readonly pid: number;
|
|
180
|
-
readonly addonId: string | null;
|
|
181
|
-
readonly classification: string;
|
|
182
|
-
readonly cpuDeci: number;
|
|
183
|
-
readonly rssMib: number;
|
|
184
|
-
readonly cpuMainDeci: number | null;
|
|
185
|
-
readonly cpuGcDeci: number | null;
|
|
186
|
-
}
|
|
187
|
-
/** The process shape both tiers hand back — the series' own row, not `ps`'s. */
|
|
188
|
-
export interface RetainedProcess {
|
|
189
|
-
readonly pid: number;
|
|
190
|
-
readonly addonId: string | null;
|
|
191
|
-
readonly classification: string;
|
|
192
|
-
readonly cpuPercent: number;
|
|
193
|
-
readonly memoryRssBytes: number;
|
|
194
|
-
readonly cpuMainPercent: number | null;
|
|
195
|
-
readonly cpuGcPercent: number | null;
|
|
196
|
-
}
|
|
197
|
-
/** One retained sample: when it was taken, and the processes in it. */
|
|
198
|
-
export interface RetainedSample {
|
|
199
|
-
readonly atMs: number;
|
|
200
|
-
readonly processes: readonly RetainedProcess[];
|
|
201
|
-
}
|
|
202
|
-
/** Storage row → the shape a reader gets. The ONE decode. */
|
|
203
|
-
export declare function rowToProcess(row: LoadSeriesRow): RetainedProcess;
|
|
204
|
-
/** The shape a reader gets → the storage row. The ONE encode. */
|
|
205
|
-
export declare function processToRow(nodeId: string, atMs: number, process: RetainedProcess): LoadSeriesRow;
|
|
206
|
-
/** Group flat rows into samples, oldest-first. A sample is one `atMs`. */
|
|
207
|
-
export declare function rowsToSamples(rows: readonly LoadSeriesRow[]): readonly RetainedSample[];
|
|
208
|
-
/** The two bounds in force. */
|
|
209
|
-
export interface LoadSeriesRetention {
|
|
210
|
-
readonly retentionHours: number;
|
|
211
|
-
readonly maxRows: number;
|
|
212
|
-
}
|
|
213
|
-
export interface LoadSeriesStoreDeps {
|
|
214
|
-
readonly store: SettingsStoreClient;
|
|
215
|
-
readonly logger: IScopedLogger;
|
|
216
|
-
readonly now?: () => number;
|
|
217
|
-
readonly prunePageRows?: number;
|
|
218
|
-
readonly pruneIntervalMs?: number;
|
|
219
|
-
}
|
|
220
|
-
/** What one prune pass did. Returned so a test can assert the bound held. */
|
|
221
|
-
export interface PruneOutcome {
|
|
222
|
-
readonly deletedByAge: number;
|
|
223
|
-
readonly deletedByCap: number;
|
|
224
|
-
/** Rows read to find the cutoffs — the number that must stay bounded. */
|
|
225
|
-
readonly rowsExamined: number;
|
|
226
|
-
readonly capBit: boolean;
|
|
227
|
-
}
|
|
228
|
-
/**
|
|
229
|
-
* The cold tier. Append-only, bounded, and never the authority on anything the
|
|
230
|
-
* hot ring can answer.
|
|
231
|
-
*
|
|
232
|
-
* Every method is best-effort and says so in the log rather than throwing at
|
|
233
|
-
* the emitter: a storage hiccup must cost a gap in a chart, never a dropped
|
|
234
|
-
* snapshot on the bus.
|
|
235
|
-
*/
|
|
236
|
-
export declare class LoadSeriesStore {
|
|
237
|
-
private declared;
|
|
238
|
-
private lastPruneAtMs;
|
|
239
|
-
private readonly store;
|
|
240
|
-
private readonly logger;
|
|
241
|
-
private readonly nowFn;
|
|
242
|
-
private readonly prunePageRows;
|
|
243
|
-
private readonly pruneIntervalMs;
|
|
244
|
-
constructor(deps: LoadSeriesStoreDeps);
|
|
245
|
-
/** Idempotently declare the collection. `false` when the store refused. */
|
|
246
|
-
declare(): Promise<boolean>;
|
|
247
|
-
/**
|
|
248
|
-
* Append ONE sample — every process row of it — in ONE transaction.
|
|
249
|
-
*
|
|
250
|
-
* Never a write per row. `insertMany` exists for exactly this: at the 10 s
|
|
251
|
-
* default the fleet produces 7.6 rows/s, and 7.6 separate commits per second
|
|
252
|
-
* on the connection that also serves every cluster-wide configuration read
|
|
253
|
-
* is a constant load nobody asked for.
|
|
254
|
-
*/
|
|
255
|
-
append(nodeId: string, atMs: number, processes: readonly RetainedProcess[]): Promise<number>;
|
|
256
|
-
/**
|
|
257
|
-
* Read one node's cold samples, oldest-first.
|
|
258
|
-
*
|
|
259
|
-
* `sinceMs` is EXCLUSIVE, matching the ring, so a caller passing the newest
|
|
260
|
-
* timestamp it holds gets only what it is missing. `limitRows` bounds the
|
|
261
|
-
* read in ROWS (not samples) because rows are what the query costs.
|
|
262
|
-
*/
|
|
263
|
-
read(nodeId: string, sinceMs: number, limitRows: number): Promise<readonly RetainedSample[]>;
|
|
264
|
-
/**
|
|
265
|
-
* Enforce BOTH bounds, oldest-first, through a bounded page each.
|
|
266
|
-
*
|
|
267
|
-
* Rate-limited to {@link pruneIntervalMs}: a bound is not a deadline, and the
|
|
268
|
-
* append path must not pay a sweep on every sample.
|
|
269
|
-
*/
|
|
270
|
-
prune(nodeIds: readonly string[], retention: LoadSeriesRetention, force?: boolean): Promise<PruneOutcome | null>;
|
|
271
|
-
/** Total rows, or `null` when the store could not answer. */
|
|
272
|
-
count(): Promise<number | null>;
|
|
273
|
-
/**
|
|
274
|
-
* Delete this node's rows older than `cutoff`, at most one page's worth.
|
|
275
|
-
*
|
|
276
|
-
* The page is the whole point. `deleteWhere({ atMs: [0, cutoff] })` on its
|
|
277
|
-
* own is one statement but an UNBOUNDED one — a first pass after a retention
|
|
278
|
-
* change would delete millions of rows inside a single stalling transaction.
|
|
279
|
-
* So the page is read first (keys only, ordered by the index), its last
|
|
280
|
-
* `atMs` becomes the EFFECTIVE cutoff, and the delete is bounded by it.
|
|
281
|
-
*/
|
|
282
|
-
private pruneNodeToCutoff;
|
|
283
|
-
/**
|
|
284
|
-
* Drop the oldest rows across the known nodes until `excess` is covered.
|
|
285
|
-
*
|
|
286
|
-
* Same bounded-page technique, walked per node so the one index serves it.
|
|
287
|
-
* The node holding the oldest rows pays first, which is also the node
|
|
288
|
-
* producing the pressure when a runaway process count is the cause.
|
|
289
|
-
*/
|
|
290
|
-
private pruneOldestRows;
|
|
291
|
-
}
|
|
292
|
-
/** Structural validation on read. A malformed row is skipped, never charted. */
|
|
293
|
-
export declare function recordToRow(data: Record<string, unknown>): LoadSeriesRow | null;
|