@cloudbitmaps/core 0.15.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-B7IGS3W6.js → chunk-CV7GJO3B.js} +363 -23
- package/dist/chunk-CV7GJO3B.js.map +7 -0
- package/dist/core/codec.d.ts +10 -0
- package/dist/core/cost.d.ts +4 -4
- package/dist/core/crbm-storage-source.d.ts +76 -68
- package/dist/core/engine.d.ts +59 -6
- 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 +21 -0
- package/dist/core/load.d.ts +33 -12
- package/dist/core/metrics.d.ts +24 -0
- package/dist/core/ports.d.ts +128 -6
- 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 +142 -10
- package/dist/driver-kit.js.map +2 -2
- package/dist/drivers/_shared/object-registry.d.ts +34 -3
- package/dist/drivers/_shared/registry.d.ts +7 -9
- package/dist/drivers/localfs/registry.d.ts +4 -0
- package/dist/drivers/memory.d.ts +4 -0
- package/dist/export/index.d.ts +20 -8
- package/dist/index.d.ts +10 -4
- package/dist/index.js +745 -123
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/dist/chunk-B7IGS3W6.js.map +0 -7
|
@@ -1,12 +1,13 @@
|
|
|
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';
|
|
7
8
|
import type { CrbmReaderOptions } from './crbm/reader.js';
|
|
8
9
|
import type { CodecBitmap, CodecInterface } from './codec.js';
|
|
9
|
-
import { DecodedLoadInput } from './load-input.js';
|
|
10
|
+
import { ChunkLoadInput, DecodedLoadInput } from './load-input.js';
|
|
10
11
|
export interface CrbmStorageChunkSourceOptions extends CrbmReaderOptions {
|
|
11
12
|
/**
|
|
12
13
|
* Optional {@link IRegistryDriver}. When provided, the current generation is resolved via the registry's
|
|
@@ -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
|
|
@@ -655,7 +663,7 @@ export interface BulkLoadResult {
|
|
|
655
663
|
* {@link WriteConflictError} (write-once). Without a registry a `StorageChunkSource` serves the **highest**
|
|
656
664
|
* generation present, so a too-high number silently shadows real data; with one, `publishGeneration` decides.
|
|
657
665
|
*/
|
|
658
|
-
export declare function bulkLoadCrbmGeneration(driver: IStorageDriver, key: GenKey, ids: Iterable<number> | AsyncIterable<number> | DecodedLoadInput, options?: {
|
|
666
|
+
export declare function bulkLoadCrbmGeneration(driver: IStorageDriver, key: GenKey, ids: Iterable<number> | AsyncIterable<number> | DecodedLoadInput | ChunkLoadInput, options?: {
|
|
659
667
|
registry?: IRegistryDriver;
|
|
660
668
|
keystore?: IKeystore;
|
|
661
669
|
requireEncryption?: boolean;
|
|
@@ -724,7 +732,7 @@ export interface LoadAhead {
|
|
|
724
732
|
* {@link bulkLoadCrbmGeneration} for a load whose number, and perhaps its key, are still being fetched: the same
|
|
725
733
|
* options, with the same meaning, and the same writes. The number is joined only where the object is written.
|
|
726
734
|
*/
|
|
727
|
-
export declare function bulkLoadAhead(driver: IStorageDriver, ref: SegmentRef, ahead: LoadAhead, ids:
|
|
735
|
+
export declare function bulkLoadAhead(driver: IStorageDriver, ref: SegmentRef, ahead: LoadAhead, ids: Parameters<typeof bulkLoadCrbmGeneration>[2], options?: NonNullable<Parameters<typeof bulkLoadCrbmGeneration>[3]>): Promise<BulkLoadResult>;
|
|
728
736
|
/**
|
|
729
737
|
* Open a {@link CrbmReader} on one generation over the storage driver's range/tail reads (decrypting if `crypto`).
|
|
730
738
|
* The reader the write paths use to re-read what they wrote, and the erasure rewrite uses to stream the old
|
package/dist/core/engine.d.ts
CHANGED
|
@@ -78,6 +78,11 @@ export interface CombineOptions extends IdRange {
|
|
|
78
78
|
*/
|
|
79
79
|
readonly allowAbsentOperands?: boolean;
|
|
80
80
|
}
|
|
81
|
+
/** One chunk of a combine's result: the bitmap of its remainders, owned by whoever reads it. */
|
|
82
|
+
export interface CombinedChunk {
|
|
83
|
+
readonly chunkKey: number;
|
|
84
|
+
readonly bitmap: CodecBitmap;
|
|
85
|
+
}
|
|
81
86
|
export declare class SegmentEngine {
|
|
82
87
|
private readonly storage;
|
|
83
88
|
private readonly cache;
|
|
@@ -99,6 +104,8 @@ export declare class SegmentEngine {
|
|
|
99
104
|
* they deliver, as a read whose entry an invalidation dropped does not (streams hold no `openReads` entry).
|
|
100
105
|
*/
|
|
101
106
|
private readonly openStreams;
|
|
107
|
+
/** How many times {@link invalidate} has been called: a read compares it with the count it began under. */
|
|
108
|
+
private invalidations;
|
|
102
109
|
constructor(deps: EngineDeps);
|
|
103
110
|
/** Membership: one chunk lookup — the cache, else one Storage fetch of that chunk. */
|
|
104
111
|
has(seg: SegmentRef, id: number): Promise<boolean>;
|
|
@@ -159,6 +166,29 @@ export declare class SegmentEngine {
|
|
|
159
166
|
* the caller's to keep.
|
|
160
167
|
*/
|
|
161
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;
|
|
162
192
|
/**
|
|
163
193
|
* `segs[0] ∩ segs[1] ∩ …`, minus every id in `options.exclude` — streamed ascending.
|
|
164
194
|
*
|
|
@@ -215,6 +245,20 @@ export declare class SegmentEngine {
|
|
|
215
245
|
unionBatches(segs: readonly SegmentRef[], options?: CombineOptions): AsyncGenerator<Uint32Array>;
|
|
216
246
|
/** {@link andNot}, one `Uint32Array` per chunk — see {@link iterateBatches}. */
|
|
217
247
|
andNotBatches(seg: SegmentRef, excludes: readonly SegmentRef[], options?: Omit<CombineOptions, 'exclude'>): AsyncGenerator<Uint32Array>;
|
|
248
|
+
/**
|
|
249
|
+
* {@link intersect}, as the chunks the result is made of: `{ chunkKey, bitmap }` in ascending key order, each a
|
|
250
|
+
* non-empty bitmap of the chunk's 16-bit remainders, cut to the read's range. What a materialisation writes
|
|
251
|
+
* straight into its new generation, with no id built on the way.
|
|
252
|
+
*
|
|
253
|
+
* **Each bitmap belongs to the caller, who may change it:** it is the combine's own accumulator or a bitmap built
|
|
254
|
+
* for the range's edge, never a chunk the cache holds or another operand's. The reads, the budget, the metric and the
|
|
255
|
+
* errors are the id reads' own.
|
|
256
|
+
*/
|
|
257
|
+
intersectChunks(segs: readonly SegmentRef[], options?: CombineOptions): AsyncGenerator<CombinedChunk>;
|
|
258
|
+
/** {@link union}, as chunks: see {@link intersectChunks}. */
|
|
259
|
+
unionChunks(segs: readonly SegmentRef[], options?: CombineOptions): AsyncGenerator<CombinedChunk>;
|
|
260
|
+
/** {@link andNot}, as chunks: see {@link intersectChunks}. */
|
|
261
|
+
andNotChunks(seg: SegmentRef, excludes: readonly SegmentRef[], options?: Omit<CombineOptions, 'exclude'>): AsyncGenerator<CombinedChunk>;
|
|
218
262
|
/**
|
|
219
263
|
* Everything a combine decides before it reads a chunk: validation, each operand's keys and generation, the
|
|
220
264
|
* absent-operand refusal, key alignment, the budget and the metric. `null` when the range is empty. The per-id
|
|
@@ -223,6 +267,10 @@ export declare class SegmentEngine {
|
|
|
223
267
|
private combinePlan;
|
|
224
268
|
private combine;
|
|
225
269
|
private combineBatches;
|
|
270
|
+
/** The chunk read: the plan every combine shares, then each key's result, cut to the range at its edges. */
|
|
271
|
+
private combineChunks;
|
|
272
|
+
/** An edge chunk's bitmap cut to the window, as a new bitmap: the remainders `edgeIds` would yield. */
|
|
273
|
+
private cutToWindow;
|
|
226
274
|
/**
|
|
227
275
|
* The AND (or OR) of one chunk key across all operands, minus the excludes that hold it, or `null` if empty.
|
|
228
276
|
* Operand chunks are fetched **in parallel** (the spec's parallel byte-range reads).
|
|
@@ -239,16 +287,21 @@ export declare class SegmentEngine {
|
|
|
239
287
|
*/
|
|
240
288
|
private operandChunk;
|
|
241
289
|
/**
|
|
242
|
-
*
|
|
243
|
-
* `getChunks`, a segment with no generation, or nothing to read.
|
|
244
|
-
*
|
|
245
|
-
* requested until the first chunk is asked for.
|
|
290
|
+
* `keys` of `seg` as a stream of coalesced ranges, or `undefined` when the read is per key: a source with no
|
|
291
|
+
* `getChunks`, a segment with no generation, or nothing to read. Nothing is requested, and no cache lookup made, until
|
|
292
|
+
* the first chunk is asked for (see {@link StreamedChunks}).
|
|
246
293
|
*/
|
|
247
294
|
private openStreamed;
|
|
295
|
+
/**
|
|
296
|
+
* Open the stream of `streamed`, at its first uncached chunk `from`: over that chunk and the later keys the cache does
|
|
297
|
+
* not hold. Each later key is counted as a cache hit or miss here, so a key is counted once whichever way it is served.
|
|
298
|
+
*/
|
|
299
|
+
private startStream;
|
|
248
300
|
private closeStreamed;
|
|
249
301
|
/**
|
|
250
|
-
* The decoded chunk at `chunkKey` of a streamed read: the
|
|
251
|
-
*
|
|
302
|
+
* The decoded chunk at `chunkKey` of a streamed read: from the cache while the stream is unopened and the cache holds
|
|
303
|
+
* it; else the next chunk of its stream, or, for a chunk the cache held when the stream opened, the cached instance
|
|
304
|
+
* while it is still there, and a read of that chunk alone once it is not.
|
|
252
305
|
*/
|
|
253
306
|
private streamedChunk;
|
|
254
307
|
/**
|
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;
|