@harperfast/rocksdb-js 2.8.0 → 2.9.1
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 +357 -27
- package/dist/index.cjs +290 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +327 -38
- package/dist/index.d.mts +327 -38
- package/dist/index.mjs +289 -41
- package/dist/index.mjs.map +1 -1
- package/docs/stats.md +46 -1
- package/docs/transaction-log.md +18 -5
- package/docs/transaction-timestamp-integrity-design.md +123 -0
- package/package.json +19 -19
package/docs/stats.md
CHANGED
|
@@ -73,6 +73,28 @@ By default, `db.getStats()` returns properties only.
|
|
|
73
73
|
| `txnlog.totalSizeBytes` | Total on-disk size in bytes of all transaction log files, summed across all logs. | gauge |
|
|
74
74
|
| `txnlog.transactionsWritten` | Cumulative number of transactions successfully written to the logs (lifetime total), summed across all logs. | ticker |
|
|
75
75
|
| `txnlog.uncommittedTransactions` | Number of transactions written to a log but not yet committed to RocksDB, summed across all logs. | gauge |
|
|
76
|
+
| `writeBufferManager.bufferSize` | The `WriteBufferManager`'s memtable-memory budget in bytes, read live. `0` when no manager is configured. **Process-wide**, not per-database — see below. | gauge |
|
|
77
|
+
| `writeBufferManager.memoryUsage` | Total memtable memory in bytes charged against the manager. **Process-wide.** | gauge |
|
|
78
|
+
| `writeBufferManager.mutableMemoryUsage` | The share of `writeBufferManager.memoryUsage` held by active (mutable) memtables; the rest is awaiting flush or retained write history. **Process-wide.** | gauge |
|
|
79
|
+
| `writeBufferManager.stallActive` | `1` while the manager is stalling writes, else `0`. **Process-wide.** | gauge |
|
|
80
|
+
| `writeBufferManager.stallActiveMs` | How long the current stall has been active, in milliseconds; `0` when not stalled. Sampled once a second by the stall watchdog. **Process-wide.** | gauge |
|
|
81
|
+
|
|
82
|
+
#### The `writeBufferManager.*` keys are process-wide
|
|
83
|
+
|
|
84
|
+
The `WriteBufferManager` is a **singleton shared by every database opened in this process**,
|
|
85
|
+
`worker_threads` included. These five keys therefore describe the whole process no matter which
|
|
86
|
+
database's `getStats()` returned them — two databases in one process always report identical
|
|
87
|
+
values, and the numbers do not attribute memory to the database you asked. They appear regardless
|
|
88
|
+
of `enableStats`, like the `txnlog.*` and `commitPipeline.*` keys.
|
|
89
|
+
`getWriteBufferManagerStats()` returns the same values plus the manager's
|
|
90
|
+
configuration and its live column-family inventory.
|
|
91
|
+
|
|
92
|
+
`writeBufferManager.stallActive` is the only signal that distinguishes a `WriteBufferManager`
|
|
93
|
+
stall from an idle database. RocksDB's own stall accounting does not cover it:
|
|
94
|
+
`rocksdb.stall.micros`, `rocksdb.db.write.stall` and the `'writeStall'` event all track the
|
|
95
|
+
`WriteController` (L0 and pending-compaction triggers), while a `WriteBufferManager` stall blocks
|
|
96
|
+
writers on a separate queue and touches none of them. Through an eight-hour production wedge all
|
|
97
|
+
three read `0` (HarperFast/rocksdb-js#822).
|
|
76
98
|
|
|
77
99
|
### Basic + Curated Stats
|
|
78
100
|
|
|
@@ -433,6 +455,27 @@ which includes all of the stats above plus the following:
|
|
|
433
455
|
| `rocksdb.write.self` | Number of writes processed by the calling thread itself (as the write group leader). | ticker |
|
|
434
456
|
| `rocksdb.write.wal` | Number of writes that were written to the WAL. | ticker |
|
|
435
457
|
|
|
458
|
+
#### `flush.reason.*` vs `atomic_flush.request.reason.*`
|
|
459
|
+
|
|
460
|
+
This library always opens databases with RocksDB's `atomic_flush`. That does **not** move
|
|
461
|
+
`WriteBufferManager`-pressure flushes off `rocksdb.flush.reason.write_buffer_manager` and onto
|
|
462
|
+
`rocksdb.atomic_flush.request.reason.write_buffer_manager` — measured on this library, both counters
|
|
463
|
+
move together (HarperFast/rocksdb-js#822).
|
|
464
|
+
|
|
465
|
+
They count different events, though, and neither magnitude can be derived from the other:
|
|
466
|
+
|
|
467
|
+
- `rocksdb.atomic_flush.request.reason.<reason>` counts flush **requests** — scheduling decisions.
|
|
468
|
+
- `rocksdb.flush.reason.<reason>` counts the flushes that actually **ran**.
|
|
469
|
+
|
|
470
|
+
Measured ratios in one process ranged from 1:1 (a single column family) to about 1:2 (two to six
|
|
471
|
+
column families), and requests can outnumber executed flushes when several databases share one
|
|
472
|
+
manager. Treat them as two views of the same pressure, not as a fixed multiple.
|
|
473
|
+
|
|
474
|
+
Neither counter reports a stall. A `0` on both during a `WriteBufferManager` stall is the expected
|
|
475
|
+
reading, not a broken counter: the flush trigger looks only at _mutable_ memory, so a budget held by
|
|
476
|
+
retained history or by memtables already awaiting flush never requests a flush at all.
|
|
477
|
+
`writeBufferManager.stallActive` is what distinguishes that state from an idle database.
|
|
478
|
+
|
|
436
479
|
## Transaction Log Stats
|
|
437
480
|
|
|
438
481
|
`log.getStats()` returns a detailed snapshot for a single transaction log store. All sizes are in
|
|
@@ -477,7 +520,9 @@ const stats: TransactionLogStats = log.getStats();
|
|
|
477
520
|
- `offset: number` The byte offset within that file.
|
|
478
521
|
- `purge: object`
|
|
479
522
|
- `oldestFileAgeMs: number` Age in milliseconds of the oldest file on disk.
|
|
480
|
-
- `purgeableFiles: number` Number of files
|
|
523
|
+
- `purgeableFiles: number` Number of files below the retention floor (the `txn.state` sequence,
|
|
524
|
+
or the highest sequence when there is no persisted flush position) that are eligible for purge
|
|
525
|
+
under the retention policy.
|
|
481
526
|
- `retainedUnflushedFiles: number` Number of files past the retention threshold but retained
|
|
482
527
|
because they are unflushed.
|
|
483
528
|
- `lastPurgeMs: number` Timestamp of the last purge scan.
|
package/docs/transaction-log.md
CHANGED
|
@@ -198,7 +198,9 @@ await db.transaction((txn) => {
|
|
|
198
198
|
- Log files are automatically rotated when either the index or data file reaches their configured
|
|
199
199
|
maximum sizes
|
|
200
200
|
- Rotation happens on the next write after the size limit is exceeded
|
|
201
|
-
- Old log files can be automatically purged based on retention policy
|
|
201
|
+
- Old log files can be automatically purged based on retention policy. The sequence file named by
|
|
202
|
+
`txn.state` and every newer file form the live store's retention floor, so an idle store can keep
|
|
203
|
+
one bounded file past the cutoff until a later write rotates and flushes it.
|
|
202
204
|
|
|
203
205
|
### Error Handling
|
|
204
206
|
|
|
@@ -218,10 +220,21 @@ The CLI exposes the same check as `verify-logs [name]`.
|
|
|
218
220
|
|
|
219
221
|
## Reading The Transaction Log
|
|
220
222
|
|
|
221
|
-
Log entries are not
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
223
|
+
Log entries are not in timestamp order. A transaction claims its timestamp from the process-wide
|
|
224
|
+
monotonic clock when it is constructed but is appended when it commits, so under concurrency a later
|
|
225
|
+
entry can carry a smaller timestamp; an entry that adopted an origin timestamp with
|
|
226
|
+
`txn.setTimestamp()` carries that origin's clock instead. Timestamps are not unique: the monotonic
|
|
227
|
+
clock never issues the same value twice within a process, but a restart after the wall clock moved
|
|
228
|
+
backwards can reissue one, and `txn.setTimestamp()` can assign any value — including one already
|
|
229
|
+
in the log — to as many transactions as the caller likes. Deduplicating on the timestamp alone is
|
|
230
|
+
therefore never safe; a consumer that needs identity has to supply it (Harper pairs the timestamp
|
|
231
|
+
with the originating node).
|
|
232
|
+
|
|
233
|
+
When reading the transaction log file, each transaction entry header must be read and indexed. The
|
|
234
|
+
index records only the entries whose timestamp is greater than every earlier one in the file — a
|
|
235
|
+
running maxima — and a query seeks to the lower bound of that index, which is guaranteed to sit at
|
|
236
|
+
or before every entry in the requested range. Reading forward from there and filtering is what makes
|
|
237
|
+
range queries correct on an unordered file.
|
|
225
238
|
|
|
226
239
|
### Sequential Read
|
|
227
240
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Transaction timestamp integrity
|
|
2
|
+
|
|
3
|
+
## Intent
|
|
4
|
+
|
|
5
|
+
Harper's source-record version and a RocksDB transaction's write identity are separate values.
|
|
6
|
+
Harper's `RecordEncoder` and `PrimaryRocksDatabase` already own the record value layout and decode
|
|
7
|
+
its metadata; rocksdb-js owns the transaction timestamp and uses it as the transaction-log batch
|
|
8
|
+
key. This change only makes overriding that timestamp safe for Harper's two legitimate adopters,
|
|
9
|
+
replication apply and crash replay.
|
|
10
|
+
|
|
11
|
+
## Invariant
|
|
12
|
+
|
|
13
|
+
Within one transaction attempt, the timestamp cannot change after the transaction leaves `Pending`
|
|
14
|
+
or stages a database write or transaction-log entry. A retry may adopt again only when the failed
|
|
15
|
+
attempt did not durably write its log batch; once a batch is durable, its timestamp remains frozen
|
|
16
|
+
across retry resets. Encoding a transaction timestamp into record bytes remains the producer's
|
|
17
|
+
obligation because native code cannot observe when a producer reads and copies the timestamp.
|
|
18
|
+
|
|
19
|
+
## Requirements traced to Harper
|
|
20
|
+
|
|
21
|
+
- HarperFast/harper#2065 intentionally allowed a source record's version to differ from the
|
|
22
|
+
transaction timestamp.
|
|
23
|
+
- HarperFast/harper#2409 fixed the immediate subscription regression by carrying both values in
|
|
24
|
+
Harper's audit representation.
|
|
25
|
+
- HarperFast/harper#2411 and HarperFast/harper-pro#790 require replay and replication respectively
|
|
26
|
+
to adopt the origin transaction timestamp through `setTimestamp()` before applying writes.
|
|
27
|
+
- HarperFast/harper#2412 owns the optional second version word in `RecordEncoder`, including its
|
|
28
|
+
encoding, decoding, compatibility gates, and call-site audit.
|
|
29
|
+
|
|
30
|
+
Checked against Harper's source rather than the issue text: `resources/RecordEncoder.ts` decodes the
|
|
31
|
+
value header itself (producing `localTime`, `version` and the metadata flags) and owns every flag bit
|
|
32
|
+
in that word, and `resources/PrimaryRocksDatabase.ts#getEntry()` overrides the base read entirely —
|
|
33
|
+
it calls `super.getSync()`/`super.get()` and reads the encoder's output. So nothing in Harper would
|
|
34
|
+
call a rocksdb-js decoder for the second word. Harper's three `setTimestamp()` call sites
|
|
35
|
+
(`resources/DatabaseTransaction.ts` at the read-transaction, save and retry-replay paths) each set it
|
|
36
|
+
on a freshly constructed transaction before any write, which is what this change makes enforceable.
|
|
37
|
+
|
|
38
|
+
One consequence for #2412 that is not in its call-site audit: `PrimaryRocksDatabase#getEntry()` passes
|
|
39
|
+
the cached entry's `version` as `expectedVersion` to the verification table, and the table keys on the
|
|
40
|
+
value's **first** word. Once `version` becomes the record version, that argument has to become
|
|
41
|
+
`localTime`, or cached reads stop hitting the fast path — and a record version that happened to equal
|
|
42
|
+
the key's current first word would report a stale copy as fresh.
|
|
43
|
+
|
|
44
|
+
## Approaches considered
|
|
45
|
+
|
|
46
|
+
### Different layer
|
|
47
|
+
|
|
48
|
+
Teach rocksdb-js to define and decode Harper's second version word. Rejected because Harper's
|
|
49
|
+
`PrimaryRocksDatabase#getEntry()` already overrides the base read path and receives the structured
|
|
50
|
+
entry produced by Harper's `RecordEncoder`; a second decoder in rocksdb-js would duplicate the
|
|
51
|
+
layout and cannot supply Harper's other metadata fields.
|
|
52
|
+
|
|
53
|
+
Reserving Harper's proposed `0x20000` bit in rocksdb-js without decoding it was also considered.
|
|
54
|
+
Rejected because rocksdb-js does not allocate the producer's metadata namespace: Harper already
|
|
55
|
+
defines the other optional-field flags in that word, while rocksdb-js knows `0x10000` only because
|
|
56
|
+
the native verification table gives that specific bit behavior. A bit with no native behavior or
|
|
57
|
+
public rocksdb-js contract belongs beside Harper's encoder and downgrade gate.
|
|
58
|
+
|
|
59
|
+
### Deeper cause
|
|
60
|
+
|
|
61
|
+
Remove `setTimestamp()` and redesign replay and replication so rocksdb-js always issues a new local
|
|
62
|
+
timestamp. Rejected because Harper's record-to-log lookup and per-origin resume cursor require the
|
|
63
|
+
origin transaction timestamp to remain the transaction-log key on every receiver.
|
|
64
|
+
|
|
65
|
+
A construction-time timestamp option was considered as a safer replacement for the setter. It
|
|
66
|
+
would add another public API and migrate the replay and replication callers, but could not remove
|
|
67
|
+
the existing setter compatibly; therefore it would not eliminate the state it is meant to prevent.
|
|
68
|
+
|
|
69
|
+
### Do less
|
|
70
|
+
|
|
71
|
+
Make no rocksdb-js change and rely on Harper always calling `setTimestamp()` before staging writes.
|
|
72
|
+
Rejected because the current public API permits a later call, after the producer has encoded the old
|
|
73
|
+
timestamp or the log batch has captured it, which can split one transaction across two identities.
|
|
74
|
+
|
|
75
|
+
For the separate clock-rollback concern, seeding the process clock from the largest durable key
|
|
76
|
+
across the database's transaction logs was considered and rejected on Harper's own log layout.
|
|
77
|
+
`resources/RocksTransactionLogStore.ts` opens one log per origin node — `useLog('local')` for locally
|
|
78
|
+
originated writes and `logById(nodeId)` for each peer — and a receiver deliberately keeps the origin's
|
|
79
|
+
clock as the key in the peer's log (HarperFast/harper-pro#790). A "largest key in the database" seed
|
|
80
|
+
therefore reads adopted peer keys as if they were locally issued: one peer running ahead ratchets this
|
|
81
|
+
node's clock on its next restart, and the node then originates those keys to its own peers. Reading
|
|
82
|
+
the headers only to warn has the same ambiguity — normal peer skew would be reported as a local clock
|
|
83
|
+
rollback. A correct floor has to be told which log is locally originated (a per-log opt-in, or an
|
|
84
|
+
explicit "raise the floor to this value" call Harper makes from its own `local` log), which is a
|
|
85
|
+
different API than the one the issue sketched and is not a prerequisite for the dual-version record
|
|
86
|
+
work.
|
|
87
|
+
|
|
88
|
+
### Chosen
|
|
89
|
+
|
|
90
|
+
Keep `setTimestamp()` as the adoption API, reject invalid timestamp values, and reject changes once
|
|
91
|
+
the transaction is no longer pending or has staged a write or log entry. Preserve the existing
|
|
92
|
+
retry behavior: a reset with no durable batch clears staged writes and may adopt again, while
|
|
93
|
+
`committedPosition` freezes a timestamp whose batch already landed. Remove the second-word read API,
|
|
94
|
+
flag, and clock-floor-at-open implementation from this PR. The first two belong to Harper's record
|
|
95
|
+
codec; the clock floor addresses a pre-existing restart/clock-rollback concern and, as specified,
|
|
96
|
+
would seed from adopted origin keys as readily as from locally issued ones. The existing
|
|
97
|
+
`latestTimestamp` behavior used to populate new transaction-log segment headers remains unchanged.
|
|
98
|
+
|
|
99
|
+
Document what is true about log keys today, which the issue also asked for: entries are appended in
|
|
100
|
+
commit order rather than timestamp order, an adopted key carries another node's clock, keys are
|
|
101
|
+
unique only within a process, and `findPositionByTimestamp` is a running-maxima seek rather than a
|
|
102
|
+
sorted binary search.
|
|
103
|
+
|
|
104
|
+
## Verification route
|
|
105
|
+
|
|
106
|
+
The existing transaction tests cover all transaction modes, invalid inputs, post-write and
|
|
107
|
+
post-dispatch rejection, log-entry staging, and a coordinated retry whose log batch is already
|
|
108
|
+
durable. README and AGENTS documentation will state the enforceable transaction-layer rule and the
|
|
109
|
+
producer-owned record boundary. The repository build, native tests, full Vitest suite, and checks
|
|
110
|
+
will run before push.
|
|
111
|
+
|
|
112
|
+
## Alternatives rejected
|
|
113
|
+
|
|
114
|
+
Freezing the timestamp on the first `getTimestamp()` call would break existing documented and tested
|
|
115
|
+
read-then-override behavior. A getter is observational for callers that do not encode its result,
|
|
116
|
+
and making every read a state transition still could not prove that a producer's record bytes match
|
|
117
|
+
the transaction timestamp. The enforced boundary therefore remains the first native staging event;
|
|
118
|
+
producer ordering is documented explicitly.
|
|
119
|
+
|
|
120
|
+
Retaining the removed clock-floor branch's far-future header bound would change the append path using
|
|
121
|
+
a ten-year policy introduced only for that floor. This change instead restores `latestTimestamp` and
|
|
122
|
+
segment-header behavior exactly to `origin/main`; any provenance-aware clock rollback design belongs
|
|
123
|
+
in separate work.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harperfast/rocksdb-js",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.1",
|
|
4
4
|
"description": "RocksDB binding for Node.js",
|
|
5
5
|
"homepage": "https://github.com/HarperFast/rocksdb-js",
|
|
6
6
|
"bugs": [
|
|
@@ -32,38 +32,38 @@
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@harperfast/extended-iterable": "1.0.3",
|
|
36
|
-
"msgpackr": "2.0.6",
|
|
37
|
-
"ordered-binary": "1.6.1"
|
|
35
|
+
"@harperfast/extended-iterable": "^1.0.3",
|
|
36
|
+
"msgpackr": "^2.0.6",
|
|
37
|
+
"ordered-binary": "^1.6.1"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@rollup/plugin-replace": "6.0.3",
|
|
41
|
-
"@types/node": "26.
|
|
41
|
+
"@types/node": "26.5.0",
|
|
42
42
|
"@vitest/coverage-v8": "4.1.11",
|
|
43
43
|
"cross-env": "10.1.0",
|
|
44
44
|
"dotenv": "17.4.2",
|
|
45
|
-
"lefthook": "2.1.
|
|
45
|
+
"lefthook": "2.1.12",
|
|
46
46
|
"lmdb": "3.5.6",
|
|
47
|
-
"node-gyp": "13.0.
|
|
48
|
-
"oxfmt": "0.
|
|
49
|
-
"oxlint": "1.
|
|
47
|
+
"node-gyp": "13.0.2",
|
|
48
|
+
"oxfmt": "0.67.0",
|
|
49
|
+
"oxlint": "1.82.0",
|
|
50
50
|
"prebuildify": "6.0.1",
|
|
51
51
|
"semver": "7.8.5",
|
|
52
52
|
"tar": "7.5.22",
|
|
53
|
-
"tsdown": "0.
|
|
53
|
+
"tsdown": "0.23.0",
|
|
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.9.1",
|
|
60
|
+
"@harperfast/rocksdb-js-darwin-x64": "2.9.1",
|
|
61
|
+
"@harperfast/rocksdb-js-linux-arm64-glibc": "2.9.1",
|
|
62
|
+
"@harperfast/rocksdb-js-linux-arm64-musl": "2.9.1",
|
|
63
|
+
"@harperfast/rocksdb-js-linux-x64-glibc": "2.9.1",
|
|
64
|
+
"@harperfast/rocksdb-js-linux-x64-musl": "2.9.1",
|
|
65
|
+
"@harperfast/rocksdb-js-win32-arm64": "2.9.1",
|
|
66
|
+
"@harperfast/rocksdb-js-win32-x64": "2.9.1"
|
|
67
67
|
},
|
|
68
68
|
"engines": {
|
|
69
69
|
"node": "^22.18.0 || >=24.0.0"
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"bench": "cross-env CI=1 node --expose-gc ./node_modules/vitest/vitest.mjs bench --passWithNoTests --outputJson benchmark-results.json",
|
|
77
77
|
"bench:bun": "cross-env CI=1 bun --bun bench",
|
|
78
78
|
"bench:deno": "cross-env CI=1 deno run --allow-all --sloppy-imports ./node_modules/vitest/vitest.mjs bench",
|
|
79
|
-
"build": "pnpm build:bundle && pnpm rebuild",
|
|
79
|
+
"build": "pnpm build:bundle && pnpm run rebuild",
|
|
80
80
|
"build:binding": "node-gyp build",
|
|
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",
|