@harperfast/rocksdb-js 2.4.1 → 2.6.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 +224 -15
- package/bin/rocksdb-js.mjs +67 -3
- package/dist/index.cjs +293 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1382 -1125
- package/dist/index.d.mts +1382 -1125
- package/dist/index.mjs +294 -41
- package/dist/index.mjs.map +1 -1
- package/docs/backups.md +15 -0
- package/docs/stats.md +2 -0
- package/docs/transaction-log.md +10 -0
- package/package.json +17 -17
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ A Node.js binding for the RocksDB library.
|
|
|
10
10
|
- Transaction log system for recording transaction related data
|
|
11
11
|
- Custom stores provide ability to override default database interactions
|
|
12
12
|
- Efficient binary key and value encoding
|
|
13
|
+
- Configurable block/blob compression (LZ4, Zstd, Zlib, and more)
|
|
13
14
|
- Access to internal RocksDB statistics
|
|
14
15
|
- Designed for Node.js and Bun on Linux, macOS, and Windows
|
|
15
16
|
|
|
@@ -43,10 +44,26 @@ Creates a new database instance.
|
|
|
43
44
|
- `path: string` The path to write the database files to. This path does not need to exist, but the
|
|
44
45
|
parent directories do.
|
|
45
46
|
- `options: object` [optional]
|
|
47
|
+
- `compression: string | { algorithm: string, level?: number }` The block/blob compression
|
|
48
|
+
algorithm for this column family. Pass an algorithm name — one of `'none'`, `'snappy'`,
|
|
49
|
+
`'zlib'`, `'bzip2'`, `'lz4'`, `'lz4hc'`, or `'zstd'` — or an object with an
|
|
50
|
+
`algorithm` and an optional `level` (forwarded to RocksDB's `compression_opts.level`; the
|
|
51
|
+
meaning is algorithm-specific). Applies to both SST data blocks and blob files (large values).
|
|
52
|
+
Defaults to `'lz4'` when the native build supports it, otherwise RocksDB's own default (Snappy
|
|
53
|
+
when linked, else no compression). See [Compression](#compression). Throws if the algorithm is
|
|
54
|
+
not compiled into the native build — check [`supportedCompression`](#supportedcompression) for
|
|
55
|
+
the available list.
|
|
46
56
|
- `disableWAL: boolean` Whether to disable the RocksDB write ahead log. Defaults to `false`.
|
|
47
57
|
- `enableStats: boolean` When `true` and the database is open, RocksDB will captures stats that
|
|
48
58
|
are retrieved by calling `db.getStats()`. Enabling statistics imposes 5-10% in overhead.
|
|
49
59
|
Defaults to `false`.
|
|
60
|
+
- `maxOpenFiles: number` The maximum number of table files RocksDB keeps open. `0` (the default)
|
|
61
|
+
derives a budget from the effective per-process open-file limit (an eighth of the limit —
|
|
62
|
+
several databases can share one process — clamped to `[1024, 262144]`); `-1` holds every table
|
|
63
|
+
file open (the RocksDB default, which can exhaust the process file-descriptor limit when
|
|
64
|
+
compaction falls behind under sustained ingest); a positive `int32` is an explicit cap. Reads
|
|
65
|
+
only pay a reopen cost when the number of live table files exceeds the budget, so raise the
|
|
66
|
+
process fd limit (and with it the derived budget) for very large databases.
|
|
50
67
|
- `name: string` The column family name. Defaults to `"default"`.
|
|
51
68
|
- `noBlockCache: boolean` When `true`, disables the block cache. Block caching is enabled by
|
|
52
69
|
default and the cache is shared across all database instances.
|
|
@@ -60,7 +77,8 @@ Creates a new database instance.
|
|
|
60
77
|
- `statsLevel: StatsLevel` Controls which type of statistics to skip and reduce statistic
|
|
61
78
|
overhead. Defaults to `StatsLevel.ExceptDetailedTimers`.
|
|
62
79
|
- `store: Store` A custom store that handles all interaction between the `RocksDatabase` or
|
|
63
|
-
`Transaction` instances and the native database interface.
|
|
80
|
+
`Transaction` instances and the native database interface. A store is bound to a single
|
|
81
|
+
`RocksDatabase` instance and cannot be shared between them. See [Custom Store](#custom-store) for
|
|
64
82
|
more information.
|
|
65
83
|
- `transactionLogMaxAgeThreshold: number` The threshold for the transaction log file's last
|
|
66
84
|
modified time to be older than the retention period before it is rotated to the next sequence
|
|
@@ -102,6 +120,18 @@ const db2 = new RocksDatabase('path/to/db', { name: 'users' });
|
|
|
102
120
|
console.log(db.columns); // ['default', 'users']
|
|
103
121
|
```
|
|
104
122
|
|
|
123
|
+
### `db.compression: { algorithm: string, level?: number }`
|
|
124
|
+
|
|
125
|
+
Returns the compression currently in effect for this database's column family, read live from
|
|
126
|
+
RocksDB. `algorithm` is a friendly name (e.g. `'lz4'`, `'zstd'`, `'none'`); `level` is present only
|
|
127
|
+
when a non-default compression level is set. The database must be open. See
|
|
128
|
+
[Compression](#compression).
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
const db = RocksDatabase.open('path/to/db', { compression: { algorithm: 'zstd', level: 3 } });
|
|
132
|
+
console.log(db.compression); // { algorithm: 'zstd', level: 3 }
|
|
133
|
+
```
|
|
134
|
+
|
|
105
135
|
### `db.config(options)`
|
|
106
136
|
|
|
107
137
|
Sets global database settings.
|
|
@@ -904,6 +934,77 @@ An object is a record with the following properties:
|
|
|
904
934
|
- `standardDeviation: number` A double containing the standard deviation.
|
|
905
935
|
- `sum: number` An unsigned 64-bit integer containing the sum of all values.
|
|
906
936
|
|
|
937
|
+
## Compression
|
|
938
|
+
|
|
939
|
+
RocksDB compresses data blocks in SST files (and, in `rocksdb-js`, blob files that hold large
|
|
940
|
+
values) using a configurable algorithm. Compression trades CPU for disk space and, because it
|
|
941
|
+
reduces bytes read from disk, often improves read throughput on I/O-bound workloads.
|
|
942
|
+
|
|
943
|
+
The following algorithm names are recognized. Which ones are actually usable depends on the native
|
|
944
|
+
build — see **Availability** below.
|
|
945
|
+
|
|
946
|
+
| Name | Notes |
|
|
947
|
+
| -------- | ----------------------------------------------------------------------------- |
|
|
948
|
+
| `none` | No compression. Always available. |
|
|
949
|
+
| `snappy` | Fast, modest ratio. RocksDB's own stock default when linked. |
|
|
950
|
+
| `zlib` | Higher ratio, slower. Supports `level` (see zlib's manual). |
|
|
951
|
+
| `bzip2` | High ratio, slow; rarely worth it over zstd. |
|
|
952
|
+
| `lz4` | Very fast, modest ratio. `rocksdb-js` default when available. |
|
|
953
|
+
| `lz4hc` | LZ4 high-compression variant: better ratio, slower writes, same fast reads. |
|
|
954
|
+
| `zstd` | Best ratio-for-speed of the set; supports `level` (higher = smaller, slower). |
|
|
955
|
+
|
|
956
|
+
Set the algorithm per column family with the `compression` option when opening a database:
|
|
957
|
+
|
|
958
|
+
```typescript
|
|
959
|
+
// Algorithm name
|
|
960
|
+
const db = RocksDatabase.open('/path/to/db', { compression: 'zstd' });
|
|
961
|
+
|
|
962
|
+
// Or an object with an explicit level
|
|
963
|
+
const db2 = RocksDatabase.open('/path/to/db2', { compression: { algorithm: 'zstd', level: 3 } });
|
|
964
|
+
|
|
965
|
+
// Disable compression entirely
|
|
966
|
+
const db3 = RocksDatabase.open('/path/to/db3', { compression: 'none' });
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
**Default.** When `compression` is omitted, `rocksdb-js` defaults to **LZ4** if the native build
|
|
970
|
+
supports it. LZ4 is fast with a modest compression ratio, making it a good general-purpose default.
|
|
971
|
+
If LZ4 is not compiled in, the default falls back to RocksDB's own default — Snappy when it is
|
|
972
|
+
linked, otherwise no compression. (RocksDB's stock default is Snappy; `rocksdb-js` overrides it to
|
|
973
|
+
LZ4.) Read the algorithm actually in effect with the `db.compression` getter.
|
|
974
|
+
|
|
975
|
+
**Availability.** The set of algorithms depends on which compression libraries the native binding
|
|
976
|
+
was compiled against, so it varies by build. Always check
|
|
977
|
+
[`supportedCompression`](#supportedcompression) at runtime — opening with an unavailable algorithm
|
|
978
|
+
throws. `'none'` is always available.
|
|
979
|
+
|
|
980
|
+
**Scope and mutability.** Compression is genuinely **per-column-family**. Each `RocksDatabase`
|
|
981
|
+
targets one column family (`name`), and its `compression` applies only to that CF — opening one CF
|
|
982
|
+
never changes another's algorithm, regardless of open order. A column family keeps its own
|
|
983
|
+
compression across a close/reopen (a plain reopen inherits it); the algorithm is dynamically
|
|
984
|
+
changeable, so an explicit change governs files written afterward while existing SST/blob files keep
|
|
985
|
+
their original compression until rewritten by compaction. It also applies to blob files (large
|
|
986
|
+
values), whose compression otherwise defaults to none.
|
|
987
|
+
|
|
988
|
+
Because a column family's compression is fixed while it is open, if the same column family is opened
|
|
989
|
+
a second time in the same process (another `RocksDatabase` on the same path/`name`, including from a
|
|
990
|
+
`worker_thread`) with an **explicitly different** algorithm or level, the second open **throws** — a
|
|
991
|
+
plain reopen (no `compression`) instead inherits the live setting.
|
|
992
|
+
|
|
993
|
+
### `supportedCompression`
|
|
994
|
+
|
|
995
|
+
A module-level, frozen array of the compression algorithm names compiled into the loaded native
|
|
996
|
+
binding. The set is fixed for a given binary. `'none'` is always present.
|
|
997
|
+
|
|
998
|
+
```typescript
|
|
999
|
+
import { supportedCompression } from '@harperfast/rocksdb-js';
|
|
1000
|
+
|
|
1001
|
+
console.log(supportedCompression); // e.g. ['none', 'snappy', 'lz4', 'zstd']
|
|
1002
|
+
|
|
1003
|
+
if (supportedCompression.includes('zstd')) {
|
|
1004
|
+
// safe to open with { compression: 'zstd' }
|
|
1005
|
+
}
|
|
1006
|
+
```
|
|
1007
|
+
|
|
907
1008
|
## Exclusive Locking
|
|
908
1009
|
|
|
909
1010
|
`rocksdb-js` includes a handful of functions for executing thread-safe mutually exclusive functions.
|
|
@@ -996,18 +1097,28 @@ Note: If the `callback` throws an error, Node.js suppress the error. Node.js 18.
|
|
|
996
1097
|
`--force-node-api-uncaught-exceptions-policy` flag which will cause errors to emit the
|
|
997
1098
|
`'uncaughtException'` event. Future Node.js releases will enable this flag by default.
|
|
998
1099
|
|
|
999
|
-
##
|
|
1100
|
+
## File Locking
|
|
1000
1101
|
|
|
1001
1102
|
`rocksdb-js` includes helper functions for creating lock files and releasing them using native APIs.
|
|
1002
1103
|
This can be used to prevent multiple processes from concurrently accessing a resource. The lock is
|
|
1003
1104
|
automatically released when the process exits.
|
|
1004
1105
|
|
|
1005
|
-
### `tryFileLock(file: string): number`
|
|
1106
|
+
### `tryFileLock(file: string, shared?: boolean): number`
|
|
1107
|
+
|
|
1108
|
+
Attempts to acquire a lock on the given file, creating the file and any missing parent directories.
|
|
1109
|
+
The lock is exclusive by default; pass `shared: true` for a shared (reader) lock, which coexists
|
|
1110
|
+
with other shared holders but conflicts with an exclusive holder in either direction. Returns a
|
|
1111
|
+
non-zero token to pass to `fileLockRelease` if the lock was acquired, or `0` if a conflicting
|
|
1112
|
+
holder — in any process, container, or worker thread — currently has it. Throws on a hard error.
|
|
1006
1113
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1114
|
+
A shared lock needs only read access: it opens the file read-only and does not create it (falling
|
|
1115
|
+
back to create only when the file is missing on a writable directory), so it can lock an existing
|
|
1116
|
+
lock file on a read-only filesystem. If the media is read-only for every process (`EROFS` /
|
|
1117
|
+
`ERROR_WRITE_PROTECT`) so the open fails outright, a shared acquire degrades to a successful no-op
|
|
1118
|
+
token — there can be no exclusive holder to exclude on a directory nothing can write. A plain
|
|
1119
|
+
permission denial (`EACCES` / `ERROR_ACCESS_DENIED`) is _not_ degraded — only the caller is blocked,
|
|
1120
|
+
so a privileged holder may still exist — and throws instead. An exclusive lock always opens
|
|
1121
|
+
read-write and creates the file, and so requires a writable location.
|
|
1011
1122
|
|
|
1012
1123
|
```typescript
|
|
1013
1124
|
import { tryFileLock } from '@harperfast/rocksdb-js';
|
|
@@ -1372,6 +1483,57 @@ Returns an object containing all of the information in the log file.
|
|
|
1372
1483
|
- `length: number` The size of the entry data.
|
|
1373
1484
|
- `timestamp: number` The entry timestamp.
|
|
1374
1485
|
|
|
1486
|
+
### Transaction Log Validation
|
|
1487
|
+
|
|
1488
|
+
#### `validateTransactionLogStore(path, options?)`
|
|
1489
|
+
|
|
1490
|
+
Validates a transaction log store directory (`<dbDir>/transaction_logs/<name>`): every
|
|
1491
|
+
`<sequence>.txnlog` file is checked for a valid header and intact entry framing (using the same
|
|
1492
|
+
scan as open-time crash recovery), file-name and sequence continuity are checked, and the
|
|
1493
|
+
`txn.state` side file (when present) is checked for shape and a plausible flushed position.
|
|
1494
|
+
|
|
1495
|
+
Validation runs natively on a worker thread and does not require (or open) a database, so it works
|
|
1496
|
+
on the store of a closed database and on backup snapshots alike. It resolves with a report rather
|
|
1497
|
+
than throwing when the store is invalid — check `result.valid`. It rejects only when `path` does
|
|
1498
|
+
not exist or is not a directory.
|
|
1499
|
+
|
|
1500
|
+
```typescript
|
|
1501
|
+
const result = await validateTransactionLogStore('/path/to/db/transaction_logs/mylog');
|
|
1502
|
+
if (!result.valid) {
|
|
1503
|
+
console.error(result.errors);
|
|
1504
|
+
for (const file of result.files) {
|
|
1505
|
+
console.error(file.file, file.errors, file.warnings);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
```
|
|
1509
|
+
|
|
1510
|
+
Returns a `TransactionLogStoreValidation` object:
|
|
1511
|
+
|
|
1512
|
+
- `path: string` The validated store directory.
|
|
1513
|
+
- `valid: boolean` `true` when there are no store-level errors and every log file is valid.
|
|
1514
|
+
- `errors: string[]` Store-level errors (malformed file names, corrupt `txn.state`).
|
|
1515
|
+
- `warnings: string[]` Store-level warnings (sequence gaps, unexpected files).
|
|
1516
|
+
- `files: TransactionLogFileValidation[]` Per-file results, ordered by sequence number.
|
|
1517
|
+
- `file: string` File name, e.g. `"3.txnlog"`.
|
|
1518
|
+
- `sequence: number` Sequence number parsed from the file name.
|
|
1519
|
+
- `size: number` On-disk file size in bytes.
|
|
1520
|
+
- `entries: number` Number of well-formed entries.
|
|
1521
|
+
- `validBytes: number` End offset of the validly framed data.
|
|
1522
|
+
- `valid: boolean` `true` when the file has no errors.
|
|
1523
|
+
- `errors: string[]` Bad header, unsupported version, or mid-file framing corruption.
|
|
1524
|
+
- `warnings: string[]` Torn tail (recoverable on next open) and per-entry anomalies.
|
|
1525
|
+
|
|
1526
|
+
Options:
|
|
1527
|
+
|
|
1528
|
+
- `strict?: boolean` Report conditions that indicate an incomplete snapshot — a torn tail, a
|
|
1529
|
+
sequence gap, or a `txn.state` flushed position beyond the newest log file or beyond its file's
|
|
1530
|
+
actual size — as errors instead of warnings. A torn tail on a live store is a normal crash
|
|
1531
|
+
artifact that open-time recovery truncates, but a backup snapshot must be clean end to end —
|
|
1532
|
+
`backups.verify()` uses `strict: true`. Defaults to `false`.
|
|
1533
|
+
|
|
1534
|
+
Validating a store that is being actively appended to can spuriously report a torn tail for the
|
|
1535
|
+
current log file — the tail of an in-flight append is indistinguishable from a crash artifact.
|
|
1536
|
+
|
|
1375
1537
|
### `currentThreadId(): number`
|
|
1376
1538
|
|
|
1377
1539
|
Returns the current thread ID.
|
|
@@ -1464,11 +1626,14 @@ grouped under the `backups` namespace export.
|
|
|
1464
1626
|
|
|
1465
1627
|
> **Only one backup per directory may be in-flight at a time.** RocksDB has no cross-engine lock on
|
|
1466
1628
|
> a backup directory, so the writing operations — `db.backup()`, `backups.delete()`, and
|
|
1467
|
-
> `backups.purge()` — take an on-disk lock (a `.backup.lock` file) for the directory.
|
|
1468
|
-
>
|
|
1469
|
-
>
|
|
1470
|
-
> the
|
|
1471
|
-
>
|
|
1629
|
+
> `backups.purge()` — take an on-disk lock (a `.backup.lock` file) for the directory.
|
|
1630
|
+
> `backups.restore()` takes the same lock in **shared** mode: concurrent restores run in parallel,
|
|
1631
|
+
> but a writer and a restore on the same directory exclude each other, so a `purge` cannot delete
|
|
1632
|
+
> the backup a restore is copying from. A conflicting operation on the same directory, whether from
|
|
1633
|
+
> the same process, a `worker_thread`, or a separate process, **rejects** with a "locked" error
|
|
1634
|
+
> rather than corrupting the backup; retry once the in-flight operation finishes. Operations on
|
|
1635
|
+
> _different_ directories run in parallel, and the read-only operations (`list`, `verify`) are not
|
|
1636
|
+
> locked.
|
|
1472
1637
|
|
|
1473
1638
|
```typescript
|
|
1474
1639
|
import { RocksDatabase, backups } from '@harperfast/rocksdb-js';
|
|
@@ -1491,6 +1656,10 @@ re-copied.
|
|
|
1491
1656
|
When the database was opened with `disableWAL`, the memtable is flushed before the backup by
|
|
1492
1657
|
default so unflushed data is not lost; otherwise flushing follows `options.flushBeforeBackup`.
|
|
1493
1658
|
|
|
1659
|
+
`backupDir` must not be the database directory itself or a path beneath it — backing up into the
|
|
1660
|
+
live database directory would write backup files on top of RocksDB's own files. Such a call rejects
|
|
1661
|
+
before anything is written.
|
|
1662
|
+
|
|
1494
1663
|
```typescript
|
|
1495
1664
|
const id = await db.backup('/path/to/backups', { metadata: 'nightly-2026-06-04' });
|
|
1496
1665
|
```
|
|
@@ -1500,6 +1669,7 @@ const id = await db.backup('/path/to/backups', { metadata: 'nightly-2026-06-04'
|
|
|
1500
1669
|
| Option | Type | Default | Description |
|
|
1501
1670
|
| ------------------------- | --------- | ---------------------- | ------------------------------------------------------------------------------------ |
|
|
1502
1671
|
| `backupLogFiles` | `boolean` | `true` | Include write-ahead log files in the backup. |
|
|
1672
|
+
| `checkDiskSpace` | `boolean` | `true` | Preflight the destination volume and reject if it lacks room for the backup. |
|
|
1503
1673
|
| `flushBeforeBackup` | `boolean` | `true` if WAL disabled | Flush the memtable before backing up. |
|
|
1504
1674
|
| `maxBackgroundOperations` | `number` | `1` | Number of background threads used to copy files. |
|
|
1505
1675
|
| `metadata` | `string` | `''` | Application metadata stored with the backup, returned by `list()`. |
|
|
@@ -1515,6 +1685,16 @@ backup either has its complete snapshot or none. The snapshot is captured just a
|
|
|
1515
1685
|
engine snapshot, so restored logs may run slightly ahead of the restored key-value data (never
|
|
1516
1686
|
behind it), which is safe for redo-style logs replayed against the restored data.
|
|
1517
1687
|
|
|
1688
|
+
`checkDiskSpace` (on by default) rejects with an I/O error — its message reports the available and
|
|
1689
|
+
required bytes — before any files are copied, so a full destination fails fast instead of partway
|
|
1690
|
+
through (a mid-copy failure can leave zero usable backups). The required size is estimated
|
|
1691
|
+
conservatively as a full copy of the database's live files, plus the transaction-log snapshot when
|
|
1692
|
+
`transactionLogs` is set; since a backup only ever copies files, this can never under-estimate.
|
|
1693
|
+
Incremental backups to a right-sized volume may therefore be over-rejected — set `checkDiskSpace:
|
|
1694
|
+
false` to skip the check. It also self-disables when free space can't be determined (e.g. some
|
|
1695
|
+
network filesystems report `0`), so it never blocks a backup on an untrustworthy number. Only
|
|
1696
|
+
directory-target backups are checked; the streaming backup path is unaffected.
|
|
1697
|
+
|
|
1518
1698
|
### `db.backup(stream: WritableStream<Uint8Array>, options?: BackupStreamOptions): Promise<void>`
|
|
1519
1699
|
|
|
1520
1700
|
Streams a consistent snapshot of the entire database to `stream` as a tar archive, with **no
|
|
@@ -1551,6 +1731,20 @@ Restores a backup from `backupDir` into `dbDir` (creating parent directories as
|
|
|
1551
1731
|
database must **not** be open at `dbDir`, and the default restore mode is **destructive** — it
|
|
1552
1732
|
purges `dbDir` before restoring. Restoring into the backup directory itself is rejected.
|
|
1553
1733
|
|
|
1734
|
+
The restore holds the backup directory's `.backup.lock` in shared mode for its duration, so it
|
|
1735
|
+
rejects with a "locked" error while a writer (`db.backup()`, `backups.delete()`, `backups.purge()`)
|
|
1736
|
+
is in flight — and writers reject while the restore runs. Concurrent restores from the same
|
|
1737
|
+
directory are allowed.
|
|
1738
|
+
|
|
1739
|
+
Because a restore only reads the backup, the shared lock needs no write access to `backupDir`:
|
|
1740
|
+
restoring from a **read-only backup directory** (an immutable/WORM store or a read-only-mounted
|
|
1741
|
+
volume) works — it locks the existing `.backup.lock` read-only, and if the media is read-only for
|
|
1742
|
+
every process (`EROFS`), the lock degrades to a no-op, since nothing can write the directory and so
|
|
1743
|
+
no writer can be racing the restore. A mere permission denial is _not_ treated this way — it means
|
|
1744
|
+
only the current identity is blocked, so a more-privileged writer could still be running, and the
|
|
1745
|
+
restore fails loudly rather than skipping coordination. Writers always need write access and fail on
|
|
1746
|
+
a read-only directory.
|
|
1747
|
+
|
|
1554
1748
|
```typescript
|
|
1555
1749
|
// Restore the latest backup.
|
|
1556
1750
|
await backups.restore('/path/to/backups', '/path/to/restored-db');
|
|
@@ -1592,11 +1786,17 @@ backup references them, so this is not equivalent to deleting files manually.
|
|
|
1592
1786
|
|
|
1593
1787
|
Deletes all but the newest `keepCount` backups.
|
|
1594
1788
|
|
|
1595
|
-
### `backups.verify(backupDir: string, backupId: number, options?: { verifyWithChecksum?: boolean }): Promise<void>`
|
|
1789
|
+
### `backups.verify(backupDir: string, backupId: number, options?: { verifyWithChecksum?: boolean; verifyTransactionLogs?: boolean }): Promise<void>`
|
|
1596
1790
|
|
|
1597
1791
|
Verifies a backup's file sizes, and optionally their checksums (which requires reading all
|
|
1598
1792
|
backed-up data). Resolves if the backup is intact and rejects otherwise.
|
|
1599
1793
|
|
|
1794
|
+
When the backup was created with `transactionLogs: true`, the backup's transaction log snapshot
|
|
1795
|
+
(`<backupDir>/transaction_logs/<backupId>/`) is also validated with
|
|
1796
|
+
[`validateTransactionLogStore`](#validatetransactionlogstorepath-options) in strict mode — every
|
|
1797
|
+
log file's header and entry framing must be intact (snapshots are copied on committed entry
|
|
1798
|
+
boundaries, so even a torn tail fails verification). Set `verifyTransactionLogs: false` to skip.
|
|
1799
|
+
|
|
1600
1800
|
```typescript
|
|
1601
1801
|
await backups.verify('/path/to/backups', 1, { verifyWithChecksum: true });
|
|
1602
1802
|
```
|
|
@@ -1661,6 +1861,13 @@ await db.put('foo', 'bar');
|
|
|
1661
1861
|
console.log(await db.get('foo'));
|
|
1662
1862
|
```
|
|
1663
1863
|
|
|
1864
|
+
> [!NOTE]
|
|
1865
|
+
> A `Store` is bound to a single `RocksDatabase` instance. Passing a store that another
|
|
1866
|
+
> `RocksDatabase` already owns throws `Store is already in use by another RocksDatabase instance`.
|
|
1867
|
+
> The claim is durable across `db.close()`, so reopening the original owner does not release it.
|
|
1868
|
+
> (Sharing a store with the owning database's `Transaction` instances is expected and handled
|
|
1869
|
+
> internally.)
|
|
1870
|
+
|
|
1664
1871
|
> [!IMPORTANT]
|
|
1665
1872
|
> If your custom store overrides `putSync()` without calling `super.putSync()` and it performs its
|
|
1666
1873
|
> own `this.encodeKey(key)`, then you MUST encode the VALUE before you encode the KEY.
|
|
@@ -1756,8 +1963,9 @@ Available commands:
|
|
|
1756
1963
|
- `backups <dir> [subcommand]` Manage database backups; with no subcommand or `ls`/`list`, lists the backups in
|
|
1757
1964
|
`<dir>`. Subcommands: `backup` (create a backup of the open database), `restore <backup-id>`
|
|
1758
1965
|
(restore into the open database after confirmation; unavailable in read-only mode),
|
|
1759
|
-
`verify <backup-id>` (checksum verification
|
|
1760
|
-
(delete all but the newest backups). `delete` and
|
|
1966
|
+
`verify <backup-id>` (checksum verification, including the backup's transaction log snapshot),
|
|
1967
|
+
`delete <backup-id>`, and `purge <keep-count>` (delete all but the newest backups). `delete` and
|
|
1968
|
+
`purge` report the recovered disk space.
|
|
1761
1969
|
- `clear` Clear all data in the current column family
|
|
1762
1970
|
- `columns` List column families
|
|
1763
1971
|
- `compact` Compact the current column family
|
|
@@ -1775,6 +1983,7 @@ Available commands:
|
|
|
1775
1983
|
- `repl` Open a JS sub-REPL; "db" refers to the current column family
|
|
1776
1984
|
- `stats` Show the statistics for the current column family
|
|
1777
1985
|
- `use [column]` Create a new column family or switch to an existing one
|
|
1986
|
+
- `verify-logs [name]` Validate the transaction log store files (all stores if no name)
|
|
1778
1987
|
|
|
1779
1988
|
## Development
|
|
1780
1989
|
|
package/bin/rocksdb-js.mjs
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
RocksDatabase,
|
|
5
|
+
backups,
|
|
6
|
+
parseTransactionLog,
|
|
7
|
+
validateTransactionLogStore,
|
|
8
|
+
versions,
|
|
9
|
+
} from '../dist/index.mjs';
|
|
4
10
|
import { existsSync, readdirSync } from 'node:fs';
|
|
5
11
|
import { mkdir, readdir, stat } from 'node:fs/promises';
|
|
6
12
|
import { join, resolve } from 'node:path';
|
|
@@ -29,6 +35,7 @@ const COMMANDS = {
|
|
|
29
35
|
repl: replCommand,
|
|
30
36
|
stats: statsCommand,
|
|
31
37
|
use: useCommand,
|
|
38
|
+
'verify-logs': verifyLogsCommand,
|
|
32
39
|
};
|
|
33
40
|
|
|
34
41
|
const BACKUP_SUBCOMMANDS = ['backup', 'restore', 'verify', 'delete', 'purge'];
|
|
@@ -73,13 +80,19 @@ function completer(line) {
|
|
|
73
80
|
return [hits.length ? hits : columns, arg];
|
|
74
81
|
}
|
|
75
82
|
|
|
76
|
-
if (
|
|
83
|
+
if (
|
|
84
|
+
(command === 'log' ||
|
|
85
|
+
command === 'logs' ||
|
|
86
|
+
command === 'verify-logs' ||
|
|
87
|
+
command === 'purge-logs') &&
|
|
88
|
+
currentDB
|
|
89
|
+
) {
|
|
77
90
|
if (parts.length === 2) {
|
|
78
91
|
const arg = parts[1];
|
|
79
92
|
const logs = currentDB.listLogs();
|
|
80
93
|
const hits = logs.filter((l) => l.startsWith(arg));
|
|
81
94
|
return [(hits.length ? hits : logs).map((s) => `${s} `), arg];
|
|
82
|
-
} else if (parts.length === 3) {
|
|
95
|
+
} else if (parts.length === 3 && (command === 'log' || command === 'logs')) {
|
|
83
96
|
const [name, store] = parts.slice(1);
|
|
84
97
|
const logs = currentDB.listLogs();
|
|
85
98
|
const hit = logs.find((l) => l === name);
|
|
@@ -616,6 +629,9 @@ function helpCommand() {
|
|
|
616
629
|
console.log(
|
|
617
630
|
`${hl('use [column]')} Create a new column family or switch to an existing one`
|
|
618
631
|
);
|
|
632
|
+
console.log(
|
|
633
|
+
`${hl('verify-logs [name]')} Validate transaction log store files (all stores if no name)`
|
|
634
|
+
);
|
|
619
635
|
console.log();
|
|
620
636
|
}
|
|
621
637
|
|
|
@@ -732,6 +748,54 @@ async function purgeLogsCommand(args) {
|
|
|
732
748
|
console.log();
|
|
733
749
|
}
|
|
734
750
|
|
|
751
|
+
async function verifyLogsCommand(args) {
|
|
752
|
+
const [name] = args;
|
|
753
|
+
const logs = currentDB.listLogs();
|
|
754
|
+
if (name && !logs.includes(name)) {
|
|
755
|
+
console.log(`Log store ${hl(name)} does not exist\n`);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
if (logs.length === 0) {
|
|
759
|
+
console.log('No log stores found\n');
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
let invalidStores = 0;
|
|
764
|
+
for (const storeName of name ? [name] : logs) {
|
|
765
|
+
const storePath = join(currentDB.path, 'transaction_logs', storeName);
|
|
766
|
+
const { result, time } = await run(() => validateTransactionLogStore(storePath));
|
|
767
|
+
console.log(`${hl(storeName)} ${result.valid ? 'OK' : bad('INVALID')} ${note(`(${time}ms)`)}`);
|
|
768
|
+
if (!result.valid) {
|
|
769
|
+
invalidStores++;
|
|
770
|
+
}
|
|
771
|
+
for (const error of result.errors) {
|
|
772
|
+
console.log(` ${bad(`! ${error}`)}`);
|
|
773
|
+
}
|
|
774
|
+
for (const warning of result.warnings) {
|
|
775
|
+
console.log(` ${warn(`! ${warning}`)}`);
|
|
776
|
+
}
|
|
777
|
+
for (const file of result.files) {
|
|
778
|
+
console.log(
|
|
779
|
+
` ${file.file} ${file.valid ? 'OK' : bad('INVALID')} ${note(
|
|
780
|
+
`${file.entries.toLocaleString()} entr${file.entries === 1 ? 'y' : 'ies'}, ${formatBytes(file.size)}`
|
|
781
|
+
)}`
|
|
782
|
+
);
|
|
783
|
+
for (const error of file.errors) {
|
|
784
|
+
console.log(` ${bad(`! ${error}`)}`);
|
|
785
|
+
}
|
|
786
|
+
for (const warning of file.warnings) {
|
|
787
|
+
console.log(` ${warn(`! ${warning}`)}`);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
if (invalidStores > 0) {
|
|
792
|
+
console.log(
|
|
793
|
+
`\n${bad(`${invalidStores} log store${invalidStores === 1 ? ' is' : 's are'} invalid`)}`
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
console.log();
|
|
797
|
+
}
|
|
798
|
+
|
|
735
799
|
async function putCommand(args) {
|
|
736
800
|
const [key, ...valueParts] = args;
|
|
737
801
|
if (!key || valueParts.length === 0) {
|