@harperfast/rocksdb-js 2.11.0 → 2.13.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/README.md +158 -27
- package/dist/index.cjs +322 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +178 -39
- package/dist/index.d.mts +178 -39
- package/dist/index.mjs +320 -67
- package/dist/index.mjs.map +1 -1
- package/docs/backups.md +27 -0
- package/docs/performance/mutex-contention.md +4 -3
- package/docs/stats.md +11 -4
- package/docs/steady-clock-design.md +1 -1
- package/docs/transaction-log.md +27 -7
- package/package.json +19 -18
package/docs/backups.md
CHANGED
|
@@ -80,6 +80,33 @@ verification). Pass `verifyTransactionLogs: false` to check only the RocksDB fil
|
|
|
80
80
|
stores (e.g. the `transaction_logs` directory of a closed database) can be validated directly with
|
|
81
81
|
`validateTransactionLogStore()`.
|
|
82
82
|
|
|
83
|
+
Pass `verifyTables: true` to check that **this build** can read the backup, not only that its files
|
|
84
|
+
are intact. Every table file is opened in turn (one at a time), so verification rejects, naming the
|
|
85
|
+
file, when a table uses a `format_version` or a compression algorithm this build does not support.
|
|
86
|
+
That is the case after a downgrade, or when restoring a backup taken by a build that linked a
|
|
87
|
+
compressor this one lacks. Blob files are not checked.
|
|
88
|
+
|
|
89
|
+
Check before a destructive restore. The default `purgeAllFiles` mode deletes the destination before
|
|
90
|
+
copying, so an unreadable backup would otherwise destroy the database it was meant to replace:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { backups, fileLockRelease, tryFileLock } from '@harperfast/rocksdb-js';
|
|
94
|
+
import { join } from 'node:path';
|
|
95
|
+
|
|
96
|
+
// A shared lock keeps a concurrent delete/purge from changing the backup between the two calls.
|
|
97
|
+
// Shared locks coexist, so restore can still take its own.
|
|
98
|
+
const token = tryFileLock(join(backupDir, '.backup.lock'), true);
|
|
99
|
+
if (!token) {
|
|
100
|
+
throw new Error('A backup, delete, or purge is running on this directory; retry later');
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
await backups.verify(backupDir, backupId, { verifyWithChecksum: true, verifyTables: true });
|
|
104
|
+
await backups.restore(backupDir, dbPath, { backupId });
|
|
105
|
+
} finally {
|
|
106
|
+
fileLockRelease(token);
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
83
110
|
### Restoring
|
|
84
111
|
|
|
85
112
|
```typescript
|
|
@@ -171,11 +171,12 @@ A separate native reproduction confirmed [#860](https://github.com/HarperFast/ro
|
|
|
171
171
|
`tryClose()` releases its written-position lock before latching admission closed.
|
|
172
172
|
A transaction can bind, finish `writeBatch()` and reduce the pending count back to zero
|
|
173
173
|
between those checks. A temporary scheduling hook before phase 3 reproduced
|
|
174
|
-
`pending=0`, one real uncommitted position, and `tryClose() == true`. This
|
|
174
|
+
`pending=0`, one real uncommitted position, and `tryClose() == true`. This was a
|
|
175
175
|
pre-existing close-protocol defect, not fixed by the completion-lock change. Its
|
|
176
176
|
production trigger is a concurrent destructive log purge; no customer incident or
|
|
177
|
-
JavaScript-level data loss was reproduced.
|
|
178
|
-
|
|
177
|
+
JavaScript-level data loss was reproduced. It is now fixed without changing the binding
|
|
178
|
+
lock: every bind advances a generation that `tryClose()` rechecks before latching (see
|
|
179
|
+
"Conditional close" in `src/binding/transaction_log/DESIGN.md`).
|
|
179
180
|
|
|
180
181
|
The investigation also found [#859](https://github.com/HarperFast/rocksdb-js/issues/859):
|
|
181
182
|
the DB-instance stress test awaits an empty promise array in its worker-close phase.
|
package/docs/stats.md
CHANGED
|
@@ -189,6 +189,10 @@ which includes all of the stats above plus the following:
|
|
|
189
189
|
| `rocksdb.blobdb.gc.num.new.files` | Number of new blob files generated by garbage collection (legacy stacked BlobDB). | ticker |
|
|
190
190
|
| `rocksdb.blobdb.get.micros` | Distribution of `Get()` latencies (microseconds) (legacy stacked BlobDB). | histogram |
|
|
191
191
|
| `rocksdb.blobdb.key.size` | Distribution of key sizes for BlobDB operations. | histogram |
|
|
192
|
+
| `rocksdb.blobdb.lazy.partial.bytes.saved` | Bytes not read from blob files thanks to partial (byte-range) lazy reads: the value size minus the bytes actually read. | ticker |
|
|
193
|
+
| `rocksdb.blobdb.lazy.partial.read.count` | Number of lazy blob reads that read only a byte range of an uncompressed blob value. | ticker |
|
|
194
|
+
| `rocksdb.blobdb.lazy.read.bytes` | Bytes read from storage while resolving lazy wide-column results (`GetEntityLazy`/`MultiGetEntityLazy`). | ticker |
|
|
195
|
+
| `rocksdb.blobdb.lazy.read.count` | Number of storage reads issued while resolving lazy wide-column results (`GetEntityLazy`/`MultiGetEntityLazy`). | ticker |
|
|
192
196
|
| `rocksdb.blobdb.multiget.micros` | Distribution of `MultiGet()` latencies (microseconds) (legacy stacked BlobDB). | histogram |
|
|
193
197
|
| `rocksdb.blobdb.next.micros` | Distribution of iterator `Next()` latencies (microseconds) (legacy stacked BlobDB). | histogram |
|
|
194
198
|
| `rocksdb.blobdb.num.get` | Number of `Get()` calls (legacy stacked BlobDB). | ticker |
|
|
@@ -493,7 +497,9 @@ const stats: TransactionLogStats = log.getStats();
|
|
|
493
497
|
|
|
494
498
|
- `name: string` The name of the transaction log store.
|
|
495
499
|
- `path: string` Filesystem path to the log store's directory.
|
|
496
|
-
- `fileCount: number` Number of sequence log files on disk for this log.
|
|
500
|
+
- `fileCount: number` Number of sequence log files on disk for this log. `0` does not mean the log
|
|
501
|
+
was never written: retention can purge every file of an idle log. A log that has never been written
|
|
502
|
+
reports `currentSequenceNumber` 1; one emptied by retention reports a higher one.
|
|
497
503
|
- `currentSequenceNumber: number` Sequence number of the active write file.
|
|
498
504
|
- `oldestSequenceNumber: number` Sequence number of the oldest file still on disk.
|
|
499
505
|
- `totalSizeBytes: number` Total on-disk size of all of this log's files.
|
|
@@ -522,9 +528,10 @@ const stats: TransactionLogStats = log.getStats();
|
|
|
522
528
|
- `offset: number` The byte offset within that file.
|
|
523
529
|
- `purge: object`
|
|
524
530
|
- `oldestFileAgeMs: number` Age in milliseconds of the oldest file on disk.
|
|
525
|
-
- `purgeableFiles: number` Number of files
|
|
526
|
-
|
|
527
|
-
|
|
531
|
+
- `purgeableFiles: number` Number of files past the retention threshold that lie entirely at or
|
|
532
|
+
before the flushed position, including the current file, so the next purge can delete them.
|
|
533
|
+
- `retentionPins: number` Backups currently copying this store's files. Ordinary purges of the
|
|
534
|
+
store are skipped while it is nonzero, so a value that stays nonzero points at a stalled backup.
|
|
528
535
|
- `retainedUnflushedFiles: number` Number of files past the retention threshold but retained
|
|
529
536
|
because they are unflushed.
|
|
530
537
|
- `lastPurgeMs: number` Timestamp of the last purge scan.
|
|
@@ -87,7 +87,7 @@ separate properties. No promise is made that runtime timers use the same suspend
|
|
|
87
87
|
## Verification
|
|
88
88
|
|
|
89
89
|
The end-to-end route is the real N-API export through `src/index.ts`, parent/worker bracketing on
|
|
90
|
-
Node
|
|
90
|
+
Node and Bun (Deno: local `pnpm test:deno` only, #947), staggered/restarted and concurrent workers (including `workerFirst >= parentBefore + stagger - tolerance`), elapsed
|
|
91
91
|
progression, and built ESM/CJS
|
|
92
92
|
exports. Temporarily substituting a worker-local clock must make those brackets fail, proving
|
|
93
93
|
they reject the motivating defect.
|
package/docs/transaction-log.md
CHANGED
|
@@ -58,7 +58,7 @@ The token is used to validate that the file is indeed a transaction log.
|
|
|
58
58
|
|
|
59
59
|
#### `version`
|
|
60
60
|
|
|
61
|
-
The transaction log file format version. Currently, version `1` is the latest.
|
|
61
|
+
The transaction log file format version. Currently, version `1` is the latest. Opening a database whose newest transaction-log segments include one with a higher version (written by a newer release) fails with an error naming the segment, rather than skipping it, and this release never appends after such a segment. A malformed or unreadable segment is still skipped.
|
|
62
62
|
|
|
63
63
|
#### `timestamp`
|
|
64
64
|
|
|
@@ -84,9 +84,10 @@ The size of the data entry directly following the transaction header.
|
|
|
84
84
|
|
|
85
85
|
Transaction entry related flags.
|
|
86
86
|
|
|
87
|
-
| Flag
|
|
88
|
-
|
|
|
89
|
-
| `TRANSACTION_LOG_ENTRY_LAST_FLAG`
|
|
87
|
+
| Flag | Value | Description |
|
|
88
|
+
| ------------------------------------ | ------ | --------------------------------------------------------------------------------------------------- |
|
|
89
|
+
| `TRANSACTION_LOG_ENTRY_LAST_FLAG` | `0x01` | Indicates the last entry for a transaction |
|
|
90
|
+
| `TRANSACTION_LOG_ENTRY_ABORTED_FLAG` | `0x02` | On a transaction's first entry: the transaction did not commit, and readers skip all of its entries |
|
|
90
91
|
|
|
91
92
|
## Encoding Details
|
|
92
93
|
|
|
@@ -198,9 +199,23 @@ await db.transaction((txn) => {
|
|
|
198
199
|
- Log files are automatically rotated when either the index or data file reaches their configured
|
|
199
200
|
maximum sizes
|
|
200
201
|
- Rotation happens on the next write after the size limit is exceeded
|
|
201
|
-
- Old log files can be automatically purged based on retention policy.
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
- Old log files can be automatically purged based on retention policy. A file is purgeable once it
|
|
203
|
+
is past the cutoff and lies entirely at or before the flushed position recorded in `txn.state`.
|
|
204
|
+
That includes the file `txn.state` names and the file currently being appended to, so an idle
|
|
205
|
+
store whose entries are all flushed ends up with no log files at all. The next write starts a new,
|
|
206
|
+
higher sequence; `txn.state` stays behind as the record of the highest sequence used, and a
|
|
207
|
+
restart appends past it. Before deleting a store's last file, retention syncs `txn.state` and its
|
|
208
|
+
directory; on a filesystem that rejects a directory fsync it keeps that file and warns once.
|
|
209
|
+
- `txn.state` is rewritten in place (8 bytes at offset 0, not synced on each flush). Retention relies
|
|
210
|
+
on storage writing that range atomically: a power loss that tears it into a mix of old and new
|
|
211
|
+
bytes can name a later position than was flushed, and retention would then treat unflushed
|
|
212
|
+
entries below it as flushed.
|
|
213
|
+
- A reader that starts from the last flushed position, or one already inside a purged file,
|
|
214
|
+
continues at the lowest retained sequence (or at the next write when nothing is retained).
|
|
215
|
+
- Downgrading to a release without this rule after a store's last file was purged is unsafe: older
|
|
216
|
+
releases restart an empty store at sequence 1, below the flushed position, so a replay from
|
|
217
|
+
`txn.state` skips the new writes and retention treats them as flushed. Remove the store's
|
|
218
|
+
`txn.state` before downgrading.
|
|
204
219
|
|
|
205
220
|
### Error Handling
|
|
206
221
|
|
|
@@ -251,6 +266,11 @@ handle is returned, so no transaction can be constructed below it. Keys are not
|
|
|
251
266
|
across segments, and a segment header records the store's latest timestamp only as of that
|
|
252
267
|
segment's creation, so there is no shortcut: every segment is read.
|
|
253
268
|
|
|
269
|
+
Retention can purge every segment of the named log. The walk then finds no keys and leaves the clock
|
|
270
|
+
alone, so a backward step after restart can reissue a key that was only in purged segments. With
|
|
271
|
+
age-based retention alone that needs a backward step at least as large as the retention period; a
|
|
272
|
+
`purgeLogs({ before })` cutoff closer to now shrinks it to the time since that cutoff.
|
|
273
|
+
|
|
254
274
|
Name only a log this process originates. A log a replication receiver writes under an adopted origin
|
|
255
275
|
timestamp is keyed by another node's clock, and seeding from it would ratchet this process's clock
|
|
256
276
|
to the fastest of those nodes at each restart. Native code cannot tell the two kinds of log apart,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harperfast/rocksdb-js",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"description": "RocksDB binding for Node.js",
|
|
5
5
|
"homepage": "https://github.com/HarperFast/rocksdb-js",
|
|
6
6
|
"bugs": [
|
|
@@ -38,39 +38,39 @@
|
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@rollup/plugin-replace": "6.0.3",
|
|
41
|
-
"@types/node": "26.6.
|
|
41
|
+
"@types/node": "26.6.5",
|
|
42
42
|
"@vitest/coverage-v8": "4.1.11",
|
|
43
43
|
"cross-env": "10.1.0",
|
|
44
|
-
"dotenv": "18.0.
|
|
45
|
-
"lefthook": "2.
|
|
46
|
-
"lmdb": "3.5.
|
|
44
|
+
"dotenv": "18.0.9",
|
|
45
|
+
"lefthook": "2.2.2",
|
|
46
|
+
"lmdb": "3.5.9",
|
|
47
47
|
"node-gyp": "13.1.0",
|
|
48
|
-
"oxfmt": "0.
|
|
49
|
-
"oxlint": "1.
|
|
48
|
+
"oxfmt": "0.75.0",
|
|
49
|
+
"oxlint": "1.90.0",
|
|
50
50
|
"prebuildify": "6.0.1",
|
|
51
51
|
"semver": "7.8.5",
|
|
52
52
|
"tar": "7.5.22",
|
|
53
|
-
"tsdown": "0.23.
|
|
53
|
+
"tsdown": "0.23.3",
|
|
54
54
|
"tslib": "2.8.1",
|
|
55
55
|
"typescript": "7.0.2",
|
|
56
56
|
"vitest": "4.1.11"
|
|
57
57
|
},
|
|
58
58
|
"optionalDependencies": {
|
|
59
|
-
"@harperfast/rocksdb-js-darwin-arm64": "2.
|
|
60
|
-
"@harperfast/rocksdb-js-darwin-x64": "2.
|
|
61
|
-
"@harperfast/rocksdb-js-linux-arm64-glibc": "2.
|
|
62
|
-
"@harperfast/rocksdb-js-linux-arm64-musl": "2.
|
|
63
|
-
"@harperfast/rocksdb-js-linux-x64-glibc": "2.
|
|
64
|
-
"@harperfast/rocksdb-js-linux-x64-musl": "2.
|
|
65
|
-
"@harperfast/rocksdb-js-win32-arm64": "2.
|
|
66
|
-
"@harperfast/rocksdb-js-win32-x64": "2.
|
|
59
|
+
"@harperfast/rocksdb-js-darwin-arm64": "2.13.0",
|
|
60
|
+
"@harperfast/rocksdb-js-darwin-x64": "2.13.0",
|
|
61
|
+
"@harperfast/rocksdb-js-linux-arm64-glibc": "2.13.0",
|
|
62
|
+
"@harperfast/rocksdb-js-linux-arm64-musl": "2.13.0",
|
|
63
|
+
"@harperfast/rocksdb-js-linux-x64-glibc": "2.13.0",
|
|
64
|
+
"@harperfast/rocksdb-js-linux-x64-musl": "2.13.0",
|
|
65
|
+
"@harperfast/rocksdb-js-win32-arm64": "2.13.0",
|
|
66
|
+
"@harperfast/rocksdb-js-win32-x64": "2.13.0"
|
|
67
67
|
},
|
|
68
68
|
"engines": {
|
|
69
69
|
"node": "^22.18.0 || >=24.0.0"
|
|
70
70
|
},
|
|
71
71
|
"gypfile": true,
|
|
72
72
|
"rocksdb": {
|
|
73
|
-
"version": "11.
|
|
73
|
+
"version": "11.12.0"
|
|
74
74
|
},
|
|
75
75
|
"scripts": {
|
|
76
76
|
"bench": "cross-env CI=1 node --expose-gc ./node_modules/vitest/vitest.mjs bench --passWithNoTests --outputJson benchmark-results.json",
|
|
@@ -81,7 +81,8 @@
|
|
|
81
81
|
"build:binding:debug": "node-gyp build --coverage --debug --verbose",
|
|
82
82
|
"build:bundle": "node scripts/clean-dist.mjs && tsdown -c tsdown.config.ts",
|
|
83
83
|
"build:bundle:minify": "cross-env MINIFY=1 pnpm build:bundle",
|
|
84
|
-
"check": "pnpm
|
|
84
|
+
"check": "pnpm check:code && pnpm fmt:check",
|
|
85
|
+
"check:code": "pnpm type-check && pnpm lint",
|
|
85
86
|
"clean": "node-gyp clean",
|
|
86
87
|
"coverage": "node scripts/coverage/main.ts",
|
|
87
88
|
"coverage:native": "node scripts/native-test/coverage.mjs",
|