@smart-data-engines/sde 0.1.0-dev.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/LICENSE +201 -0
- package/NOTICE +13 -0
- package/README.md +153 -0
- package/bin/weather.mjs +32 -0
- package/dist/_usage.d.ts +30 -0
- package/dist/_usage.js +194 -0
- package/dist/_usage.js.map +1 -0
- package/dist/bulk.d.ts +9 -0
- package/dist/bulk.js +83 -0
- package/dist/bulk.js.map +1 -0
- package/dist/canonical.d.ts +46 -0
- package/dist/canonical.js +150 -0
- package/dist/canonical.js.map +1 -0
- package/dist/capabilities.d.ts +48 -0
- package/dist/capabilities.js +62 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/cutover.d.ts +36 -0
- package/dist/cutover.js +219 -0
- package/dist/cutover.js.map +1 -0
- package/dist/demo/model.d.ts +28 -0
- package/dist/demo/model.js +40 -0
- package/dist/demo/model.js.map +1 -0
- package/dist/demo/project.d.ts +19 -0
- package/dist/demo/project.js +128 -0
- package/dist/demo/project.js.map +1 -0
- package/dist/demo/weather.d.ts +73 -0
- package/dist/demo/weather.js +334 -0
- package/dist/demo/weather.js.map +1 -0
- package/dist/engines/_clickhouse-connection.d.ts +17 -0
- package/dist/engines/_clickhouse-connection.js +182 -0
- package/dist/engines/_clickhouse-connection.js.map +1 -0
- package/dist/engines/_tls-peer-identity.d.ts +2 -0
- package/dist/engines/_tls-peer-identity.js +23 -0
- package/dist/engines/_tls-peer-identity.js.map +1 -0
- package/dist/engines/_write-fences.d.ts +51 -0
- package/dist/engines/_write-fences.js +189 -0
- package/dist/engines/_write-fences.js.map +1 -0
- package/dist/engines/clickhouse.d.ts +193 -0
- package/dist/engines/clickhouse.js +899 -0
- package/dist/engines/clickhouse.js.map +1 -0
- package/dist/engines/postgres.d.ts +293 -0
- package/dist/engines/postgres.js +981 -0
- package/dist/engines/postgres.js.map +1 -0
- package/dist/errors.d.ts +89 -0
- package/dist/errors.js +90 -0
- package/dist/errors.js.map +1 -0
- package/dist/frozen-verification.d.ts +26 -0
- package/dist/frozen-verification.js +67 -0
- package/dist/frozen-verification.js.map +1 -0
- package/dist/generation.d.ts +34 -0
- package/dist/generation.js +81 -0
- package/dist/generation.js.map +1 -0
- package/dist/groups.d.ts +17 -0
- package/dist/groups.js +66 -0
- package/dist/groups.js.map +1 -0
- package/dist/hashing.d.ts +68 -0
- package/dist/hashing.js +146 -0
- package/dist/hashing.js.map +1 -0
- package/dist/in-place-index.d.ts +43 -0
- package/dist/in-place-index.js +272 -0
- package/dist/in-place-index.js.map +1 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +64 -0
- package/dist/index.js.map +1 -0
- package/dist/inspection.d.ts +19 -0
- package/dist/inspection.js +31 -0
- package/dist/inspection.js.map +1 -0
- package/dist/internal.d.ts +42 -0
- package/dist/internal.js +56 -0
- package/dist/internal.js.map +1 -0
- package/dist/layout.d.ts +36 -0
- package/dist/layout.js +62 -0
- package/dist/layout.js.map +1 -0
- package/dist/migration.d.ts +197 -0
- package/dist/migration.js +592 -0
- package/dist/migration.js.map +1 -0
- package/dist/model.d.ts +93 -0
- package/dist/model.js +313 -0
- package/dist/model.js.map +1 -0
- package/dist/physical.d.ts +128 -0
- package/dist/physical.js +421 -0
- package/dist/physical.js.map +1 -0
- package/dist/placement.d.ts +157 -0
- package/dist/placement.js +651 -0
- package/dist/placement.js.map +1 -0
- package/dist/provisioning.d.ts +6 -0
- package/dist/provisioning.js +45 -0
- package/dist/provisioning.js.map +1 -0
- package/dist/query.d.ts +68 -0
- package/dist/query.js +340 -0
- package/dist/query.js.map +1 -0
- package/dist/routing.d.ts +25 -0
- package/dist/routing.js +35 -0
- package/dist/routing.js.map +1 -0
- package/dist/schema.d.ts +110 -0
- package/dist/schema.js +337 -0
- package/dist/schema.js.map +1 -0
- package/dist/session.d.ts +195 -0
- package/dist/session.js +870 -0
- package/dist/session.js.map +1 -0
- package/dist/shapes.d.ts +30 -0
- package/dist/shapes.js +112 -0
- package/dist/shapes.js.map +1 -0
- package/dist/staging.d.ts +29 -0
- package/dist/staging.js +214 -0
- package/dist/staging.js.map +1 -0
- package/dist/telemetry.d.ts +468 -0
- package/dist/telemetry.js +872 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/testing/loader.d.ts +38 -0
- package/dist/testing/loader.js +86 -0
- package/dist/testing/loader.js.map +1 -0
- package/dist/testing/memory.d.ts +131 -0
- package/dist/testing/memory.js +311 -0
- package/dist/testing/memory.js.map +1 -0
- package/dist/timestamp.d.ts +20 -0
- package/dist/timestamp.js +89 -0
- package/dist/timestamp.js.map +1 -0
- package/dist/types.d.ts +79 -0
- package/dist/types.js +100 -0
- package/dist/types.js.map +1 -0
- package/dist/verification.d.ts +41 -0
- package/dist/verification.js +169 -0
- package/dist/verification.js.map +1 -0
- package/dist/watermark.d.ts +103 -0
- package/dist/watermark.js +170 -0
- package/dist/watermark.js.map +1 -0
- package/dist/write-fence.d.ts +58 -0
- package/dist/write-fence.js +225 -0
- package/dist/write-fence.js.map +1 -0
- package/package.json +86 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Measuring what the application actually does, without ever seeing what it does it to.
|
|
3
|
+
*
|
|
4
|
+
* This is the input a placement decision is made from, so its shape matters more than its
|
|
5
|
+
* precision. Three constraints shaped everything here.
|
|
6
|
+
*
|
|
7
|
+
* **It carries no values.** A record is keyed by an operation shape, which is assembled from the
|
|
8
|
+
* structure of a call and never sees its arguments. There is no code path by which a customer's row
|
|
9
|
+
* reaches a telemetry record, which is why this file can be read by a client and believed.
|
|
10
|
+
*
|
|
11
|
+
* **It cannot cost anything.** Routing already has a one percent budget for the whole library and
|
|
12
|
+
* recording happens on the same path. So: no string formatting per record, no stack walking, and a
|
|
13
|
+
* histogram rather than a list of samples. There is no lock, and that is not a shortcut - this
|
|
14
|
+
* runtime has one thread per record, so the trade the reference implementation makes (a lock taken
|
|
15
|
+
* only when a shape is first seen) has nothing to buy here.
|
|
16
|
+
*
|
|
17
|
+
* **It cannot fail the caller.** Every entry point goes through `guard`. A bug in an aggregation
|
|
18
|
+
* counter must not take down somebody's request - and the failure is counted, so it is not
|
|
19
|
+
* invisible either.
|
|
20
|
+
*
|
|
21
|
+
* The histogram deserves a word, because it is the one deliberate loss of precision. Latency lands
|
|
22
|
+
* in exponential buckets, so a percentile read out of it is approximate - within one bucket width,
|
|
23
|
+
* which is a factor of two at the extremes. That is ample for the decision it feeds: a planner
|
|
24
|
+
* cares whether a group's reads are microseconds or milliseconds, not whether p99 is 412 or 431
|
|
25
|
+
* microseconds.
|
|
26
|
+
*/
|
|
27
|
+
import type { Group } from './groups.js';
|
|
28
|
+
import type { LogicalModel } from './model.js';
|
|
29
|
+
import type { ShapeKind } from './shapes.js';
|
|
30
|
+
/** 1 microsecond to about 17 s, doubling. Enough to tell a cache hit from a full scan. */
|
|
31
|
+
export declare const BUCKET_COUNT = 25;
|
|
32
|
+
export declare const BUCKET_BASE_NS = 1000;
|
|
33
|
+
/** Exponential-bucket histogram. Fixed memory, O(1) record, approximate percentiles. */
|
|
34
|
+
export declare class Histogram {
|
|
35
|
+
readonly buckets: number[];
|
|
36
|
+
count: number;
|
|
37
|
+
total: number;
|
|
38
|
+
/**
|
|
39
|
+
* Put one duration in its bucket. **Integer arithmetic only, and that is the point.**
|
|
40
|
+
*
|
|
41
|
+
* The obvious form is `Math.floor(Math.log2(ns / BUCKET_BASE_NS)) + 1`, which is the same
|
|
42
|
+
* function and the wrong way to compute it in a library that has to agree with another
|
|
43
|
+
* implementation. `log2` is not required by IEEE 754 to be correctly rounded, so two runtimes may
|
|
44
|
+
* differ in the last bit - and one bit at a power-of-two boundary is a different bucket, which is
|
|
45
|
+
* a different p99 for identical traffic, in a number a placement decision is made from.
|
|
46
|
+
*
|
|
47
|
+
* The bit length of the integer quotient is exact everywhere. `Math.clz32` is defined on the
|
|
48
|
+
* 32-bit value, and the quotient is below 2^24 in every case that is not clamped, so the clamp is
|
|
49
|
+
* checked first rather than relying on the coercion.
|
|
50
|
+
*
|
|
51
|
+
* **The vectors cannot see the difference, and that is worth saying rather than implying.**
|
|
52
|
+
* Measured: the logarithm form passes every vector in `telemetry/`, because glibc's `log2` and
|
|
53
|
+
* V8's are both exact at a power of two - the two runtimes we have agree, and the hazard is a
|
|
54
|
+
* *third* libm that does not. A property no output can distinguish on the machines available is
|
|
55
|
+
* not one a vector can hold, so it is held statically: `telemetry.test.ts` refuses a logarithm in
|
|
56
|
+
* this file.
|
|
57
|
+
*/
|
|
58
|
+
record(nanoseconds: number): void;
|
|
59
|
+
/**
|
|
60
|
+
* Approximate percentile in milliseconds, or null if nothing was recorded.
|
|
61
|
+
*
|
|
62
|
+
* Returns the *upper* edge of the bucket the percentile falls in. Rounding up rather than
|
|
63
|
+
* interpolating is deliberate: a placement decision made on an optimistic latency figure is the
|
|
64
|
+
* wrong kind of wrong.
|
|
65
|
+
*/
|
|
66
|
+
percentileMs(fraction: number): number | null;
|
|
67
|
+
merge(other: Histogram): void;
|
|
68
|
+
}
|
|
69
|
+
/** What a read filtered on: the fields compared by equality and the field a range bounded. */
|
|
70
|
+
export interface ReadPredicates {
|
|
71
|
+
readonly equal: readonly string[];
|
|
72
|
+
readonly ranged: string;
|
|
73
|
+
}
|
|
74
|
+
/** What was observed for one operation shape. No values, by construction. */
|
|
75
|
+
export declare class ShapeStats {
|
|
76
|
+
readonly shapeId: string;
|
|
77
|
+
readonly group: string;
|
|
78
|
+
readonly entity: string;
|
|
79
|
+
readonly kind: ShapeKind;
|
|
80
|
+
calls: number;
|
|
81
|
+
rows: number;
|
|
82
|
+
errors: number;
|
|
83
|
+
readonly latency: Histogram;
|
|
84
|
+
/**
|
|
85
|
+
* Calls by what they filtered on - equality fields and the bounded field (`''` for none) - names
|
|
86
|
+
* only, never values. Two reads of one shape can want different key orders; only an operation
|
|
87
|
+
* that takes a `where` reports this, so writes and point reads leave it empty.
|
|
88
|
+
*/
|
|
89
|
+
readonly filtered: Map<string, {
|
|
90
|
+
predicates: ReadPredicates;
|
|
91
|
+
calls: number;
|
|
92
|
+
}>;
|
|
93
|
+
constructor(shapeId: string, group: string, entity: string, kind: ShapeKind);
|
|
94
|
+
record(nanoseconds: number, rows: number, failed: boolean, predicates?: ReadPredicates): void;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* What was observed writing one row to one derived copy. No values, by construction.
|
|
98
|
+
*
|
|
99
|
+
* Deliberately **not** a `ShapeStats`, and that is the load-bearing decision. A fan-out is not an
|
|
100
|
+
* operation the application asked for - it is the library keeping a copy current - so recording it
|
|
101
|
+
* as a shape would add a write to the very counters a placement is scored on: `read_write_ratio`
|
|
102
|
+
* would move because a copy exists, and a group with one copy would look twice as write-heavy as
|
|
103
|
+
* the same group without one. It is also not part of `GroupFeatures`: a copy's freshness does not
|
|
104
|
+
* score a placement, it reports the health of one already made.
|
|
105
|
+
*/
|
|
106
|
+
export declare class FanOutStats {
|
|
107
|
+
readonly group: string;
|
|
108
|
+
readonly materialization: string;
|
|
109
|
+
writes: number;
|
|
110
|
+
/** Rows that did not reach the copy. Absence, not lateness - see `CopyFreshness`. */
|
|
111
|
+
failures: number;
|
|
112
|
+
readonly latency: Histogram;
|
|
113
|
+
constructor(group: string, materialization: string);
|
|
114
|
+
record(nanoseconds: number, failed: boolean): void;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* How far behind one derived copy is, measured.
|
|
118
|
+
*
|
|
119
|
+
* A derived copy in this library is maintained by the fan-out in a write, in the client's own
|
|
120
|
+
* process, straight after the source. There is no asynchronous replication anywhere, so there is no
|
|
121
|
+
* queue to fall behind in. Two things can therefore be true of a copy, and only two: it is **late**
|
|
122
|
+
* by at most the duration of that one write, or the write **failed** and the row is absent rather
|
|
123
|
+
* than late. Both are reported, because a copy missing a thousand rows can have an excellent p99
|
|
124
|
+
* and a client told only the percentile reads "0.9 ms behind" off a copy that is missing yesterday.
|
|
125
|
+
*/
|
|
126
|
+
export interface CopyFreshness {
|
|
127
|
+
readonly group: string;
|
|
128
|
+
readonly materialization: string;
|
|
129
|
+
readonly writes: number;
|
|
130
|
+
readonly failures: number;
|
|
131
|
+
readonly lagP50Ms: number | null;
|
|
132
|
+
readonly lagP99Ms: number | null;
|
|
133
|
+
/** Whether every write reached the copy in this window. */
|
|
134
|
+
readonly complete: boolean;
|
|
135
|
+
}
|
|
136
|
+
export declare function copyFreshnessRecord(copy: CopyFreshness): Record<string, unknown>;
|
|
137
|
+
/**
|
|
138
|
+
* The contract between telemetry and the planner.
|
|
139
|
+
*
|
|
140
|
+
* `null` means *unknown*, which is not zero and is treated differently by the planner. Anything
|
|
141
|
+
* unknown also appears in `missing`, so a reader never has to infer absence from a null.
|
|
142
|
+
*/
|
|
143
|
+
export interface GroupFeatures {
|
|
144
|
+
/**
|
|
145
|
+
* How many operations were observed. A count, never a value.
|
|
146
|
+
*
|
|
147
|
+
* Present because a planner comparing two groups has to know which one carries the traffic:
|
|
148
|
+
* without it, an idle group and the group serving every request are equally important. It is also
|
|
149
|
+
* evidence about the features themselves - a read/write ratio derived from twelve calls is
|
|
150
|
+
* arithmetic, not a measurement.
|
|
151
|
+
*/
|
|
152
|
+
readonly calls: number;
|
|
153
|
+
readonly readWriteRatio: number | null;
|
|
154
|
+
readonly shapeMix: Readonly<Record<string, number>>;
|
|
155
|
+
readonly latencyP50Ms: number | null;
|
|
156
|
+
readonly latencyP99Ms: number | null;
|
|
157
|
+
readonly resultCardinalityP50: number | null;
|
|
158
|
+
readonly resultCardinalityP99: number | null;
|
|
159
|
+
readonly totalBytes: number | null;
|
|
160
|
+
readonly dailyGrowthBytes: number | null;
|
|
161
|
+
readonly indexToTableRatio: number | null;
|
|
162
|
+
readonly pkAccessShare: number | null;
|
|
163
|
+
readonly hasTimeDimension: boolean;
|
|
164
|
+
readonly timeFilteredShare: number | null;
|
|
165
|
+
readonly distinctShapes: number;
|
|
166
|
+
readonly writeBurstiness: number | null;
|
|
167
|
+
readonly errorShare: number | null;
|
|
168
|
+
readonly missing: readonly string[];
|
|
169
|
+
readonly complete: boolean;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* The document key for every measured field, and the property that carries it.
|
|
173
|
+
*
|
|
174
|
+
* Two spellings because two things are being named: the **document** is the format, shared with
|
|
175
|
+
* every other implementation and therefore `snake_case`; the **property** is this language's, and a
|
|
176
|
+
* library that reads like transliterated Python is a worse library in TypeScript and no better an
|
|
177
|
+
* SDE one.
|
|
178
|
+
*
|
|
179
|
+
* The reference implementation derives its equivalent from the dataclass at runtime, which is not
|
|
180
|
+
* available here - types are erased before the code runs. So the list is the source and the type is
|
|
181
|
+
* checked against it, which is the same guarantee in the other direction and is enforced by the
|
|
182
|
+
* compiler rather than by a test: see `FIELD_LIST_IS_TOTAL`.
|
|
183
|
+
*/
|
|
184
|
+
export declare const MEASURED_FIELDS: readonly [readonly ["calls", "calls"], readonly ["read_write_ratio", "readWriteRatio"], readonly ["shape_mix", "shapeMix"], readonly ["latency_p50_ms", "latencyP50Ms"], readonly ["latency_p99_ms", "latencyP99Ms"], readonly ["result_cardinality_p50", "resultCardinalityP50"], readonly ["result_cardinality_p99", "resultCardinalityP99"], readonly ["total_bytes", "totalBytes"], readonly ["daily_growth_bytes", "dailyGrowthBytes"], readonly ["index_to_table_ratio", "indexToTableRatio"], readonly ["pk_access_share", "pkAccessShare"], readonly ["has_time_dimension", "hasTimeDimension"], readonly ["time_filtered_share", "timeFilteredShare"], readonly ["distinct_shapes", "distinctShapes"], readonly ["write_burstiness", "writeBurstiness"], readonly ["error_share", "errorShare"]];
|
|
185
|
+
type MeasuredProperty = (typeof MEASURED_FIELDS)[number][1];
|
|
186
|
+
type Bookkeeping = 'missing' | 'complete';
|
|
187
|
+
/**
|
|
188
|
+
* A compile-time ratchet in both directions.
|
|
189
|
+
*
|
|
190
|
+
* Add a field to `GroupFeatures` and forget the list, and the second element stops being
|
|
191
|
+
* assignable; put a name in the list that is not a field, and the first does. `tsc` fails either
|
|
192
|
+
* way, which is where this belongs: the control plane's reader had a hand-written field list
|
|
193
|
+
* against a dataclass where every field has a default, so the next field added to the library would
|
|
194
|
+
* have been recorded as *measured* rather than missing. That defect had to be found by mutation.
|
|
195
|
+
* This one cannot exist.
|
|
196
|
+
*/
|
|
197
|
+
export declare const FIELD_LIST_IS_TOTAL: [
|
|
198
|
+
MeasuredProperty extends keyof GroupFeatures ? true : never,
|
|
199
|
+
Exclude<keyof GroupFeatures, Bookkeeping> extends MeasuredProperty ? true : never
|
|
200
|
+
];
|
|
201
|
+
/**
|
|
202
|
+
* The feature vector as the document that crosses the boundary to the control plane.
|
|
203
|
+
*
|
|
204
|
+
* **A field with no value is omitted, and `missing` is what says so.** Emitting a null would work
|
|
205
|
+
* too - the reader treats absent and null alike - but omitting is the honest spelling of "not
|
|
206
|
+
* measured", and it keeps this function from having an opinion about what a null means.
|
|
207
|
+
*/
|
|
208
|
+
export declare function featuresRecord(features: GroupFeatures): Record<string, unknown>;
|
|
209
|
+
/** The bucket a write's rows are counted in for `write_burstiness`. */
|
|
210
|
+
export declare const SECOND_NS = 1000000000;
|
|
211
|
+
/** The kinds of read that take a `where` and so report what they filtered on (`filtered_on`). */
|
|
212
|
+
export declare const FILTERED_KINDS: ReadonlySet<string>;
|
|
213
|
+
/**
|
|
214
|
+
* The shortest span a daily growth is projected from.
|
|
215
|
+
*
|
|
216
|
+
* A day projected from a few seconds of writes is a number with no basis: a demonstration writing a
|
|
217
|
+
* hundred rows in two seconds would claim gigabytes a day. An hour is where the projection
|
|
218
|
+
* multiplies a measurement by 24 rather than by thousands.
|
|
219
|
+
*/
|
|
220
|
+
export declare const GROWTH_MIN_NS = 3600000000000;
|
|
221
|
+
/** The unit growth is projected to, and how long the recorder keeps a group's storage samples. */
|
|
222
|
+
export declare const DAY_NS = 86400000000000;
|
|
223
|
+
/**
|
|
224
|
+
* One group's bytes on its source materialisation at one moment, from the engine's catalogue.
|
|
225
|
+
*
|
|
226
|
+
* Numbers only. `totalBytes` is everything the group's tables occupy, indexes included;
|
|
227
|
+
* `secondaryIndexBytes` is the part that is indexes other than the one enforcing the key - the part
|
|
228
|
+
* a physical design adds and can remove. `atNs` is the recorder's clock.
|
|
229
|
+
*/
|
|
230
|
+
export interface StorageSample {
|
|
231
|
+
readonly group: string;
|
|
232
|
+
readonly atNs: number;
|
|
233
|
+
readonly totalBytes: number;
|
|
234
|
+
readonly secondaryIndexBytes: number;
|
|
235
|
+
}
|
|
236
|
+
/** One group's size on its source materialisation, as the engine's catalogue gave it. */
|
|
237
|
+
export interface StorageSize {
|
|
238
|
+
readonly group: string;
|
|
239
|
+
readonly materialization: string;
|
|
240
|
+
readonly engine: string;
|
|
241
|
+
readonly totalBytes: number;
|
|
242
|
+
readonly secondaryIndexBytes: number;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Why a group's size stayed unknown: its engine's adapter has no catalogue to read, a table the map
|
|
246
|
+
* names does not exist, the catalogue refused the login (ClickHouse without the `system.parts`
|
|
247
|
+
* grant) or the read failed otherwise.
|
|
248
|
+
*/
|
|
249
|
+
export declare const STORAGE_UNAVAILABLE: readonly ["failed", "missing_table", "refused", "unsupported"];
|
|
250
|
+
export type StorageUnavailable = (typeof STORAGE_UNAVAILABLE)[number];
|
|
251
|
+
/**
|
|
252
|
+
* What `Session.measureStorage` measured, and why the rest stayed unknown - a class of reason per
|
|
253
|
+
* group, never the engine's message.
|
|
254
|
+
*/
|
|
255
|
+
export interface StorageMeasurement {
|
|
256
|
+
readonly sizes: readonly StorageSize[];
|
|
257
|
+
readonly unavailable: Readonly<Record<string, StorageUnavailable>>;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* A catalogue size as an exact safe integer, or a refusal. Drivers hand a 64-bit count over as text;
|
|
261
|
+
* a size past 2^53 bytes would round, and a rounded size is a wrong one.
|
|
262
|
+
*/
|
|
263
|
+
export declare function exactBytes(value: unknown): number;
|
|
264
|
+
/**
|
|
265
|
+
* One aggregation period, ready to send.
|
|
266
|
+
*
|
|
267
|
+
* `complete` is false when the application could not collect part of the period, or when the buffer
|
|
268
|
+
* dropped windows. A window that is not complete is still sent - the planner needs to know traffic
|
|
269
|
+
* existed - but it may not be used to justify a migration.
|
|
270
|
+
*/
|
|
271
|
+
export interface Window {
|
|
272
|
+
readonly modelVersion: string;
|
|
273
|
+
readonly startedNs: number;
|
|
274
|
+
readonly endedNs: number;
|
|
275
|
+
readonly shapes: readonly ShapeStats[];
|
|
276
|
+
readonly complete: boolean;
|
|
277
|
+
readonly droppedWindows: number;
|
|
278
|
+
/**
|
|
279
|
+
* What the fan-out to each derived copy did. Empty when the group has no copy, which is the
|
|
280
|
+
* ordinary case - a copy exists during a migration and while a derived materialisation is in the
|
|
281
|
+
* map, not otherwise.
|
|
282
|
+
*/
|
|
283
|
+
readonly fanned: readonly FanOutStats[];
|
|
284
|
+
/**
|
|
285
|
+
* The storage samples taken during this window - between the roll that opened it and the one
|
|
286
|
+
* that closed it, by the recorder's own state rather than by comparing clock readings, so a
|
|
287
|
+
* sample taken at the instant of a roll belongs to exactly one window.
|
|
288
|
+
*/
|
|
289
|
+
readonly storage: readonly StorageSample[];
|
|
290
|
+
/**
|
|
291
|
+
* Every storage sample the recorder kept when this window closed: this window's and earlier ones
|
|
292
|
+
* up to a day old, which a daily growth is projected against.
|
|
293
|
+
*/
|
|
294
|
+
readonly storageHistory: readonly StorageSample[];
|
|
295
|
+
/** Rows written by successful writes, per group, per whole second of the window from its start. */
|
|
296
|
+
readonly writeSeconds: ReadonlyMap<string, ReadonlyMap<number, number>>;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* How far behind each of this group's derived copies ran, sorted by materialisation.
|
|
300
|
+
*
|
|
301
|
+
* Read out of the histogram rather than stored, like every other percentile here. A stored
|
|
302
|
+
* percentile is a second copy of a fact that changes when the samples do.
|
|
303
|
+
*/
|
|
304
|
+
export declare function windowCopies(window: Window, group: string): CopyFreshness[];
|
|
305
|
+
/**
|
|
306
|
+
* What each operation shape of one group measured, in the model's enumeration order.
|
|
307
|
+
*
|
|
308
|
+
* The evidence a physical design can point at. A group's features say that 62% of its calls were
|
|
309
|
+
* range reads; only this says *which field* they ranged over, which is the fact a key order or a
|
|
310
|
+
* partition is chosen from. Still no values: a shape is the structure of a call, and the fields it
|
|
311
|
+
* names are the model's field names (digests, when the client hashes them).
|
|
312
|
+
*
|
|
313
|
+
* The descriptor - entity, kind, fields, target - comes from the model's own enumeration by
|
|
314
|
+
* identifier, never from the recorder, and `target` appears only on a relation walk. A record for an
|
|
315
|
+
* identifier the model does not enumerate is refused rather than described by guesswork. Every
|
|
316
|
+
* number is an integer count or a bucket edge divided by a million, like the rest of the document.
|
|
317
|
+
*/
|
|
318
|
+
export declare function windowShapes(window: Window, model: LogicalModel, group: string): Record<string, unknown>[];
|
|
319
|
+
export interface FeatureOptions {
|
|
320
|
+
readonly hasTimeDimension?: boolean;
|
|
321
|
+
/**
|
|
322
|
+
* Per entity, the fields of a time type (`timeFields`), which `time_filtered_share` is counted
|
|
323
|
+
* against. Without it that share stays unknown: which fields are times is a fact about the model,
|
|
324
|
+
* and the window does not guess it.
|
|
325
|
+
*/
|
|
326
|
+
readonly timeFields?: ReadonlyMap<string, ReadonlySet<string>>;
|
|
327
|
+
/**
|
|
328
|
+
* A range read's shape identifier and the field its shape ranges over, which settles a range read
|
|
329
|
+
* whose filters were not reported.
|
|
330
|
+
*/
|
|
331
|
+
readonly rangeFields?: ReadonlyMap<string, string>;
|
|
332
|
+
}
|
|
333
|
+
/** Fold this window's records for one group into the planner's feature vector. */
|
|
334
|
+
export declare function windowFeatures(window: Window, group: string, options?: FeatureOptions): GroupFeatures;
|
|
335
|
+
/**
|
|
336
|
+
* This window as the document the control plane reads. Numbers, never rows.
|
|
337
|
+
*
|
|
338
|
+
* The model is required and it is checked. Only one fact is read from it - whether a group carries a
|
|
339
|
+
* time dimension, which is decided by declared *type* and never by a field's name - but a window
|
|
340
|
+
* serialised against the wrong model would attach that fact to the wrong groups and claim
|
|
341
|
+
* `has_time_dimension: false` for a group that has one. False is a claim; a measurement this
|
|
342
|
+
* library cannot make has to be absent.
|
|
343
|
+
*
|
|
344
|
+
* **This document is deliberately not canonical, and that needs saying because section 1 of the
|
|
345
|
+
* format contract rejects floating point outright.** Its reason is that a float's textual form
|
|
346
|
+
* differs between languages, and almost every number here is a float. This document is not signed,
|
|
347
|
+
* not hashed and never compared for equality, so the rule it breaks does not apply - but the thing
|
|
348
|
+
* that makes the `telemetry/` family checkable is narrower and worth stating: every number here is
|
|
349
|
+
* either **a ratio of two integers** or **a bucket edge divided by a million**, and IEEE 754
|
|
350
|
+
* requires division to be correctly rounded. So two languages compute the same double from the same
|
|
351
|
+
* traffic even where they would print it differently, which is why those vectors compare numbers
|
|
352
|
+
* rather than bytes.
|
|
353
|
+
*/
|
|
354
|
+
export declare function windowRecord(window: Window, model: LogicalModel): Record<string, unknown>;
|
|
355
|
+
/**
|
|
356
|
+
* Does any entity in this group carry a time dimension?
|
|
357
|
+
*
|
|
358
|
+
* Decided by the declared type and never by the field's name. A name is not evidence: `created_at`
|
|
359
|
+
* typed as a string is a string, and treating it as a timestamp would have the planner recommend
|
|
360
|
+
* time partitioning on a column no engine can range-scan usefully. And a client may hash identifier
|
|
361
|
+
* names so that we never see them - a derivation that read names would silently produce different
|
|
362
|
+
* answers with hashing on and off, which is the property the hashed-model vectors forbid.
|
|
363
|
+
*/
|
|
364
|
+
export declare function hasTimeDimension(model: LogicalModel, group: Group): boolean;
|
|
365
|
+
/**
|
|
366
|
+
* Each entity of this group and its fields of a time type - by type, never by name.
|
|
367
|
+
*
|
|
368
|
+
* What `time_filtered_share` counts against, for the reasons `hasTimeDimension` gives: a
|
|
369
|
+
* `created_at` typed as a string is not a time, and a hashed model must answer the same.
|
|
370
|
+
*/
|
|
371
|
+
export declare function timeFields(model: LogicalModel, group: Group): Map<string, Set<string>>;
|
|
372
|
+
export interface StorageOptions {
|
|
373
|
+
readonly group: string;
|
|
374
|
+
readonly totalBytes: number;
|
|
375
|
+
readonly secondaryIndexBytes: number;
|
|
376
|
+
}
|
|
377
|
+
export interface RecordOptions {
|
|
378
|
+
readonly shapeId: string;
|
|
379
|
+
readonly group: string;
|
|
380
|
+
readonly entity: string;
|
|
381
|
+
readonly kind: ShapeKind;
|
|
382
|
+
readonly nanoseconds: number;
|
|
383
|
+
readonly rows?: number;
|
|
384
|
+
readonly failed?: boolean;
|
|
385
|
+
/**
|
|
386
|
+
* For an operation that takes a `where`: the fields it compared by equality. Absent for one that
|
|
387
|
+
* does not filter at all, and then nothing about filters is recorded.
|
|
388
|
+
*/
|
|
389
|
+
readonly equal?: readonly string[];
|
|
390
|
+
/** The field a range bounded, for a filtered read. */
|
|
391
|
+
readonly ranged?: string | null;
|
|
392
|
+
}
|
|
393
|
+
export interface FanOutOptions {
|
|
394
|
+
readonly group: string;
|
|
395
|
+
readonly materialization: string;
|
|
396
|
+
readonly nanoseconds: number;
|
|
397
|
+
readonly failed?: boolean;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Accumulates records, rolls windows, and drops telemetry rather than anything else.
|
|
401
|
+
*
|
|
402
|
+
* No lock, because this runtime does not need one - and that is worth stating rather than leaving
|
|
403
|
+
* as an absence. The reference implementation takes one only when a shape is first seen or a window
|
|
404
|
+
* rolls, and accepts that two threads racing on the same shape can lose a call from a count. Here
|
|
405
|
+
* there is nothing to race: the cost this class is allowed is zero either way.
|
|
406
|
+
*/
|
|
407
|
+
export declare class Recorder {
|
|
408
|
+
private readonly modelVersion;
|
|
409
|
+
private readonly maxWindows;
|
|
410
|
+
private readonly clock;
|
|
411
|
+
private current;
|
|
412
|
+
private fanned;
|
|
413
|
+
private writes;
|
|
414
|
+
private storage;
|
|
415
|
+
private storageWindow;
|
|
416
|
+
private startedNs;
|
|
417
|
+
private windows;
|
|
418
|
+
private dropped;
|
|
419
|
+
private incomplete;
|
|
420
|
+
/**
|
|
421
|
+
* `clock` returns nanoseconds and defaults to a monotonic one; tests and the conformance vectors
|
|
422
|
+
* pass their own, because a write's second and a sample's age are read from it.
|
|
423
|
+
*/
|
|
424
|
+
constructor(modelVersion: string, maxWindows?: number, clock?: () => number);
|
|
425
|
+
/** Record one operation. Never throws. */
|
|
426
|
+
record(options: RecordOptions): void;
|
|
427
|
+
/**
|
|
428
|
+
* Record one group's size, as the engine's catalogue reported it. Never throws.
|
|
429
|
+
*
|
|
430
|
+
* `Session.measureStorage` calls this for every group it could measure. A sample is kept for a
|
|
431
|
+
* day, across windows, because growth is a property of time rather than of one window; a sample
|
|
432
|
+
* that is not two non-negative safe integers with the index part inside the total is dropped,
|
|
433
|
+
* never guessed into shape.
|
|
434
|
+
*/
|
|
435
|
+
recordStorage(options: StorageOptions): void;
|
|
436
|
+
/**
|
|
437
|
+
* Record one write to one derived copy. Never throws.
|
|
438
|
+
*
|
|
439
|
+
* A separate entry point from `record` rather than a shape kind, because a fan-out is not an
|
|
440
|
+
* operation the application asked for - see `FanOutStats`.
|
|
441
|
+
*/
|
|
442
|
+
recordFanOut(options: FanOutOptions): void;
|
|
443
|
+
/**
|
|
444
|
+
* Close the current period and queue it.
|
|
445
|
+
*
|
|
446
|
+
* Returns the window, or undefined if nothing was recorded in it.
|
|
447
|
+
*/
|
|
448
|
+
roll(): Window | undefined;
|
|
449
|
+
pending(): readonly Window[];
|
|
450
|
+
/**
|
|
451
|
+
* Called by the application when part of the current period was not recorded.
|
|
452
|
+
*
|
|
453
|
+
* The flag travels in `Window.complete` and the planner reads it, because a window missing a
|
|
454
|
+
* slice of the traffic must not be scored as though it were the whole period. Nothing here
|
|
455
|
+
* decides when that happened: the application collects these windows and hands them over, and
|
|
456
|
+
* only it knows whether a collection was skipped.
|
|
457
|
+
*/
|
|
458
|
+
markIncomplete(): void;
|
|
459
|
+
/**
|
|
460
|
+
* Drop the oldest `count` windows once the application has taken them.
|
|
461
|
+
*
|
|
462
|
+
* The delivering party is the client's own process, and there is no other. This library never
|
|
463
|
+
* opens a connection to us - the buffer is read with `pending`, written wherever the application
|
|
464
|
+
* writes it, and that file is what we are handed.
|
|
465
|
+
*/
|
|
466
|
+
acknowledge(count: number): void;
|
|
467
|
+
}
|
|
468
|
+
export {};
|