@cloudbitmaps/roaring 0.11.1 → 0.12.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/PRIVACY.md +53 -16
- package/README.md +6 -4
- package/dist/bin/export-segments.js +237 -60
- package/dist/bin/export-segments.js.map +3 -3
- package/dist/index.d.ts +146 -35
- package/dist/index.js +237 -60
- package/dist/index.js.map +3 -3
- package/dist/portable/layout.d.ts +20 -1
- package/dist/roaring-codec.d.ts +28 -3
- package/dist/testing/conformance.d.ts +35 -3
- package/package.json +2 -2
package/PRIVACY.md
CHANGED
|
@@ -43,7 +43,7 @@ from constructing a cross-region topology. The points where personal data moves
|
|
|
43
43
|
| Location | What's there | Residency note |
|
|
44
44
|
|---|---|---|
|
|
45
45
|
| **Storage** (object store) | immutable `.crbm` generations — every generation a segment has had, until a superseded one is collected. Azure Blob objects, and GCS objects above the simple-upload threshold, also carry a random write id in their metadata (`cbwid`): 128 random bits, no data from the bitmap or the source | the region of the bucket you wire |
|
|
46
|
-
| **Registry** (S3 / GCS / Azure Blob / local) | one row per segment: the current-generation pointer, wrapped keys, retention metadata — no IDs. An Azure Blob row also carries the random write id in its metadata | the region of the bucket you wire |
|
|
46
|
+
| **Registry** (S3 / GCS / Azure Blob / local) | one row per segment: the current-generation pointer, wrapped keys, retention metadata, and the current generation's id count and your metadata (sealed when the segment is encrypted) — no IDs, unless you put one in the metadata, which you must not. An Azure Blob row also carries the random write id in its metadata | the region of the bucket you wire |
|
|
47
47
|
| **cache** (process RAM) | decoded chunks, bounded LRU | **wherever your process/Lambda runs** — an EU segment queried from a US function is processed in the US |
|
|
48
48
|
| **Loads and rewrites** (`store.load()`, the `*Into` verbs, `eraseSubject`) | read your source (or existing generations), write a new generation | run wherever you run them — a loader in one region writing to a bucket in another is a transfer |
|
|
49
49
|
| **Intersection** | pulls chunks from N segments into one process | co-locates those segments in one region |
|
|
@@ -60,8 +60,8 @@ CloudBitmaps gives you three levers with different guarantees. Use them delibera
|
|
|
60
60
|
|
|
61
61
|
| Lever | API | Guarantee | Use for |
|
|
62
62
|
|---|---|---|---|
|
|
63
|
-
| **Subject erasure** | `store.eraseSubject(id, { namespace })` | *Physical on return* — the segment's current generation is rewritten without the id (every chunk streamed through, one bit cleared), published **fenced on the generation it streamed**, and **the generation that held the bit is deleted before the call returns**. So is every other generation still holding the id: a retained *superseded* one (an ex-member dropped by a re-seed), and each one above the pointer after a `rollback` — generations a rollback can move the pointer onto again — while, when the current generation does not hold the id, the ones there that never held it stay as rollback targets (a rewrite is numbered above everything, so its `keep: 0` collection takes every older generation, above the pointer or below). Before it says `erased: true` the call lists the bucket and reads what is left, so **`erased: true` means no generation of the segment holds the id**, above the pointer or below it. `eraseSubject` reports a per-segment fault as an `error: …` ledger entry rather than throwing it, and a call that a racing writer overtook (a load, another erasure, or a rollback) as `erased: false, note: 'superseded'`, so "on return" is a claim about every segment whose entry says **`erased: true`** (a fault is an `error: …` entry, never `erased: true`). The store that performs the call stops serving the id immediately; **other processes converge on their own read TTL** — see [One process, and the rest of your fleet](#one-process-and-the-rest-of-your-fleet). Does **not** reach backups, replicas or noncurrent object versions — those hold the old generation until their own lifecycle removes it. | "forget this person" — GDPR Art. 17 |
|
|
64
|
-
| **Dispose** | `store.dropSegment(ref, { confirmSegment })` | *Immediate* — the segment is tombstoned and its Storage generations deleted, reclaiming the space. **Check `generationsRemaining`:** if it is non-empty the space was *not* fully reclaimed and the drop should be re-run (a load that
|
|
63
|
+
| **Subject erasure** | `store.eraseSubject(id, { namespace })` | *Physical on return* — the segment's current generation is rewritten without the id (every chunk streamed through, one bit cleared), published **fenced on the generation it streamed**, and **the generation that held the bit is deleted before the call returns**. So is every other generation still holding the id: a retained *superseded* one (an ex-member dropped by a re-seed), and each one above the pointer after a `rollback` — generations a rollback can move the pointer onto again — while, when the current generation does not hold the id, the ones there that never held it stay as rollback targets (a rewrite is numbered above everything, so its `keep: 0` collection takes every older generation, above the pointer or below). Before it says `erased: true` the call lists the bucket and reads what is left, so **`erased: true` means no generation of the segment holds the id**, above the pointer or below it. `eraseSubject` reports a per-segment fault as an `error: …` ledger entry rather than throwing it, and a call that a racing writer overtook (a load, another erasure, or a rollback) as `erased: false, note: 'superseded'`, so "on return" is a claim about every segment whose entry says **`erased: true`** (a fault is an `error: …` entry, never `erased: true`). The store that performs the call stops serving the id immediately; **other processes converge on their own read TTL** — see [One process, and the rest of your fleet](#one-process-and-the-rest-of-your-fleet). Does **not** reach backups, replicas or noncurrent object versions — those hold the old generation until their own lifecycle removes it. It carries a generation's metadata over unchanged and does **not** scan it, so **never put a subject's id in metadata**. | "forget this person" — GDPR Art. 17 |
|
|
64
|
+
| **Dispose** | `store.dropSegment(ref, { confirmSegment })` | *Immediate* — the segment is tombstoned and its Storage generations deleted, reclaiming the space. **Check `generationsRemaining`:** if it is non-empty the space was *not* fully reclaimed and the drop should be re-run (a load that had read the segment before the tombstone landed, even one still consuming its ids, can write its object after the drop's sweep; it deletes that object itself once its publish is refused, and only one whose process stops in between, or whose publish fails without a definite answer (a lost response, a timeout), leaves it, for a re-run of the drop). Works on cleartext; on an encrypted segment it *also* crypto-shreds, on the terms in the next row. Does **not** reach noncurrent versions / replicas / PITR snapshots — deleting an object is weaker than removing a key. | retiring a dated bucket; rolling-window retention |
|
|
65
65
|
| **Crypto-shred** | `destroySegment` / `eraseNamespace` | *At rest, complete once no copy of the wrapped key survives* — removes the segment's wrapped DEK(s) from its registry row in one compare-and-swap, without touching the bytes or any KEK. **Every** copy of the objects (current, prior generations, backups, WORM-locked objects) is then unreadable once no retained copy of that row still holds the wrapped key — noncurrent object versions, backups and PITR copies of the row included — or once every KEK that wrapped it is destroyed. Until then, a retained copy of the row and a KEK that wrapped it decrypt the segment, and a registry restore to a point before the shred makes it readable again; see *Erasure vs. backups / WORM* below, and [an optional registry expiry rule](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/disaster-recovery.md#optional-make-a-shred-durable-with-a-registry-expiry-rule) that makes a shred durable after a set number of days. Requires the segment to be encrypted. A process that had already opened the segment holds the **unwrapped** key in memory and keeps reading until it is told — call `store.invalidate(ref)` there; see [One process, and the rest of your fleet](#one-process-and-the-rest-of-your-fleet). | whole-segment / tenant offboarding; erasure under immutable backups (see below) |
|
|
66
66
|
|
|
67
67
|
**Subject-wide erasure** (GDPR Art. 17 — "forget this person everywhere") is
|
|
@@ -123,15 +123,17 @@ after it returns, and re-run any erasure a rollback interrupted.
|
|
|
123
123
|
|
|
124
124
|
### One process, and the rest of your fleet
|
|
125
125
|
|
|
126
|
-
Erasure and crypto-shred are **immediate in storage and immediate in the store whose verb performed them
|
|
127
|
-
|
|
126
|
+
Erasure and crypto-shred are **immediate in storage and immediate in the store whose verb performed them**, for
|
|
127
|
+
every read that starts after the verb returns. A read already in progress in that store can still yield an erased id
|
|
128
|
+
from a chunk it had requested before the verb ran: up to 8 chunks for `iterate` and `count`, and up to `concurrency`
|
|
129
|
+
keys for a combine. They are not immediate in any *other* store, in the same process or another, and this library ships nothing that could
|
|
128
130
|
make them so — there is no daemon, no bus, and no connection between two stores that happen to point at the same
|
|
129
131
|
bucket.
|
|
130
132
|
|
|
131
133
|
| | when the id stops being readable |
|
|
132
134
|
|---|---|
|
|
133
135
|
| storage | on return — the generation holding it is deleted, or the wrapped DEK is removed from the segment's current registry row (a crypto-shred is complete once no retained copy of that row holds it, or every KEK that wrapped it is destroyed; see *Erasure vs. backups / WORM* below) |
|
|
134
|
-
| the store whose verb made the call (`eraseSubject`, `dropSegment`, `retireExpired`) | on return — it invalidates what it cached, and its pins then fail |
|
|
136
|
+
| the store whose verb made the call (`eraseSubject`, `dropSegment`, `retireExpired`) | on return, for every read that starts after it — it invalidates what it cached, and its pins then fail; a read already in progress there can still yield it from a chunk it had requested before |
|
|
135
137
|
| another store, with a registry and a `cache.genTtlMs` above 0 | within `cache.genTtlMs` (default 2 s), when its snapshot re-resolves, **while the registry can be read** (see below) |
|
|
136
138
|
| another store with **no registry** (a bare `IStorageDriver`), with `cache: { genTtlMs: 0 }`, or on a storage source built with **no clock** | **no bound** — only when its caches happen to let the segment go, or something tells it |
|
|
137
139
|
| a pinned handle (`seg.pin()`) in another store | **no bound** — until that store's reader cache evicts the pin's reader and its chunk cache evicts the chunks the pin decoded, or something tells it |
|
|
@@ -242,6 +244,16 @@ to run, and the deletion is ours to perform correctly.** Practical patterns:
|
|
|
242
244
|
assumed: `generationsRemaining` non-empty means bytes survived and the call should be repeated. It tombstones
|
|
243
245
|
the segment and deletes **every Storage generation** — so the space is actually reclaimed. It works on a
|
|
244
246
|
cleartext segment, and on an encrypted one it *also* crypto-shreds, making it a strict superset there.
|
|
247
|
+
- **A deleted row keeps its record only where the registry cannot remove it.** Where the registry reports
|
|
248
|
+
`conditionalDelete` (the default for S3 when its client sends to an AWS S3 host, for
|
|
249
|
+
Azure Blob, the local filesystem and memory), `registry.delete` and the retention sweep's purge of a tombstone remove
|
|
250
|
+
a row created by 0.12 from the bucket, and its token's random incarnation id keeps a re-create apart from it. Where it
|
|
251
|
+
does not (an S3 client that sends to an S3-compatible store or an emulator, or a GCS client, by default, or
|
|
252
|
+
`conditionalDelete: false`), and for a row written by a release before 0.12 on any backend, a delete writes a deleted marker that holds
|
|
253
|
+
the record, so the row's token counter survives a re-create. A tombstone left by `dropSegment` or `destroySegment`
|
|
254
|
+
holds no wrapped key and no summary, since both clear them, so a purge that leaves a tombstone keeps the name, the
|
|
255
|
+
pointer, the retention policy and the timestamps. A **live** row deleted directly with `registry.delete` keeps its
|
|
256
|
+
summary, the current generation's id count and your metadata, in a marker where one is written.
|
|
245
257
|
- **`destroySegment` crypto-shreds** — it removes the wrapped key from the registry row, so the Storage bytes
|
|
246
258
|
become unreadable *everywhere including backups, replicas and WORM-locked copies* once no retained copy of that
|
|
247
259
|
row still holds the key, or every KEK that wrapped it is destroyed (see the trap above), which no object
|
|
@@ -264,16 +276,41 @@ So the two are complements, not alternatives: **`dropSegment` for "stop paying f
|
|
|
264
276
|
> failed `dropSegment`, and a fine way to expire *noncurrent* object versions; give it a window comfortably
|
|
265
277
|
> longer than your retention and never let it touch a current generation.
|
|
266
278
|
|
|
267
|
-
> ⚠️ **Never let a lifecycle rule expire a registry row's current version
|
|
268
|
-
> and a worse one.
|
|
269
|
-
>
|
|
270
|
-
>
|
|
271
|
-
>
|
|
272
|
-
>
|
|
273
|
-
>
|
|
274
|
-
>
|
|
275
|
-
>
|
|
276
|
-
>
|
|
279
|
+
> ⚠️ **Never let a lifecycle rule expire a registry row's current version, a tombstone, or a due-index pointer** —
|
|
280
|
+
> a separate trap, and a worse one. A rule cannot tell a live row from a tombstone, and an expired live row is a
|
|
281
|
+
> segment whose pointer is gone: its generations look unreferenced, and nothing reads or collects them. The library
|
|
282
|
+
> removes a row only by a delete the store applies to the exact version it judged, and only a row whose token
|
|
283
|
+
> carries a random incarnation id, so a name re-created later is told apart from it with overwhelming probability.
|
|
284
|
+
> A tombstone of a row written by a release before 0.12 carries the row's token counter on instead, and that counter
|
|
285
|
+
> is what keeps the name's tokens unique against a process still on that release. That token is the segment's
|
|
286
|
+
> **identity**: it is what a cached reader, a fenced publish and a generation-collection pass each compare to decide
|
|
287
|
+
> whether two observations describe the same segment. Re-issue one and they can all answer "yes" about a segment that
|
|
288
|
+
> no longer exists — serving a deleted incarnation's data, or collecting a live one's objects. A rule on
|
|
289
|
+
> *noncurrent* versions of the registry prefix touches neither the current row nor its tombstone; what it shortens is
|
|
290
|
+
> the history a registry restore picks from, and the time a crypto-shred takes to complete (above).
|
|
291
|
+
|
|
292
|
+
**What the registry keeps of a retired segment, and for how long.** A retirement by `retireExpired` leaves a
|
|
293
|
+
`destroyed` row: the segment's namespace and name, its `retention` and `residency` metadata (any keys of yours in
|
|
294
|
+
them included), its timestamps and its token. It holds no id, and on an encrypted segment no wrapped key, since the
|
|
295
|
+
drop shreds it. The due index holds a pointer to it whose name spells out the same namespace and name. Both stay
|
|
296
|
+
for `tombstoneGraceMs` (24 h by default) after the retirement, and for as long as the segment's storage cannot be
|
|
297
|
+
proven gone. Then the sweep's purge removes the row, and the pointers it read to it, from the bucket, where the registry
|
|
298
|
+
reports `conditionalDelete`: the default for S3 when its client sends to an AWS S3 host,
|
|
299
|
+
for Azure Blob, the local filesystem and memory. **A pointer can outlive its row**, and with it the name in its key: a
|
|
300
|
+
purge that ran with another `tombstoneGraceMs` than the sweep that filed the pointer, a delete that landed and lost its
|
|
301
|
+
response, a removal the registry refused (`purgeFaults` counts it), or a pointer older than an index scan's
|
|
302
|
+
`lookbackBuckets`. The next sweep that reads it removes it once its row is confirmed absent: an index scan from the days
|
|
303
|
+
it reads, and an unscoped fleet scan, one given no `namespace`, from every day. A deployment that scopes every sweep to a
|
|
304
|
+
namespace, or runs only index scans, keeps such a pointer until an unscoped fleet scan runs. Where the registry does not
|
|
305
|
+
report `conditionalDelete` (an S3 client that sends to an S3-compatible store or an emulator, or a GCS client, by default, or
|
|
306
|
+
`conditionalDelete: false`), and for a row written by a release before 0.12 on any backend, the purge leaves a tombstone
|
|
307
|
+
instead: every one of those fields, in the bucket, indefinitely, read by every full listing. A registry that does not
|
|
308
|
+
report it files no pointer to begin with. A removed
|
|
309
|
+
row stays recoverable wherever the storage keeps a copy of it: with object versioning on, its earlier versions stay until
|
|
310
|
+
a noncurrent-version rule expires them, as an overwritten row's do; with soft delete on, it stays for the retention
|
|
311
|
+
window (GCS: on by default for a new bucket, 7 days; Azure Blob: where enabled, for the days set), whatever the registry
|
|
312
|
+
did. A tombstone a hand-run `dropSegment` or a
|
|
313
|
+
crypto-shred left is never purged by the library.
|
|
277
314
|
|
|
278
315
|
Full detail, including the dated-bucket pattern and the pitfalls, is in the retention section of the
|
|
279
316
|
[retention guide](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/guide/retention.md).
|
package/README.md
CHANGED
|
@@ -48,7 +48,9 @@ To persist, pass a different **backend** as `storage`. Nothing else changes:
|
|
|
48
48
|
| `AzureBlobStorage({ connectionString, container })` | `@cloudbitmaps/azure-blob` | Azure Blob Storage; outside the [validated envelope](https://github.com/cloudbitmaps/cloudbitmaps/blob/main/docs/ROADMAP.md#the-validated-envelope--whats-proven-and-what-isnt) |
|
|
49
49
|
|
|
50
50
|
There is no `add` or `remove`: a segment changes only by loading a new generation. A load is a batch job, not a
|
|
51
|
-
request handler. `store.exists(ref)` says whether a segment has been loaded.
|
|
51
|
+
request handler. `store.exists(ref)` says whether a segment has been loaded. A load takes ids, or a bitmap you already
|
|
52
|
+
hold: `store.load(ref, { bitmap })` for a `RoaringBitmap32`, or `{ serialized }` for portable Roaring bytes, checked
|
|
53
|
+
before anything is written and loaded with no per-id work, into the same bytes its ids would write.
|
|
52
54
|
|
|
53
55
|
## Options
|
|
54
56
|
|
|
@@ -70,7 +72,7 @@ name.
|
|
|
70
72
|
Measured against real S3 in `us-east-1`, with the pointer in the same bucket as the data: the median cold `intersect`
|
|
71
73
|
of two 500,000-id segments sharing 100 of their 1,999 chunks made 206 GETs, **$82.40 per million**, requesting only
|
|
72
74
|
the shared chunks. Inside the region it is expected at 204 GETs, $81.60 per million. Writing and publishing a segment
|
|
73
|
-
is **$11.20 per million**, pointer included, from its measured requests at list prices. `count()` reads no payload.
|
|
75
|
+
is **$11.20 per million**, pointer included, from its measured requests at list prices. A cold `count()` is one pointer read: it reads no payload and no object.
|
|
74
76
|
|
|
75
77
|
The trade is stated plainly. A membership check that misses the cache costs a ranged GET against object storage,
|
|
76
78
|
where an in-process store costs a memory read. If you need sub-millisecond answers on a working set that fits a
|
|
@@ -80,8 +82,8 @@ give the method and what the numbers do not establish.
|
|
|
80
82
|
## Before production
|
|
81
83
|
|
|
82
84
|
- Grant the permissions the library issues and add a lifecycle rule to abort incomplete uploads.
|
|
83
|
-
- Use a bucket that honors conditional writes, and `@aws-sdk/client-s3` 3.
|
|
84
|
-
- Set a request timeout on your storage client. Reads retry by themselves; a write that fails transiently is yours to re-run.
|
|
85
|
+
- Use a bucket that honors conditional writes, and `@aws-sdk/client-s3` 3.700.0 or later if you pass your own S3 client.
|
|
86
|
+
- Set a request timeout on your storage client. Reads retry by themselves, and a read can be timed on every backend (`readTimeoutMs`); a write that fails transiently is yours to re-run.
|
|
85
87
|
- Back up the registry with the data, and the keystore too if you encrypt. If you lose the key, the data is gone.
|
|
86
88
|
- Schedule your loads, and `retireExpired` if you use retention. Run loads off the request path.
|
|
87
89
|
|