@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 CHANGED
@@ -46,6 +46,19 @@ Creates a new database instance.
46
46
  - `path: string` The path to write the database files to. This path does not need to exist, but the
47
47
  parent directories do.
48
48
  - `options: object` [optional]
49
+ - `arenaBlockSize: number` The size in bytes of the blocks a column family's memtable allocates
50
+ from. Every column family that has been written to holds at least one block, and with a
51
+ `writeBufferManagerSize` set, those blocks count against that shared budget even when the
52
+ column family is idle — so with many column families, large blocks can use up most of a small
53
+ budget and cause constant tiny flushes. Defaults to `0`, which picks a size automatically: with
54
+ a write buffer manager, `writeBufferManagerSize / 2048`, but at least 64 KiB and at most
55
+ `writeBufferSize / 8` or 1 MiB, whichever is smaller (a 64 MiB budget gives 64 KiB, 512 MiB
56
+ gives 256 KiB, 2 GiB or more gives 1 MiB); without one, RocksDB's own default of
57
+ `writeBufferSize / 8` capped at 1 MiB (1 MiB at the default `writeBufferSize`). The automatic
58
+ size uses the manager's budget at the time the column family is opened. An explicit value is
59
+ used as given, except that RocksDB raises one below 4 KiB to 4 KiB. Like `writeBufferSize`,
60
+ this applies to every column family when a database is first opened in the process and to
61
+ column families created later; a column family that is already open keeps its size.
49
62
  - `compression: string | { algorithm: string, level?: number }` The block/blob compression
50
63
  algorithm for this column family. Pass an algorithm name — one of `'none'`, `'snappy'`,
51
64
  `'zlib'`, `'bzip2'`, `'lz4'`, `'lz4hc'`, or `'zstd'` — or an object with an
@@ -119,9 +132,10 @@ Creates a new database instance.
119
132
  - `readOnly: boolean` When `true`, the database is opened in read-only mode. Read operations are
120
133
  permitted. Write operations will throw an error with code `ERR_DATABASE_READONLY`. Transactions
121
134
  are a no-op in read-only mode. [`flush()`](#dbflushoptions-promisevoid),
122
- [`flushSync()`](#dbflushsyncoptions-void), [`compact()`](#dbcompactoptions-promisevoid) and
123
- [`compactSync()`](#dbcompactsyncoptions-void) are the exception: they have nothing to do when
124
- there are no writes, so they succeed as no-ops rather than throwing.
135
+ [`flushSync()`](#dbflushsyncoptions-void), [`compact()`](#dbcompactoptions-promisevoid),
136
+ [`compactSync()`](#dbcompactsyncoptions-void) and [`syncWal()`](#dbsyncwal-promisevoid) are the
137
+ exception: they have nothing to do when there are no writes, so they succeed as no-ops rather
138
+ than throwing.
125
139
 
126
140
  A read-only open is a **point-in-time snapshot**: it never sees later writes. It is safe
127
141
  against a quiescent database, but **unsafe against a live writer**: the open holds no reference
@@ -276,6 +290,17 @@ Sets global database settings.
276
290
  callers could already produce it. With `1`, a database's async commits run one at a time in
277
291
  the order `commit()` was called. Databases already open keep their limit.
278
292
  - `compactOnClose: boolean` When `true`, compacts the database on close. Defaults to `false`.
293
+ - `transactionLogAbortMarkers: boolean` When `true`, a transaction that wrote its
294
+ transaction-log batch and then ends without committing (an `abort()`, a commit error it gives
295
+ up on, or a worker torn down with it pending) gets that batch marked aborted in place, and every
296
+ reader skips it; see [Abort markers](#abort-markers). Defaults to `false`, which behaves as
297
+ before. Read when a transaction starts and when it is reset for a retry.
298
+ - `transactionLogWriteIntents: boolean` When `true` with `transactionLogAbortMarkers` on, a
299
+ conflicting attempt is refused before it writes its batch when an older transaction holds a
300
+ write intent on one of its keys; see [Abort markers](#abort-markers). This shortens the worst
301
+ waits of transactions that overlap heavily, but adds retries and costs throughput when
302
+ transactions are large or contention is low. Defaults to `false`. Needs `verificationTable`.
303
+ Read with `transactionLogAbortMarkers`.
279
304
  - `occValidation: 'parallel' | 'serial'` How writable optimistic databases opened after the call
280
305
  validate commits for conflicts. Defaults to `'parallel'` (RocksDB's default). Databases already
281
306
  open keep their policy. Pessimistic, read-only and secondary opens are unaffected.
@@ -328,6 +353,19 @@ Sets global database settings.
328
353
  [`db.isWriteStalled()`](#dbiswritestalled-boolean) — see
329
354
  [`getWriteBufferManagerStats()`](#getwritebuffermanagerstats-writebuffermanagerstats),
330
355
  which also describes the watchdog that logs a sustained stall.
356
+ - `writeBufferManagerFlushPolicy: 'oldest' | 'largest' | 'largestAcrossDBs'` What the write
357
+ buffer manager flushes when it is over budget. `'oldest'` flushes the oldest memtable of the
358
+ database that is being written; `'largest'` its largest memtable (databases here are opened with
359
+ atomic flush, so this currently behaves like `'oldest'`); `'largestAcrossDBs'` has a background
360
+ thread flush whichever database holds the most memtable memory, whether or not it is being
361
+ written. When not set, the policy is `'largestAcrossDBs'` while
362
+ `writeBufferManagerAllowStall` is `true` and `'oldest'` otherwise, and it follows later changes
363
+ to `writeBufferManagerAllowStall`. With stalls allowed, `'oldest'` can stop writes permanently:
364
+ a write to one database waits for memory held by other databases that nothing is writing to,
365
+ so nothing flushes them. Without stalls, `'largestAcrossDBs'` has measured slower. Can be
366
+ changed while databases are open. Once set, the value stays in effect for the life of the
367
+ process; it is reported as `flushPolicy` by
368
+ [`getWriteBufferManagerStats()`](#getwritebuffermanagerstats-writebuffermanagerstats).
331
369
  - `writeBufferManagerCostToCache: boolean` When `true`, memtable memory is "charged" against the
332
370
  shared block cache so the block cache and write buffers draw from a single pool. During write
333
371
  bursts the cache shrinks to make room for memtables; once memtables flush, the cache can grow
@@ -370,6 +408,8 @@ where the call was made.
370
408
  - `mutableMemoryUsage: number` The share of `memoryUsage` held by active (mutable) memtables; the
371
409
  rest is memtables awaiting flush plus retained write history.
372
410
  - `allowStall: boolean`, `costToCache: boolean` The manager's configuration.
411
+ - `flushPolicy: 'oldest' | 'largest' | 'largestAcrossDBs'` The flush policy in effect (see
412
+ `writeBufferManagerFlushPolicy`); before a manager exists, the policy one would be created with.
373
413
  - `stallActive: boolean` Whether the manager is currently stalling writes.
374
414
  - `stallActiveMs: number` How long the current stall has been active; `0` when not stalled.
375
415
  Sampled once a second by the watchdog, so `0` for a stall's first second and whenever
@@ -388,7 +428,7 @@ where the call was made.
388
428
 
389
429
  `bufferSize`, `memoryUsage`, `mutableMemoryUsage`, `stallActive` and `stallActiveMs` are also in
390
430
  [`db.getStats()`](#dbgetstatsall-boolean-rocksdbstats) and `db.getStat()` under the same
391
- `writeBufferManager.` prefix, for scraping; `enabled`, `allowStall`, `costToCache`,
431
+ `writeBufferManager.` prefix, for scraping; `enabled`, `allowStall`, `costToCache`, `flushPolicy`,
392
432
  `watchdogRunning`, and the column-family inventory are only here, because collecting them either
393
433
  walks the database registry or isn't scrape-shaped.
394
434
 
@@ -806,6 +846,22 @@ db.flushSync({ allowWriteStall: true });
806
846
  On a [read-only](#new-rocksdatabasepath-options) database it validates its options and then returns
807
847
  without flushing, rather than throwing.
808
848
 
849
+ ### `db.syncWal(): Promise<void>`
850
+
851
+ Syncs the write-ahead log to stable storage (`DB::SyncWAL`), so every WAL-backed write committed
852
+ before the call survives a power loss afterwards. Unlike [`flush()`](#dbflushoptions-promisevoid) it
853
+ writes no SST file and never stalls writers. Writes that skipped the WAL (`disableWAL`) and
854
+ transaction-log files, which keep their own durability, are not covered. RocksDB serializes
855
+ concurrent syncs of one WAL, so keep one in flight per database rather than queueing them on the
856
+ libuv threadpool.
857
+
858
+ ```typescript
859
+ await db.syncWal();
860
+ ```
861
+
862
+ On a [read-only](#new-rocksdatabasepath-options) database the returned promise resolves without
863
+ syncing.
864
+
809
865
  ### `db.get(key: Key, options?: GetOptions): MaybePromise<any>`
810
866
 
811
867
  Retreives the value for a given key. If the key does not exist, it will resolve `undefined`.
@@ -1250,7 +1306,9 @@ The `attempt` parameter is the number of times the transaction has been retried.
1250
1306
  - `disableSnapshot?: boolean` Whether to disable snapshots. Defaults to `false`.
1251
1307
  - `maxRetries?: number` The maximum number of times to retry the transaction. Defaults to `3`.
1252
1308
  - `retryOnBusy?: boolean` Whether to retry the transaction if the commit fails with `IsBusy`.
1253
- Defaults to `true` when the transaction is bound to a transaction log, otherwise `false`.
1309
+ Defaults to `true` when the transaction wrote a transaction-log batch or was refused before
1310
+ writing one (`refusedBeforeLog`), otherwise `false`. With `false`, `db.transaction()` also
1311
+ commits without `coordinatedRetry`, so a conflict fails at once instead of waiting first.
1254
1312
 
1255
1313
  ### Transaction Retry Logic
1256
1314
 
@@ -1263,7 +1321,45 @@ a bad state and the transaction will need to be retried. If the max retries is r
1263
1321
  transaction is not retried, a `ERR_TRANSACTION_ABANDONED` error will be thrown.
1264
1322
 
1265
1323
  Users should use the `attempt` transaction callback parameter to ensure duplicate transaction log
1266
- entries are not added.
1324
+ entries are not added. `txn.logWritten()` reports whether the batch is already in the log: a commit
1325
+ that conflicts after writing it keeps it for the retry, while an attempt refused before writing it
1326
+ (`err.refusedBeforeLog`, with `transactionLogAbortMarkers`) leaves nothing and must stage it again.
1327
+
1328
+ ### Abort markers
1329
+
1330
+ With `RocksDatabase.config({ transactionLogAbortMarkers: true })`, a transaction's log batch is
1331
+ never left live when the transaction does not commit:
1332
+
1333
+ - A transaction that wrote its batch and then ends without committing has the batch marked aborted:
1334
+ bit `0x02` (`TRANSACTION_LOG_ENTRY_ABORTED_FLAG`) is set on its first entry, and the change is
1335
+ synced to disk before `abort()` returns or the batch stops holding back committed readers. Every
1336
+ reader skips the batch. `abort()` then no longer throws `ERR_TRANSACTION_ABANDONED`.
1337
+ - If the marker cannot be written or synced, `abort()` throws `ERR_TRANSACTION_ABORT_UNRESOLVED`
1338
+ (again on a second call), committed readers stay behind the batch, and the log refuses new batches
1339
+ until a retry on the next batch, database flush or close marks it.
1340
+ - Under `occValidation: 'parallel'` (the default), a retried optimistic commit that RocksDB's own
1341
+ conflict check shows would fail is refused before it writes its batch too, with
1342
+ `refusedBeforeLog` set (needs the RocksDB build this package pins). A first attempt skips the
1343
+ check, so commits that do not conflict pay nothing for it, and so does a retry whose batch an
1344
+ earlier attempt already wrote.
1345
+ - With `transactionLogWriteIntents: true` as well, a commit that finds an older transaction holding
1346
+ a write intent on one of its keys (with `verificationTable: true`) is refused before writing its
1347
+ batch, with `refusedBeforeLog` set on the
1348
+ error, or, if its batch is already written from an earlier attempt, before committing. With
1349
+ `coordinatedRetry` it waits for that transaction first. `commitSync()` never waits: it throws, and
1350
+ `transactionSync()` retries after a short backoff.
1351
+ - `txn.setParkDeadline(epochMs)` bounds those waits by the caller's own deadline.
1352
+ - `log.getStats().totals` reports `abortsMarked`, `abortMarkFailures`, and `watermarkPinnedUs` /
1353
+ `pinnedTransactions`: how long logged batches held committed readers back after their first
1354
+ conflict, until they committed or ended.
1355
+ - A backup waits for every batch it captures that has neither committed nor been aborted, and is
1356
+ refused if one is still unresolved after `ROCKSDB_JS_BACKUP_UNRESOLVED_WAIT_MS` (default
1357
+ `30000`).
1358
+
1359
+ A reader older than the release that added aborted-batch support shows an aborted batch as live, so
1360
+ turn this on only once every reader of the logs (replicas, restores, repair tools) has it. A batch
1361
+ whose transaction was still unresolved when the process crashed is not marked, and is replayed as
1362
+ before.
1267
1363
 
1268
1364
  When `coordinatedRetry: true`, the retry behavior changes for `IsBusy` conflicts: instead of
1269
1365
  rejecting (or retrying immediately and potentially conflicting again), the commit waits until the
@@ -1297,6 +1393,12 @@ operations methods as the `RocksDatabase` instance plus:
1297
1393
  - `txn.commitSync()` Synchronously commits and closes the transaction.
1298
1394
  - `txn.getTimestamp(): number` Retrieves the transaction timestamp in milliseconds as a decimal. It
1299
1395
  defaults to a process-wide monotonic value assigned when the transaction was created.
1396
+ - `txn.lastConflictKind(): number` Why the most recent attempt was refused or retried, one of
1397
+ `ConflictKind`: `none` (0), `intentBeforeLog` (1), `intentAfterLog` (2), `atCommit` (3),
1398
+ `checkBeforeLog` (4).
1399
+ - `txn.logWritten(): boolean` Whether the transaction's batch is in the transaction log.
1400
+ - `txn.setParkDeadline(epochMs: number): void` Past this time a coordinated-retry wait resolves at
1401
+ once. `0` clears it.
1300
1402
  - `txn.id: number` The read-only transaction ID. Transaction IDs are unique to the RocksDB database
1301
1403
  path, regardless the database name/column family.
1302
1404
  - `txn.setTimestamp(ts?: number): void` Overrides the transaction timestamp in milliseconds. If
@@ -1937,6 +2039,10 @@ Attempts to acquire a lock for a given key. If the lock is available, the functi
1937
2039
  and the optional `onUnlocked` callback is never called. If the lock is not available, the function
1938
2040
  returns `false` and the `onUnlocked` callback is queued until the lock is released.
1939
2041
 
2042
+ A queued `onUnlocked` callback keeps the thread's event loop alive until it is called, even when the
2043
+ lock is held by another thread. A thread with a callback queued on a lock that is never released
2044
+ will not exit on its own; closing the database that queued the callback does not cancel it.
2045
+
1940
2046
  When a database is closed, all locks associated to it will be unlocked.
1941
2047
 
1942
2048
  ```typescript
@@ -1977,7 +2083,9 @@ db.unlock('foo'); // false, already unlocked
1977
2083
 
1978
2084
  ### `db.withLock(key: Key, callback: () => void | Promise<void>): Promise<void>`
1979
2085
 
1980
- Runs a function with guaranteed exclusive access across all threads.
2086
+ Runs a function with guaranteed exclusive access across all threads. The returned promise rejects
2087
+ with the error if the callback throws or its promise rejects; the lock is released either way. A
2088
+ pending `withLock()` keeps the thread's event loop alive until its callback has run.
1981
2089
 
1982
2090
  ```typescript
1983
2091
  await db.withLock('key', async () => {
@@ -2005,9 +2113,7 @@ await Promise.all([
2005
2113
  ]);
2006
2114
  ```
2007
2115
 
2008
- Note: If the `callback` throws an error, Node.js suppress the error. Node.js 18.3.0 introduced a
2009
- `--force-node-api-uncaught-exceptions-policy` flag which will cause errors to emit the
2010
- `'uncaughtException'` event. Future Node.js releases will enable this flag by default.
2116
+ Note: Errors thrown by a `tryLock()` `onUnlocked` callback are discarded.
2011
2117
 
2012
2118
  ## File Locking
2013
2119
 
@@ -2199,20 +2305,21 @@ const names = db.listLogs();
2199
2305
 
2200
2306
  ### `db.purgeLogs({ includeEntryCounts: true, ...options }): { path: string; entries: number }[]`
2201
2307
 
2202
- Deletes transaction log files older than the `transactionLogRetention` (defaults to 3 days).
2203
- Ordinary retention keeps the sequence file named by `txn.state` and every newer file as the live
2204
- store's retention floor, or the highest sequence file when there is no persisted flush position. It
2205
- removes only an eligible contiguous prefix below that floor. An idle store can therefore retain one
2206
- file past the cutoff until a later write rotates and flushes it; that extra file is bounded by the
2207
- store's `transactionLogMaxSize` (except when a single transaction exceeds the target). That bound
2208
- covers only the floor file: when the flush position lags — `txn.state` stuck at an old sequence
2209
- because RocksDB flushing is behind — every file above the floor is retained too, which
2210
- `transactionLogMaxSize` does not bound. `purge.retainedUnflushedFiles` reports that case. Use
2211
- `destroy: true` only to remove the store itself.
2308
+ Deletes transaction log files older than the `transactionLogRetention` (defaults to 3 days). A file
2309
+ is deleted only when it lies entirely at or before the flushed position recorded in `txn.state`, so
2310
+ every entry in it is already in RocksDB; that includes the file `txn.state` names and the file
2311
+ currently being written, so an idle, fully flushed store is left with no log files. Retention removes
2312
+ a contiguous prefix: the first file that is too new or holds an unflushed entry stops the run. When
2313
+ the flush position lags — `txn.state` stuck at an old sequence because RocksDB flushing is behind —
2314
+ every file after it is retained regardless of age; `purge.retainedUnflushedFiles` reports that case.
2315
+ While a backup with `transactionLogs` is copying a store's files, purges of that store are skipped
2316
+ (`purge.retentionPins` reports it). On a filesystem that rejects a directory fsync, the store's last
2317
+ file is kept, since `txn.state` could not be made durable before deleting it.
2318
+ Use `destroy: true` only to remove the store itself.
2212
2319
 
2213
2320
  - `options: object`
2214
2321
  - `before?: number` Remove transaction log files older than the specified timestamp, subject to
2215
- the retention floor and contiguous-prefix rules above.
2322
+ the flushed-position and contiguous-prefix rules above.
2216
2323
  - `destroy?: boolean` When `true`, deletes transaction log stores including all log sequence files
2217
2324
  on disk.
2218
2325
  - `includeEntryCounts?: boolean` When `true`, counts the entries in each deleted log file and
@@ -2341,6 +2448,22 @@ The iterator produces an object with the log entry timestamp and data.
2341
2448
  - `timestamp: number` The entry timestamp used to collate entries by transaction.
2342
2449
  - `endTxn: boolean` This is `true` when the entry is the last entry in a transaction.
2343
2450
 
2451
+ `next()` can throw two errors that leave the iterator usable, so calling `next()` again continues
2452
+ past the problem. Other errors, such as a failure to open a segment, do not move the iterator, so
2453
+ retrying them without a fix fails the same way.
2454
+
2455
+ - `TransactionLogGapError`: the iterator reached segments it had not read yet and could not read
2456
+ them, usually because retention purged them while the reader lagged. Their entries are not in
2457
+ this log anymore, so a consumer that replicates the log should treat its copy as discontinuous
2458
+ (for example, by resyncing from a full copy) rather than continuing. Properties: `logId` (the
2459
+ last segment read), `resumeLogId` (the segment the next `next()` call reads), and
2460
+ `unavailableLogCount`. A new query positioned within the retained segments does not report the
2461
+ segments purged before its starting point.
2462
+ - `CorruptFrameError` (a `RangeError`): an entry's framing is broken. `resyncPosition` is where
2463
+ valid framing resumes, or `undefined` when nothing follows the break.
2464
+
2465
+ Both classes are exported from the package.
2466
+
2344
2467
  ```typescript
2345
2468
  const log = db.useLog('foo');
2346
2469
  const iter = log.query({});
@@ -2386,9 +2509,8 @@ stats.totals.transactionsWritten; // lifetime count of transactions written
2386
2509
 
2387
2510
  The `purge.retainedUnflushedFiles` gauge is useful for diagnosing why logs are not being cleaned
2388
2511
  up: a file can be older than the retention period but still retained because its transactions have
2389
- not yet been flushed to RocksDB (purging it would be unsafe for crash recovery). The retention
2390
- floor — the sequence named by `txn.state`, or the highest sequence when there is no persisted flush
2391
- position — is not counted as purgeable even when it is old and fully flushed.
2512
+ not yet been flushed to RocksDB (purging it would be unsafe for crash recovery). An old, fully
2513
+ flushed current file counts as purgeable.
2392
2514
 
2393
2515
  ### Transaction Log Initialization
2394
2516
 
@@ -2495,7 +2617,7 @@ Contract:
2495
2617
  `QueryPerformanceCounter` on Windows).
2496
2618
  - **Origin and lifetime**: the origin is unspecified and fixed for the life of the process. Every
2497
2619
  sample taken in the process is in one domain — the main thread and every `worker_threads` worker,
2498
- including workers started or restarted at any later time, under Node, Bun and Deno alike. Samples
2620
+ including workers started or restarted at any later time, under Node and Bun alike. Samples
2499
2621
  are not meaningful across processes or restarts, and are unrelated to the Unix epoch: never compare
2500
2622
  them with `Date.now()`, `db.getMonotonicTimestamp()` or transaction timestamps.
2501
2623
  - **Monotonic, not unique**: a sample taken after another (in real time, on any thread) is `>=` it.
@@ -2789,7 +2911,7 @@ backup references them, so this is not equivalent to deleting files manually.
2789
2911
 
2790
2912
  Deletes all but the newest `keepCount` backups.
2791
2913
 
2792
- ### `backups.verify(backupDir: string, backupId: number, options?: { verifyWithChecksum?: boolean; verifyTransactionLogs?: boolean }): Promise<void>`
2914
+ ### `backups.verify(backupDir: string, backupId: number, options?: { verifyWithChecksum?: boolean; verifyTables?: boolean; verifyTransactionLogs?: boolean }): Promise<void>`
2793
2915
 
2794
2916
  Verifies a backup's file sizes, and optionally their checksums (which requires reading all
2795
2917
  backed-up data). Resolves if the backup is intact and rejects otherwise.
@@ -2800,8 +2922,17 @@ When the backup was created with `transactionLogs: true`, the backup's transacti
2800
2922
  log file's header and entry framing must be intact (snapshots are copied on committed entry
2801
2923
  boundaries, so even a torn tail fails verification). Set `verifyTransactionLogs: false` to skip.
2802
2924
 
2925
+ Set `verifyTables: true` (default `false`) to also prove this build can read the backup's table
2926
+ files before you restore it. Each SST is opened in turn, one at a time, so file-descriptor use does
2927
+ not grow with the number of tables. Verification rejects, naming the file, when this build cannot
2928
+ decode a table's footer (for example a `format_version` written by a newer RocksDB) or its
2929
+ properties, or when the table records a compression algorithm this build does not support (see
2930
+ [`supportedCompression`](#supportedcompression)). Checksums cannot catch either case: the file is
2931
+ intact, just unreadable here, and RocksDB would otherwise fail on the first read after a restore.
2932
+ Blob files are not checked.
2933
+
2803
2934
  ```typescript
2804
- await backups.verify('/path/to/backups', 1, { verifyWithChecksum: true });
2935
+ await backups.verify('/path/to/backups', 1, { verifyWithChecksum: true, verifyTables: true });
2805
2936
  ```
2806
2937
 
2807
2938
  ## Custom Store