@harperfast/rocksdb-js 2.7.1 → 2.9.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 +624 -45
- package/dist/index.cjs +730 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +752 -41
- package/dist/index.d.mts +752 -41
- package/dist/index.mjs +726 -47
- package/dist/index.mjs.map +1 -1
- package/docs/stats.md +355 -269
- package/docs/transaction-log.md +18 -5
- package/docs/transaction-timestamp-integrity-design.md +123 -0
- package/package.json +25 -26
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.0",
|
|
4
4
|
"description": "RocksDB binding for Node.js",
|
|
5
5
|
"homepage": "https://github.com/HarperFast/rocksdb-js",
|
|
6
6
|
"bugs": [
|
|
@@ -32,60 +32,59 @@
|
|
|
32
32
|
}
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@harperfast/extended-iterable": "1.0.3",
|
|
36
|
-
"msgpackr": "2.0.
|
|
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.1
|
|
42
|
-
"@vitest/coverage-v8": "4.1.
|
|
41
|
+
"@types/node": "26.4.1",
|
|
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.66.0",
|
|
49
|
+
"oxlint": "1.81.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
|
-
"tsx": "4.23.5",
|
|
56
55
|
"typescript": "7.0.2",
|
|
57
|
-
"vitest": "4.1.
|
|
56
|
+
"vitest": "4.1.11"
|
|
58
57
|
},
|
|
59
58
|
"optionalDependencies": {
|
|
60
|
-
"@harperfast/rocksdb-js-darwin-arm64": "2.
|
|
61
|
-
"@harperfast/rocksdb-js-darwin-x64": "2.
|
|
62
|
-
"@harperfast/rocksdb-js-linux-arm64-glibc": "2.
|
|
63
|
-
"@harperfast/rocksdb-js-linux-arm64-musl": "2.
|
|
64
|
-
"@harperfast/rocksdb-js-linux-x64-glibc": "2.
|
|
65
|
-
"@harperfast/rocksdb-js-linux-x64-musl": "2.
|
|
66
|
-
"@harperfast/rocksdb-js-win32-arm64": "2.
|
|
67
|
-
"@harperfast/rocksdb-js-win32-x64": "2.
|
|
59
|
+
"@harperfast/rocksdb-js-darwin-arm64": "2.9.0",
|
|
60
|
+
"@harperfast/rocksdb-js-darwin-x64": "2.9.0",
|
|
61
|
+
"@harperfast/rocksdb-js-linux-arm64-glibc": "2.9.0",
|
|
62
|
+
"@harperfast/rocksdb-js-linux-arm64-musl": "2.9.0",
|
|
63
|
+
"@harperfast/rocksdb-js-linux-x64-glibc": "2.9.0",
|
|
64
|
+
"@harperfast/rocksdb-js-linux-x64-musl": "2.9.0",
|
|
65
|
+
"@harperfast/rocksdb-js-win32-arm64": "2.9.0",
|
|
66
|
+
"@harperfast/rocksdb-js-win32-x64": "2.9.0"
|
|
68
67
|
},
|
|
69
68
|
"engines": {
|
|
70
69
|
"node": "^22.18.0 || >=24.0.0"
|
|
71
70
|
},
|
|
72
71
|
"gypfile": true,
|
|
73
72
|
"rocksdb": {
|
|
74
|
-
"version": "11.1
|
|
73
|
+
"version": "11.8.1"
|
|
75
74
|
},
|
|
76
75
|
"scripts": {
|
|
77
76
|
"bench": "cross-env CI=1 node --expose-gc ./node_modules/vitest/vitest.mjs bench --passWithNoTests --outputJson benchmark-results.json",
|
|
78
77
|
"bench:bun": "cross-env CI=1 bun --bun bench",
|
|
79
78
|
"bench:deno": "cross-env CI=1 deno run --allow-all --sloppy-imports ./node_modules/vitest/vitest.mjs bench",
|
|
80
|
-
"build": "pnpm build:bundle && pnpm rebuild",
|
|
79
|
+
"build": "pnpm build:bundle && pnpm run rebuild",
|
|
81
80
|
"build:binding": "node-gyp build",
|
|
82
81
|
"build:binding:debug": "node-gyp build --coverage --debug --verbose",
|
|
83
82
|
"build:bundle": "node scripts/clean-dist.mjs && tsdown -c tsdown.config.ts",
|
|
84
83
|
"build:bundle:minify": "cross-env MINIFY=1 pnpm build:bundle",
|
|
85
84
|
"check": "pnpm type-check && pnpm lint && pnpm fmt:check",
|
|
86
85
|
"clean": "node-gyp clean",
|
|
87
|
-
"coverage": "
|
|
88
|
-
"coverage:native": "
|
|
86
|
+
"coverage": "node scripts/coverage/main.ts",
|
|
87
|
+
"coverage:native": "node scripts/native-test/coverage.mjs",
|
|
89
88
|
"fmt": "oxfmt",
|
|
90
89
|
"fmt:check": "oxfmt --check",
|
|
91
90
|
"lint": "oxlint",
|
|
@@ -95,7 +94,7 @@
|
|
|
95
94
|
"test:bun": "cross-env CI=1 bun --bun --config=bunfig.toml ./node_modules/vitest/vitest.mjs --reporter=verbose",
|
|
96
95
|
"test:deno": "cross-env CI=1 deno run --allow-all --sloppy-imports --v8-flags=--expose-gc ./node_modules/vitest/vitest.mjs",
|
|
97
96
|
"test:stress": "cross-env CI=1 node --expose-gc ./node_modules/vitest/vitest.mjs --reporter=verbose --config=vitest-stress.config.ts",
|
|
98
|
-
"test:native": "
|
|
97
|
+
"test:native": "node scripts/native-test/run.mjs",
|
|
99
98
|
"type-check": "tsc --noEmit"
|
|
100
99
|
}
|
|
101
100
|
}
|