@cloudbitmaps/core 0.16.0 → 0.17.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/dist/{chunk-LFUNPMXZ.js → chunk-CV7GJO3B.js} +363 -23
- package/dist/chunk-CV7GJO3B.js.map +7 -0
- package/dist/core/cost.d.ts +3 -3
- package/dist/core/crbm-storage-source.d.ts +73 -65
- package/dist/core/engine.d.ts +23 -0
- package/dist/core/erasure.d.ts +2 -1
- package/dist/core/errors.d.ts +23 -0
- package/dist/core/generation-gc.d.ts +69 -45
- package/dist/core/kept-generations.d.ts +40 -0
- package/dist/core/leases.d.ts +119 -0
- package/dist/core/load-input.d.ts +12 -0
- package/dist/core/load.d.ts +13 -11
- package/dist/core/metrics.d.ts +24 -0
- package/dist/core/ports.d.ts +99 -2
- package/dist/core/registry-reap.d.ts +22 -0
- package/dist/core/retention.d.ts +2 -1
- package/dist/core/rollback.d.ts +2 -1
- package/dist/driver-kit.d.ts +1 -0
- package/dist/driver-kit.js +100 -1
- package/dist/driver-kit.js.map +2 -2
- package/dist/drivers/_shared/object-registry.d.ts +19 -1
- package/dist/drivers/_shared/registry.d.ts +7 -9
- package/dist/export/index.d.ts +20 -8
- package/dist/index.d.ts +9 -3
- package/dist/index.js +527 -86
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/dist/chunk-LFUNPMXZ.js.map +0 -7
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type IAuditSink } from './audit.js';
|
|
2
2
|
import type { Yielder } from './cooperative.js';
|
|
3
3
|
import type { Clock, Rng } from './determinism.js';
|
|
4
|
+
import type { PublishedKept } from './generation-gc.js';
|
|
4
5
|
import type { Aead, CrbmCrypto, IKeystore, WrappedDek } from './crypto.js';
|
|
5
6
|
import type { ChunkRef, ChunkRead, ReadChunksOptions, StorageChunkSource, GenerationMetadata, GenerationSummary, GenKey, IStorageDriver, IRegistryDriver, RegistryRecord, RegistrySummary, SegmentRef, SegmentSize, Token } from './ports.js';
|
|
6
7
|
import { CrbmReader } from './crbm/reader.js';
|
|
@@ -78,6 +79,18 @@ export interface PinnedObject {
|
|
|
78
79
|
readonly version: string;
|
|
79
80
|
readonly fingerprint?: string;
|
|
80
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* What one resolution of a segment's pointer found: the target, and what its row says of the generation, each
|
|
84
|
+
* computed only when asked for. The key is unwrapped once however many ask, and a failure is not remembered, so a
|
|
85
|
+
* transient fault in the keystore is asked again.
|
|
86
|
+
*/
|
|
87
|
+
/** What a pin that leases its generation does between reading the row and opening the object. */
|
|
88
|
+
export interface PinLease {
|
|
89
|
+
/** Write the lease against the row the pin read: its token, or `'moved'` when the pointer is no longer at the generation. */
|
|
90
|
+
take(row: RegistryRecord): Promise<{
|
|
91
|
+
readonly token: Token;
|
|
92
|
+
} | 'moved'>;
|
|
93
|
+
}
|
|
81
94
|
export declare class CrbmStorageChunkSource implements StorageChunkSource {
|
|
82
95
|
private readonly driver;
|
|
83
96
|
/**
|
|
@@ -216,9 +229,27 @@ export declare class CrbmStorageChunkSource implements StorageChunkSource {
|
|
|
216
229
|
* and a `keep: 0` sweep can land in that gap, and nothing has been handed out yet, so the generation current
|
|
217
230
|
* once the swept one is gone is exactly what a pin taken now should hold. One retry, then propagate.
|
|
218
231
|
*/
|
|
219
|
-
pinGeneration(ref: SegmentRef): Promise<({
|
|
232
|
+
pinGeneration(ref: SegmentRef, lease?: PinLease): Promise<({
|
|
220
233
|
generation: number;
|
|
221
234
|
} & Required<PinnedObject>) | null>;
|
|
235
|
+
/**
|
|
236
|
+
* Open one named generation and verify it is the object `at.fingerprint` names, for a pin the caller reopens. One
|
|
237
|
+
* row read and one tail read with a registry (none of the row for a store without one): the open is the one a
|
|
238
|
+
* pinned read makes ({@link readerAt}), memoised under the version the pin will read by, so the pin's first read
|
|
239
|
+
* finds it open. A generation that is gone, above the row's pointer, on a destroyed or absent row, or another
|
|
240
|
+
* object than the fingerprint names, throws `NotFoundError`; nothing here reads empty.
|
|
241
|
+
*
|
|
242
|
+
* A generation counts as published while the pointer is at or above it. A rollback deletes nothing, so after one
|
|
243
|
+
* and a later load, a generation it rolled back from can be reopened while its object is still stored and the
|
|
244
|
+
* fingerprint matches. Like `pinGeneration`'s pinned reads, this opens the object without the live open's check
|
|
245
|
+
* of the row's summary against it, since that check is made where the row's own generation is read.
|
|
246
|
+
*/
|
|
247
|
+
pinGenerationAt(ref: SegmentRef, at: {
|
|
248
|
+
readonly generation: number;
|
|
249
|
+
readonly fingerprint: string;
|
|
250
|
+
}, lease?: PinLease): Promise<{
|
|
251
|
+
generation: number;
|
|
252
|
+
} & Required<PinnedObject>>;
|
|
222
253
|
private pinOnce;
|
|
223
254
|
/**
|
|
224
255
|
* Where a pinned reader of one object is memoised. With a registry that is its version, which names the object:
|
|
@@ -460,72 +491,17 @@ export declare function writeCrbmGenerationStream(driver: IStorageDriver, key: G
|
|
|
460
491
|
clock?: Yielder;
|
|
461
492
|
metadata?: GenerationMetadata;
|
|
462
493
|
}): Promise<StreamWriteResult>;
|
|
463
|
-
|
|
464
|
-
* Point a segment's registry `currentGen` at `key.generation` — the publish step that makes a freshly-written
|
|
465
|
-
* generation the authoritative latest (so registry-aware readers see it). **Forward-only and idempotent:**
|
|
466
|
-
* if the registry has no row it creates one; if it's already at/ahead of `key.generation` it's a no-op (an
|
|
467
|
-
* out-of-order/duplicate publish never regresses the pointer); otherwise it advances via compare-and-swap,
|
|
468
|
-
* retrying a few times under contention. Separated from the Storage write so callers can publish atomically
|
|
469
|
-
* after the immutable object is durable (write-then-publish).
|
|
470
|
-
*
|
|
471
|
-
* **Throws** rather than returning, in two cases, because each would otherwise create a state no reader can
|
|
472
|
-
* recover from: a `destroyed` (crypto-shredded) row, where advancing the pointer would name an object encrypted
|
|
473
|
-
* under a shredded key; and an advance carrying **new** `wrappedDeks`, which would either strand that key or make
|
|
474
|
-
* the row advertise encryption over cleartext generations. Contention that never clears throws
|
|
475
|
-
* {@link WriteConflictError} after the retries.
|
|
476
|
-
*
|
|
477
|
-
* **`expectFrom` turns forward-only into read-modify-write.** Forward-only is the right rule for a writer whose
|
|
478
|
-
* content does not depend on what was current — a load computes its ids upstream, so publishing over a newer
|
|
479
|
-
* generation loses nothing the loader knew about. It is the WRONG rule for a writer that *derived* its content
|
|
480
|
-
* from a particular generation: the erasure rewrite streams generation `from` and clears one bit, so publishing
|
|
481
|
-
* it over a newer generation silently discards whatever that generation added. Passing `expectFrom` makes the
|
|
482
|
-
* publish land only while the pointer is still exactly there, and return `false` otherwise, so the caller can
|
|
483
|
-
* report `superseded` and re-derive. The compare-and-swap itself carries the token read in the same iteration,
|
|
484
|
-
* so the check and the write see the same row.
|
|
485
|
-
*
|
|
486
|
-
* **`expectToken` fences the LINEAGE, and a derived writer needs both.** A generation number identifies a
|
|
487
|
-
* generation only within one incarnation of a name: the numbering restarts at `0` once the row is purged and
|
|
488
|
-
* the bucket empty, so a name that is retired and re-created has a *different* segment wearing the *same*
|
|
489
|
-
* `currentGen`. `expectFrom` alone matched it — and an erasure rewrite then published one incarnation's content
|
|
490
|
-
* over another's, deleted the live objects with its `keep: 0` collection, and returned `erased: true`. The row's
|
|
491
|
-
* OCC token is the identity that survives this: a shipped registry's later `create` gets a token never issued before
|
|
492
|
-
* under that name, but for a collision of probability 2^-128 per pair of incarnations, so no token is reused across them.
|
|
493
|
-
*
|
|
494
|
-
* Pass the token read alongside `expectFrom` and the publish lands only on the same row it was derived from.
|
|
495
|
-
* The check is deliberately **conservative**: the token also changes on writes that are not supersessions at
|
|
496
|
-
* all (a `setRetention`, a due-index reindex), so such a write makes a derived publish report `superseded` and
|
|
497
|
-
* the caller re-derive. That costs a re-run on a rare, unrelated write; the alternative costs a segment.
|
|
498
|
-
*
|
|
499
|
-
* **The driver sends a registry write once, and an unanswered one is reconciled by its effect.** A `create` or
|
|
500
|
-
* `compareAndSwap` that fails with {@link TransientError} (a throttle, a lost response, a timeout) may or may not
|
|
501
|
-
* have landed, and may still be in flight. The publish reads the row once more and decides from what it holds:
|
|
502
|
-
*
|
|
503
|
-
* - the pointer names `key.generation`, on the incarnation the write was made against (the row's `createdAt`), and
|
|
504
|
-
* `holdsOwnObject` proves the object under it the caller's: the write landed, and the publish returns `true`. Only
|
|
505
|
-
* the writer of a write-once object publishes its number, and a number is never taken while an object holds it;
|
|
506
|
-
* - the row is still the one the write was made against (the same token, or still no row): the write may land yet. The
|
|
507
|
-
* publish then waits, on the injected clock, and sends a **new** write from the row it just read: a request of its
|
|
508
|
-
* own, not a replay of the driver's, carrying the same version, so under the registry's fence at most one of the two
|
|
509
|
-
* lands and the other meets a conflict, which the next read settles. At most {@link UNANSWERED_RESENDS} follow, after
|
|
510
|
-
* a wait under 500 ms, then 1 s, then 2 s (full jitter when an `rng` is given); each is settled the same way. Still
|
|
511
|
-
* unanswered, or with no clock to wait on, this throws the registry's own `TransientError` and writes nothing more.
|
|
512
|
-
* A caller must not delete its object on it;
|
|
513
|
-
* - anything else: the row has moved past the state the write was conditioned on, so that write can never land, and
|
|
514
|
-
* the publish goes on as after a lost race, from the row it just read. A pointer at `key.generation` that is not over
|
|
515
|
-
* the caller's own object, or is on another incarnation, is a lost race too and answers `false`, never an already
|
|
516
|
-
* current re-publish.
|
|
517
|
-
*
|
|
518
|
-
* A failed read throws `TransientError` too. The same check of the pointer runs after a {@link WriteConflictError},
|
|
519
|
-
* so a write that landed and then met itself (a registry whose client re-sent it) is not taken for a lost race.
|
|
520
|
-
*
|
|
521
|
-
* **A `summary` goes in the same write that moves the pointer**, so a reader that sees this generation as current
|
|
522
|
-
* sees the count and metadata that describe it, and every attempt sends the same one. It must be the shape the row's
|
|
523
|
-
* keys call for: sealed for an object written with a key, clear for one written without.
|
|
524
|
-
*/
|
|
525
|
-
export declare function publishGeneration(registry: IRegistryDriver, key: GenKey, options?: {
|
|
494
|
+
export declare function publishGenerationKept(registry: IRegistryDriver, key: GenKey, options?: {
|
|
526
495
|
wrappedDeks?: readonly WrappedDek[];
|
|
527
496
|
expectFrom?: number;
|
|
528
497
|
expectToken?: Token;
|
|
498
|
+
/**
|
|
499
|
+
* The row `expectToken` names, as the caller read it. With it, a row whose token has moved but which differs from
|
|
500
|
+
* this one only in its leases (a reader took or released one) does not refuse the publish: it goes on against the
|
|
501
|
+
* fresh row, after a jittered wait and without the caller redoing its work, up to a bound. Any other difference
|
|
502
|
+
* refuses as ever. Without it, a moved token refuses.
|
|
503
|
+
*/
|
|
504
|
+
expectRow?: RegistryRecord | null;
|
|
529
505
|
/**
|
|
530
506
|
* Publish only while the segment still has **no registry row**.
|
|
531
507
|
*
|
|
@@ -584,7 +560,39 @@ export declare function publishGeneration(registry: IRegistryDriver, key: GenKey
|
|
|
584
560
|
* held its generation for a while before another writer moved on.
|
|
585
561
|
*/
|
|
586
562
|
onUnanswered?: () => void;
|
|
587
|
-
|
|
563
|
+
/**
|
|
564
|
+
* The clock that judges whether a lease has ended. With it, a publish over a row that holds ended leases drops
|
|
565
|
+
* them in the write that moves the pointer, which rewrites the row anyway. Without it, leases are left as they are.
|
|
566
|
+
*/
|
|
567
|
+
leasesNow?: () => number;
|
|
568
|
+
/**
|
|
569
|
+
* Clear the row's leases in the write that moves the pointer: the caller's collection deletes every generation
|
|
570
|
+
* below the new pointer whatever a lease says, as a subject erasure's does.
|
|
571
|
+
*/
|
|
572
|
+
clearLeases?: boolean;
|
|
573
|
+
/**
|
|
574
|
+
* How many generations below the new pointer the caller keeps. With it, the write that moves the pointer also
|
|
575
|
+
* records the window in the row (`keptGens`: the newest `keep` of the row's own list and the generation that was
|
|
576
|
+
* current), derived from the very row the write is made against, and the result names the generations the window
|
|
577
|
+
* dropped. Without it, or above the most a row records, the row records none, and a collection lists.
|
|
578
|
+
*/
|
|
579
|
+
keep?: number;
|
|
580
|
+
}): Promise<PublishResult>;
|
|
581
|
+
/** What a publish did: whether the pointer is at the generation, and what the write recorded in the row. */
|
|
582
|
+
export interface PublishResult {
|
|
583
|
+
readonly published: boolean;
|
|
584
|
+
/**
|
|
585
|
+
* What the write that moved the pointer recorded: the list, the generations the window dropped, and the new row's
|
|
586
|
+
* token. Absent when no write of this call is known to have landed (the pointer was already there, or the outcome
|
|
587
|
+
* was settled without knowing which write it was), and a collection then lists.
|
|
588
|
+
*/
|
|
589
|
+
readonly kept?: PublishedKept;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* {@link publishGenerationKept}'s answer to "did the pointer move", for a caller with no use for what it recorded:
|
|
593
|
+
* a publish with no `keep` leaves the row recording no list.
|
|
594
|
+
*/
|
|
595
|
+
export declare function publishGeneration(registry: IRegistryDriver, key: GenKey, options?: Parameters<typeof publishGenerationKept>[2]): Promise<boolean>;
|
|
588
596
|
/**
|
|
589
597
|
* Whether the object under `key` is the one `fingerprint` names, from one read of its footer: false when it is another
|
|
590
598
|
* object or gone. Unlike {@link holdsObject}, a read that cannot tell throws, for a caller that must not take "could
|
package/dist/core/engine.d.ts
CHANGED
|
@@ -166,6 +166,29 @@ export declare class SegmentEngine {
|
|
|
166
166
|
* the caller's to keep.
|
|
167
167
|
*/
|
|
168
168
|
iterateBatches(seg: SegmentRef, range?: IdRange): AsyncGenerator<Uint32Array>;
|
|
169
|
+
/**
|
|
170
|
+
* The ids at 1-based ranks `n`, `2n`, `3n` … counted over the ids in `range`, ascending. The rank of each boundary
|
|
171
|
+
* is placed from the index's per-chunk counts, so only the chunks that hold a boundary are read, each once however
|
|
172
|
+
* many boundaries it holds, through the same coalesced stream, window and budget unit as {@link iterate}. A range with
|
|
173
|
+
* `after` inside a chunk reads that chunk too, to count the ids at or below `after`. A last partial window yields
|
|
174
|
+
* nothing.
|
|
175
|
+
*
|
|
176
|
+
* The counts place ids, and are trusted as `count()` trusts them: a chunk that is read must decode to the size its
|
|
177
|
+
* index says, or the read throws {@link IntegrityError} (invariant 5), but a chunk that is not read is not checked, so
|
|
178
|
+
* an index that lies consistently about one (on an unencrypted object, the sum still matches the footer's total,
|
|
179
|
+
* which opening the object checks before any boundary is placed) shifts the ranks after it. An encrypted object's
|
|
180
|
+
* index is authenticated and bound to the object, so a wrong count there needs the key. The index and the chunks must come from one
|
|
181
|
+
* generation (invariant 3): the caller reads through a source that holds one, a pinned one, and a source with no
|
|
182
|
+
* per-chunk counts is refused.
|
|
183
|
+
*
|
|
184
|
+
* Nothing is fetched when the chunks in range hold fewer than `n` ids. The budget is charged before the first fetch
|
|
185
|
+
* with an upper bound, the cut chunk plus one chunk per boundary the counts allow (capped by the chunks in range),
|
|
186
|
+
* which can exceed the chunks actually read. A range that cuts its first chunk reads it even when the ids above
|
|
187
|
+
* `after` turn out to be fewer than `n`, and one that cuts its last chunk may read it and yield nothing.
|
|
188
|
+
*/
|
|
189
|
+
everyNth(seg: SegmentRef, n: number, range?: IdRange): AsyncGenerator<number>;
|
|
190
|
+
/** A chunk's remainders ascending, once its size is confirmed against the index's count for it (invariant 5). */
|
|
191
|
+
private chunkRemainders;
|
|
169
192
|
/**
|
|
170
193
|
* `segs[0] ∩ segs[1] ∩ …`, minus every id in `options.exclude` — streamed ascending.
|
|
171
194
|
*
|
package/dist/core/erasure.d.ts
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* operation — `eraseIdFromSegment` rewrites the generation without it.
|
|
14
14
|
*/
|
|
15
15
|
import { type IAuditSink } from './audit.js';
|
|
16
|
+
import { type ChurnDeps } from './leases.js';
|
|
16
17
|
import type { IStorageDriver, IRegistryDriver, SegmentRef } from './ports.js';
|
|
17
|
-
export interface EraseDeps {
|
|
18
|
+
export interface EraseDeps extends ChurnDeps {
|
|
18
19
|
readonly registry: IRegistryDriver;
|
|
19
20
|
}
|
|
20
21
|
export interface DestroyResult {
|
package/dist/core/errors.d.ts
CHANGED
|
@@ -42,6 +42,25 @@ export declare class IntegrityError extends CloudRoaringError {
|
|
|
42
42
|
*/
|
|
43
43
|
export declare class NotFoundError extends CloudRoaringError {
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* A read of a leased pin came after its lease ended, by `until` or by `release()`. Thrown at every read site of the
|
|
47
|
+
* handle, including a leased handle used as an operand or an exclude of a combine, and never answered empty: a
|
|
48
|
+
* handle that read empty here would be an opt-out list that suppresses nobody. Carries the instant the lease ended
|
|
49
|
+
* and why; never an id or a key.
|
|
50
|
+
*/
|
|
51
|
+
export declare class LeaseExpiredError extends CloudRoaringError {
|
|
52
|
+
/** Epoch-ms the lease was taken until. */
|
|
53
|
+
readonly until: number;
|
|
54
|
+
/** `'expired'` when the clock reached `until`, `'released'` after `release()`. */
|
|
55
|
+
readonly reason: 'expired' | 'released';
|
|
56
|
+
constructor(message: string, until: number, reason: 'expired' | 'released');
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A segment already has as many live leases as one row records, so another cannot be taken. Nothing was written.
|
|
60
|
+
* Release one, wait for one to end, or share one lease across the tasks of a job.
|
|
61
|
+
*/
|
|
62
|
+
export declare class LeaseLimitError extends CloudRoaringError {
|
|
63
|
+
}
|
|
45
64
|
/**
|
|
46
65
|
* This build/configuration cannot perform the requested operation, though nothing is malformed. Two uses:
|
|
47
66
|
* (1) **format** — the bytes are well-formed but unreadable here (an unknown `.crbm` major version) —
|
|
@@ -104,6 +123,10 @@ export declare function isCloudRoaringError(err: unknown): err is CloudRoaringEr
|
|
|
104
123
|
export declare function isWriteConflictError(err: unknown): err is WriteConflictError;
|
|
105
124
|
/** A retryable transient infrastructure fault. The retry layer keys on this. */
|
|
106
125
|
export declare function isTransientError(err: unknown): err is TransientError;
|
|
126
|
+
/** A read came after its pin's lease ended. */
|
|
127
|
+
export declare function isLeaseExpiredError(err: unknown): err is LeaseExpiredError;
|
|
128
|
+
/** A segment has no room for another live lease. */
|
|
129
|
+
export declare function isLeaseLimitError(err: unknown): err is LeaseLimitError;
|
|
107
130
|
/** A requested object/row does not exist. */
|
|
108
131
|
export declare function isNotFoundError(err: unknown): err is NotFoundError;
|
|
109
132
|
/** Corrupt/oversized/failed-checksum bytes from a tier. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GenKey, IStorageDriver, IRegistryDriver, RegistryRecord, SegmentRef } from './ports.js';
|
|
1
|
+
import type { GenKey, IStorageDriver, IRegistryDriver, RegistryRecord, SegmentRef, Token } from './ports.js';
|
|
2
2
|
/** The two ports generation bookkeeping needs: the objects, and the pointer that says which one is current. */
|
|
3
3
|
export interface GenerationDeps {
|
|
4
4
|
readonly storage: IStorageDriver;
|
|
@@ -51,9 +51,9 @@ export interface LoadNumber {
|
|
|
51
51
|
export declare function nextLoadGeneration(ref: SegmentRef, deps: GenerationDeps, row: RegistryRecord | null): Promise<LoadNumber>;
|
|
52
52
|
/**
|
|
53
53
|
* How often a load that could collect by name lists instead: every generation divisible by this. A name-only pass
|
|
54
|
-
* takes
|
|
55
|
-
*
|
|
56
|
-
* stranded below the pointer) waits for a listing. The number is a generation's, not a clock's, so the choice needs
|
|
54
|
+
* takes the names its publish pushed out of the row's window and never looks at the rest, so what it misses (a crashed
|
|
55
|
+
* or refused load's object, a delete that failed, extras a fault left after a `keep` that shrank, an object a rollback
|
|
56
|
+
* or an erasure left stranded below the pointer) waits for a listing. The number is a generation's, not a clock's, so the choice needs
|
|
57
57
|
* no state and no time, and a segment that loads cleanly lists once in this many loads at most. The bound is in
|
|
58
58
|
* generation numbers: a rollback moves the pointer down, and the loads that then take the numbers above it count
|
|
59
59
|
* from there.
|
|
@@ -97,58 +97,82 @@ export declare function gcOrphanGenerations(ref: SegmentRef, deps: GenerationDep
|
|
|
97
97
|
keep?: number;
|
|
98
98
|
}): Promise<number[]>;
|
|
99
99
|
/**
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
100
|
+
* What makes a collection spare leased generations: the clock that says whether a lease has ended. Only a load's
|
|
101
|
+
* collection passes one, and a load with no clock passes none: it cannot tell a live lease from an ended one, so it
|
|
102
|
+
* reads none. A pass without a guard never reads the row's leases, which is what keeps erasure, shred and drop deleting
|
|
103
|
+
* what a lease names.
|
|
104
|
+
*/
|
|
105
|
+
export interface LeaseGuard {
|
|
106
|
+
readonly now: () => number;
|
|
107
|
+
}
|
|
108
|
+
/** What a load's publish recorded in the row, as `publishGeneration` reports it. */
|
|
109
|
+
export interface PublishedKept {
|
|
110
|
+
/** The list the publish wrote, or `undefined` when it wrote none (the row then does not know which are kept). */
|
|
111
|
+
readonly list: readonly number[] | undefined;
|
|
112
|
+
/** The names the publish pushed out of the window, ascending. */
|
|
113
|
+
readonly evict: readonly number[];
|
|
114
|
+
/** The token of the row the publish wrote, when it is known: a write settled by reading the row has none. */
|
|
115
|
+
readonly token: Token | undefined;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Collect after a load's publish. The row records the generations the load keeps, so the pass deletes by name what the
|
|
119
|
+
* publish pushed out of that window ({@link deleteEvicted}) and lists the segment ({@link gcOrphanGenerations}'s
|
|
120
|
+
* pass) only when it cannot rely on the row or must catch what a name does not:
|
|
121
|
+
*
|
|
122
|
+
* - the row recorded no list (a `keep` above {@link MAX_KEPT_GENERATIONS}, a row an earlier schema wrote, one a
|
|
123
|
+
* rollback or an erasure moved, or a publish whose write was settled by reading the row): the pass keeps the
|
|
124
|
+
* newest `keep` generations present below the pointer, and records them with one compare-and-swap, fenced on the
|
|
125
|
+
* publish's own token, so the next load collects by name;
|
|
126
|
+
* - the guard found the current generation's object gone, or, for a `keep` of 1 or more when the guard took the size
|
|
127
|
+
* from the row's summary and opened nothing, one zero-byte read of it does not find it (`proveCurrent`): the
|
|
128
|
+
* window then counts a generation that is not there, so the same pass keeps what is;
|
|
129
|
+
* - a number the listing chose (something sits above the pointer, or the existence check could not answer), a
|
|
130
|
+
* caller that asks for a listing, and every {@link LIST_COLLECTION_CADENCE}th generation: the pass deletes every
|
|
131
|
+
* generation below the pointer the row does not name, which collects what a name-only pass leaves.
|
|
132
|
+
*
|
|
133
|
+
* It makes no request when `keep` is at least the generation published, since no more generations than that exist
|
|
134
|
+
* below it. A load onto a `destroyed` row is refused at its publish and never reaches here; a drop that lands after
|
|
135
|
+
* the publish leaves this pass collecting the tombstone's generations, and the drop's own sweep takes the rest.
|
|
136
|
+
*
|
|
137
|
+
* The seed's compare-and-swap moves the row's token, so a derived writer in flight on the row (an erasure rewrite)
|
|
138
|
+
* meets a lost fence and re-derives: it happens once per row that recorded no list. A `WriteConflictError` from it is
|
|
139
|
+
* a lost race, and the load took effect, so it is not raised; the next load records the list. Any other fault is
|
|
140
|
+
* raised, as every fault of a collection after a landed publish is.
|
|
111
141
|
*/
|
|
112
142
|
export declare function collectAfterLoad(ref: SegmentRef, deps: GenerationDeps, options: {
|
|
113
143
|
generation: number;
|
|
114
144
|
keep: number;
|
|
145
|
+
/** The load's number was proved free by one existence check, and no caller asked for a listing. */
|
|
115
146
|
byName: boolean;
|
|
147
|
+
/** The guard opened the current generation's object and found it gone. */
|
|
148
|
+
currentGone?: boolean;
|
|
149
|
+
kept: PublishedKept | undefined;
|
|
116
150
|
/**
|
|
117
|
-
* The object the load's row named as current, which the caller did not open. Before it collects by name
|
|
118
|
-
*
|
|
119
|
-
*
|
|
151
|
+
* The object the load's row named as current, which the caller did not open. Before it collects by name, the pass
|
|
152
|
+
* looks for this object with one zero-byte read, and lists instead unless that finds it: a name-only pass is
|
|
153
|
+
* safe only while the object the row named is in the bucket.
|
|
120
154
|
*/
|
|
121
155
|
proveCurrent?: GenKey;
|
|
156
|
+
/** Spare the generations the row's live leases hold. Absent, the pass ignores leases. */
|
|
157
|
+
leases?: LeaseGuard;
|
|
122
158
|
}): Promise<number[]>;
|
|
123
159
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
160
|
+
* Delete, with no listing, the generations a publish of `generation` pushed out of its row's window, `evict`, taken
|
|
161
|
+
* from the list the publish was written against. Returns the names asked for, whether or not an object was there: a
|
|
162
|
+
* delete of an absent object succeeds on every backend and says nothing, so a generation a concurrent pass, an erasure
|
|
163
|
+
* or a lifecycle rule had already taken is listed all the same. A list is not a receipt (invariant 4), and neither is
|
|
164
|
+
* this.
|
|
129
165
|
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* `
|
|
136
|
-
*
|
|
137
|
-
* it (see {@link collectAfterLoad}), lists instead; a load that made neither (`allowEmpty` without `minRetained`)
|
|
138
|
-
* cannot tell, and deletes by name the older generation a listing would have kept as the window. `keep` of 2 or more is refused,
|
|
139
|
-
* because there the window counts the generations that exist, which a name cannot know.
|
|
140
|
-
*
|
|
141
|
-
* Collection never touches the current generation, and the row is re-proved before the delete as every pass does.
|
|
142
|
-
* A row that is gone, or a pointer that has fallen below `generation` (a rollback, or a name purged and re-created
|
|
143
|
-
* with fewer loads), deletes nothing and returns `[]`: the publish already landed, nothing here protects it or
|
|
144
|
-
* needs the result, and a throw would hand the caller a failure for a load that took effect. A pointer that has
|
|
145
|
-
* moved above `generation` does not stop it: a publish landing in the meantime leaves routine collection working.
|
|
146
|
-
* A fault, a registry read or a delete that throws, still propagates, with the pointer at `generation`. The bound
|
|
147
|
-
* is `generation`, the pointer this load's own publish set, so the lower-of-two-pointers rule of a listing pass
|
|
148
|
-
* holds with one observation in place of two. Nothing is deleted when `generation` has nothing below its window,
|
|
149
|
-
* and no request is made.
|
|
166
|
+
* Each delete re-reads the row first (invariant 4) and stops the pass, deleting nothing more and raising nothing, unless
|
|
167
|
+
* the row still has a pointer at or above `generation` (a rollback, or a name purged and re-created with fewer loads,
|
|
168
|
+
* has moved it below: the publish already landed, nothing here protects it or needs the result), the name is below
|
|
169
|
+
* that pointer, and the row's own list is usable and does not name it. The last condition is what keeps a late delete
|
|
170
|
+
* from taking a generation that a later publish, after a rollback, put in the window. A pointer that has moved above
|
|
171
|
+
* `generation` does not stop it: a publish landing in the meantime leaves routine collection working. A fault, a
|
|
172
|
+
* registry read or a delete that throws, propagates, with the pointer at `generation`.
|
|
150
173
|
*/
|
|
151
|
-
export declare function
|
|
174
|
+
export declare function deleteEvicted(ref: SegmentRef, deps: GenerationDeps, options: {
|
|
152
175
|
generation: number;
|
|
153
|
-
|
|
176
|
+
evict: readonly number[];
|
|
177
|
+
leases?: LeaseGuard;
|
|
154
178
|
}): Promise<number[]>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generations a segment's row records as kept: the grace window below its pointer, so a load collects what its
|
|
3
|
+
* publish pushed out of the window by name instead of listing the bucket.
|
|
4
|
+
*
|
|
5
|
+
* The record is a cache of what a listing would keep, never an inventory of the bucket. Absent means the row does not
|
|
6
|
+
* know, and the load lists; an empty list means nothing below the pointer is kept. A patch that moves the pointer
|
|
7
|
+
* without naming the list drops it (see `applyRegistryPatch`), so a writer that forgets the list leaves a row that
|
|
8
|
+
* lists, never one that deletes a generation it should keep. Pure: no I/O, time or randomness.
|
|
9
|
+
*/
|
|
10
|
+
import type { RegistryRecord } from './ports.js';
|
|
11
|
+
/**
|
|
12
|
+
* The most generations a writer records. A `keep` above it records none, and the load lists on every publish. A
|
|
13
|
+
* reader accepts up to {@link MAX_STORED_KEPT_GENERATIONS}, so a release can write more than this without a new row
|
|
14
|
+
* schema.
|
|
15
|
+
*/
|
|
16
|
+
export declare const MAX_KEPT_GENERATIONS = 64;
|
|
17
|
+
/** The most entries a stored row may hold: four times what a writer writes. */
|
|
18
|
+
export declare const MAX_STORED_KEPT_GENERATIONS = 256;
|
|
19
|
+
/**
|
|
20
|
+
* The row's list when a collection may rely on it: present, on a row with a pointer, and every entry below that
|
|
21
|
+
* pointer. A well-formed list that disagrees with the pointer is read but not used (the load lists instead), as a
|
|
22
|
+
* summary that names another generation is.
|
|
23
|
+
*/
|
|
24
|
+
export declare function usableKeptGens(record: RegistryRecord | null): readonly number[] | undefined;
|
|
25
|
+
/** What a publish of a generation writes into the row, and which names it pushes out of the window. */
|
|
26
|
+
export interface KeptAfter {
|
|
27
|
+
/** The list to write, or `undefined` to write none (the row then does not know, and the next load lists). */
|
|
28
|
+
readonly list: readonly number[] | undefined;
|
|
29
|
+
/** The generations the window no longer keeps, ascending: the names to delete. */
|
|
30
|
+
readonly evict: readonly number[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The list a publish of `generation` over `prev` writes: the newest `keep` of the row's list and the generation that
|
|
34
|
+
* was current, and the names that fell out of it. Only generations that were current are named, so a number a refused
|
|
35
|
+
* or crashed load took is never in it.
|
|
36
|
+
*
|
|
37
|
+
* No list is written when the row has no usable list to extend (except for generation 0, which has nothing below it),
|
|
38
|
+
* when `keep` is above {@link MAX_KEPT_GENERATIONS}, or when no `keep` is given.
|
|
39
|
+
*/
|
|
40
|
+
export declare function nextKept(prev: RegistryRecord | null, generation: number, keep: number | undefined): KeptAfter;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { Clock, Rng } from './determinism.js';
|
|
2
|
+
import type { IRegistryDriver, LeaseEntry, RegistryRecord, SegmentRef, Token } from './ports.js';
|
|
3
|
+
/** The longest lease: a `leaseUntil` further than this from the clock is refused. */
|
|
4
|
+
export declare const MAX_LEASE_MS: number;
|
|
5
|
+
/**
|
|
6
|
+
* How much longer than `until` a collector holds a lease, and the most a collector will believe a lease runs past
|
|
7
|
+
* its own clock. It is the tolerated difference between the holder's clock and the collector's, in either direction.
|
|
8
|
+
*/
|
|
9
|
+
export declare const LEASE_SKEW_MS = 60000;
|
|
10
|
+
/** The most live leases a writer records on one segment; the next one is refused with {@link LeaseLimitError}. */
|
|
11
|
+
export declare const MAX_LEASES_PER_SEGMENT = 64;
|
|
12
|
+
/** The most entries a stored row may hold: four times what a writer writes, so a release can raise the cap. */
|
|
13
|
+
export declare const MAX_STORED_LEASES = 256;
|
|
14
|
+
/**
|
|
15
|
+
* Read-modify-write attempts of one take or release before it reports contention. Takers of one row serialise: each
|
|
16
|
+
* round exactly one write lands, so the slowest of `k` simultaneous takers needs `k` attempts. The bound is the cap
|
|
17
|
+
* plus a few, so that the most a segment can hold, all arriving at once, can all land.
|
|
18
|
+
*/
|
|
19
|
+
export declare const LEASE_CAS_ATTEMPTS: number;
|
|
20
|
+
/**
|
|
21
|
+
* Whether an entry holds its generation against a collector reading `now`: it has not ended (`until` plus the margin
|
|
22
|
+
* is ahead), and it does not run further past `now` than any honest holder with a clock inside the margin could have
|
|
23
|
+
* written (`until` at most the longest lease plus the margin ahead). The second clause bounds a corrupt or forged
|
|
24
|
+
* `until` without refusing the row: an entry past it is read and not honoured.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isLive(entry: LeaseEntry, now: number): boolean;
|
|
27
|
+
/** The generations a collector reading `now` must spare, from the row it holds. */
|
|
28
|
+
export declare function heldGenerations(record: RegistryRecord | null, now: number): ReadonlySet<number>;
|
|
29
|
+
/** The entries of a row that are live at `now`, in the row's order. */
|
|
30
|
+
export declare function liveLeases(record: RegistryRecord | null, now: number): readonly LeaseEntry[];
|
|
31
|
+
/**
|
|
32
|
+
* What a writer that fences on the row's token needs to wait out lease churn: a sleep, and the rng that spreads it.
|
|
33
|
+
* Both are optional; without a sleep a retry follows at once.
|
|
34
|
+
*/
|
|
35
|
+
export interface ChurnDeps {
|
|
36
|
+
readonly clock?: {
|
|
37
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
38
|
+
} | undefined;
|
|
39
|
+
readonly rng?: Rng | undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* How many times one writer retries after meeting a row that differs from the one it read only in its leases. Takers of
|
|
43
|
+
* one row serialise: each round exactly one write lands, so a writer racing `k` holders needs up to `k + 1` rounds, and a
|
|
44
|
+
* holder can take and release inside one job, so the bound covers a take and a release by every holder the row can
|
|
45
|
+
* hold, with a few to spare: 2 * {@link MAX_LEASES_PER_SEGMENT} + 8. At waits of up to 25 ms growing to up to 400 ms it is a
|
|
46
|
+
* minute at the very most before the writer reports the conflict it had, and a lease writer that goes on beyond
|
|
47
|
+
* that has stopped being a lease writer and become a flood.
|
|
48
|
+
*/
|
|
49
|
+
export declare const LEASE_ONLY_RETRIES: number;
|
|
50
|
+
/**
|
|
51
|
+
* Whether `now` is `held` with nothing changed but its leases: the same incarnation, and every other field equal,
|
|
52
|
+
* `updatedAt` and the token (which every write moves) aside. `alsoIgnore` names fields the caller's own earlier write
|
|
53
|
+
* changed, so a writer comparing against a row it has itself written does not take its own change for another's.
|
|
54
|
+
*
|
|
55
|
+
* This is the one test that lets a writer fenced on the row's token go on after a lease write: a lease is written by
|
|
56
|
+
* readers, in numbers no operator controls, and it says nothing about the content the writer derived, the pointer it
|
|
57
|
+
* judged or the key it holds. Any other difference is another writer's, and the fence refuses as it always has.
|
|
58
|
+
*/
|
|
59
|
+
export declare function onlyLeasesDiffer(held: RegistryRecord, now: RegistryRecord, alsoIgnore?: readonly (keyof RegistryRecord)[]): boolean;
|
|
60
|
+
/** A writer's count of the lease-only changes it has waited out, and the wait. One per call of the writer. */
|
|
61
|
+
export interface LeaseChurn {
|
|
62
|
+
/**
|
|
63
|
+
* When `now` is `held` changed only in its leases and the bound is not spent: wait, then read the row again with
|
|
64
|
+
* `reread`, and return what it found. The wait comes before the read, so the writer's next write is made against a row
|
|
65
|
+
* as fresh as one round trip, not one as stale as the wait. The caller checks that row against `held` once more (a
|
|
66
|
+
* write may have landed during the wait) and goes on against it, without redoing its work. `undefined` otherwise, and
|
|
67
|
+
* the caller does what it always did.
|
|
68
|
+
*
|
|
69
|
+
* The wait is uniform in `[0, bound)`, the bound starting at 25 ms and doubling to 400 ms, taken on the injected
|
|
70
|
+
* clock's `sleep` and spread by its `rng`. With no `sleep` there is no wait, and the retries follow one another at
|
|
71
|
+
* once; with no `rng` the wait is exactly the bound, so contenders stay in step. The store always supplies both.
|
|
72
|
+
*/
|
|
73
|
+
settle(held: RegistryRecord | null | undefined, now: RegistryRecord | null, reread: () => Promise<RegistryRecord | null>, alsoIgnore?: readonly (keyof RegistryRecord)[]): Promise<{
|
|
74
|
+
readonly row: RegistryRecord | null;
|
|
75
|
+
} | undefined>;
|
|
76
|
+
}
|
|
77
|
+
export declare function leaseChurn(deps: ChurnDeps): LeaseChurn;
|
|
78
|
+
/** What the lease writers need: the registry, the clock that judges liveness and waits, and the rng that spreads waits. */
|
|
79
|
+
export interface LeaseDeps {
|
|
80
|
+
readonly registry: IRegistryDriver;
|
|
81
|
+
readonly clock: Clock;
|
|
82
|
+
readonly rng?: Rng;
|
|
83
|
+
}
|
|
84
|
+
/** What a take asks for. */
|
|
85
|
+
export interface LeaseTake {
|
|
86
|
+
/** Drawn once per pin call, outside any retry, so a write that landed unseen is found again and never duplicated. */
|
|
87
|
+
readonly holder: string;
|
|
88
|
+
/** The generation to hold. */
|
|
89
|
+
readonly generation: number;
|
|
90
|
+
/** Epoch-ms, already checked against the caller's clock. */
|
|
91
|
+
readonly until: number;
|
|
92
|
+
/** The row the caller read; the first write is made against it, with no row read of its own. */
|
|
93
|
+
readonly row: RegistryRecord;
|
|
94
|
+
/** The generation must still be the row's pointer on every attempt: a pin of the current generation. */
|
|
95
|
+
readonly current: boolean;
|
|
96
|
+
}
|
|
97
|
+
/** A lease that landed: the token its write returned, and the row that write was made against. */
|
|
98
|
+
export interface TakenLease {
|
|
99
|
+
readonly token: Token;
|
|
100
|
+
readonly row: RegistryRecord;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Write a lease entry for `take.holder`, fenced on the row it was made against, so a lease that lands proves the row
|
|
104
|
+
* was unchanged since the caller read it. Returns `'moved'` when `take.current` and the pointer is no longer at the
|
|
105
|
+
* generation (the caller starts over from a fresh read). Throws {@link NotFoundError} for a row that is gone,
|
|
106
|
+
* destroyed or has no such published generation, {@link LeaseLimitError} when the segment already has
|
|
107
|
+
* {@link MAX_LEASES_PER_SEGMENT} live leases (nothing is written), `WriteConflictError` after
|
|
108
|
+
* {@link LEASE_CAS_ATTEMPTS} lost races, and the registry's own `TransientError` for a write that stayed unanswered.
|
|
109
|
+
*
|
|
110
|
+
* Idempotent by holder: an entry for the holder is replaced, so a retry of a whole pin re-takes without duplicating,
|
|
111
|
+
* and a write that got no answer is settled by reading the row for the holder's entry.
|
|
112
|
+
*/
|
|
113
|
+
export declare function takeLease(ref: SegmentRef, deps: LeaseDeps, take: LeaseTake): Promise<TakenLease | 'moved'>;
|
|
114
|
+
/**
|
|
115
|
+
* Remove `holder`'s entry, and every entry that has ended, in one write. Idempotent: a row that is gone, destroyed or
|
|
116
|
+
* has no entry for the holder is already released. Throws `WriteConflictError` after {@link LEASE_CAS_ATTEMPTS} lost
|
|
117
|
+
* races and the registry's `TransientError` for a write that stayed unanswered over an unchanged row.
|
|
118
|
+
*/
|
|
119
|
+
export declare function releaseLease(ref: SegmentRef, deps: LeaseDeps, holder: string): Promise<void>;
|
|
@@ -62,3 +62,15 @@ export declare class ChunkLoadInput {
|
|
|
62
62
|
* and anything that is none of the three inputs. An error the caller's own `serialize` throws propagates as it is.
|
|
63
63
|
*/
|
|
64
64
|
export declare function prepareLoadInput(input: LoadInput, codec: CodecInterface): Iterable<number> | AsyncIterable<number> | DecodedLoadInput;
|
|
65
|
+
/**
|
|
66
|
+
* Portable bytes a caller holds, checked and decoded by the codec's safe deserializer: the one check every bitmap
|
|
67
|
+
* input of a load goes through, and what a flavor offers its users to decode bytes they hold themselves.
|
|
68
|
+
*
|
|
69
|
+
* The bytes must be a `Uint8Array` (a Node `Buffer` is one), at most {@link MAX_SERIALIZED_LOAD_BYTES}, one whole
|
|
70
|
+
* bitmap and structurally sound. A view is read over the bytes it really holds, and bytes in a `SharedArrayBuffer`
|
|
71
|
+
* are copied first. An empty or detached buffer is the empty bitmap.
|
|
72
|
+
*
|
|
73
|
+
* @param what names the input in a refusal's message.
|
|
74
|
+
* @throws {ValidationError} for anything that is not such bytes; nothing reaches the codec's native decoder then.
|
|
75
|
+
*/
|
|
76
|
+
export declare function decodeSerialized(bytes: unknown, codec: CodecInterface, what?: string): CodecBitmap;
|