@harperfast/rocksdb-js 2.3.0 → 2.5.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 +205 -12
- package/bin/rocksdb-js.mjs +67 -3
- package/dist/index.cjs +600 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1610 -1365
- package/dist/index.d.mts +1610 -1365
- package/dist/index.mjs +601 -57
- package/dist/index.mjs.map +1 -1
- package/docs/backups.md +253 -0
- package/docs/stats.md +2 -0
- package/docs/transaction-log.md +10 -0
- package/package.json +23 -23
package/README.md
CHANGED
|
@@ -47,6 +47,13 @@ Creates a new database instance.
|
|
|
47
47
|
- `enableStats: boolean` When `true` and the database is open, RocksDB will captures stats that
|
|
48
48
|
are retrieved by calling `db.getStats()`. Enabling statistics imposes 5-10% in overhead.
|
|
49
49
|
Defaults to `false`.
|
|
50
|
+
- `maxOpenFiles: number` The maximum number of table files RocksDB keeps open. `0` (the default)
|
|
51
|
+
derives a budget from the effective per-process open-file limit (an eighth of the limit —
|
|
52
|
+
several databases can share one process — clamped to `[1024, 262144]`); `-1` holds every table
|
|
53
|
+
file open (the RocksDB default, which can exhaust the process file-descriptor limit when
|
|
54
|
+
compaction falls behind under sustained ingest); a positive `int32` is an explicit cap. Reads
|
|
55
|
+
only pay a reopen cost when the number of live table files exceeds the budget, so raise the
|
|
56
|
+
process fd limit (and with it the derived budget) for very large databases.
|
|
50
57
|
- `name: string` The column family name. Defaults to `"default"`.
|
|
51
58
|
- `noBlockCache: boolean` When `true`, disables the block cache. Block caching is enabled by
|
|
52
59
|
default and the cache is shared across all database instances.
|
|
@@ -996,6 +1003,50 @@ Note: If the `callback` throws an error, Node.js suppress the error. Node.js 18.
|
|
|
996
1003
|
`--force-node-api-uncaught-exceptions-policy` flag which will cause errors to emit the
|
|
997
1004
|
`'uncaughtException'` event. Future Node.js releases will enable this flag by default.
|
|
998
1005
|
|
|
1006
|
+
## File Locking
|
|
1007
|
+
|
|
1008
|
+
`rocksdb-js` includes helper functions for creating lock files and releasing them using native APIs.
|
|
1009
|
+
This can be used to prevent multiple processes from concurrently accessing a resource. The lock is
|
|
1010
|
+
automatically released when the process exits.
|
|
1011
|
+
|
|
1012
|
+
### `tryFileLock(file: string, shared?: boolean): number`
|
|
1013
|
+
|
|
1014
|
+
Attempts to acquire a lock on the given file, creating the file and any missing parent directories.
|
|
1015
|
+
The lock is exclusive by default; pass `shared: true` for a shared (reader) lock, which coexists
|
|
1016
|
+
with other shared holders but conflicts with an exclusive holder in either direction. Returns a
|
|
1017
|
+
non-zero token to pass to `fileLockRelease` if the lock was acquired, or `0` if a conflicting
|
|
1018
|
+
holder — in any process, container, or worker thread — currently has it. Throws on a hard error.
|
|
1019
|
+
|
|
1020
|
+
A shared lock needs only read access: it opens the file read-only and does not create it (falling
|
|
1021
|
+
back to create only when the file is missing on a writable directory), so it can lock an existing
|
|
1022
|
+
lock file on a read-only filesystem. If the media is read-only for every process (`EROFS` /
|
|
1023
|
+
`ERROR_WRITE_PROTECT`) so the open fails outright, a shared acquire degrades to a successful no-op
|
|
1024
|
+
token — there can be no exclusive holder to exclude on a directory nothing can write. A plain
|
|
1025
|
+
permission denial (`EACCES` / `ERROR_ACCESS_DENIED`) is _not_ degraded — only the caller is blocked,
|
|
1026
|
+
so a privileged holder may still exist — and throws instead. An exclusive lock always opens
|
|
1027
|
+
read-write and creates the file, and so requires a writable location.
|
|
1028
|
+
|
|
1029
|
+
```typescript
|
|
1030
|
+
import { tryFileLock } from '@harperfast/rocksdb-js';
|
|
1031
|
+
|
|
1032
|
+
const token = tryFileLock('/path/to/lock');
|
|
1033
|
+
if (token) {
|
|
1034
|
+
console.log('lock acquired');
|
|
1035
|
+
} else {
|
|
1036
|
+
console.log('lock not available, another process is holding it');
|
|
1037
|
+
}
|
|
1038
|
+
```
|
|
1039
|
+
|
|
1040
|
+
### `fileLockRelease(token: number): void`
|
|
1041
|
+
|
|
1042
|
+
Releases the file lock for the given token.
|
|
1043
|
+
|
|
1044
|
+
```typescript
|
|
1045
|
+
import { fileLockRelease } from '@harperfast/rocksdb-js';
|
|
1046
|
+
|
|
1047
|
+
fileLockRelease(token);
|
|
1048
|
+
```
|
|
1049
|
+
|
|
999
1050
|
## Verification Table
|
|
1000
1051
|
|
|
1001
1052
|
The verification table is a process-global, fixed-size structure that lets an application cheaply
|
|
@@ -1338,6 +1389,57 @@ Returns an object containing all of the information in the log file.
|
|
|
1338
1389
|
- `length: number` The size of the entry data.
|
|
1339
1390
|
- `timestamp: number` The entry timestamp.
|
|
1340
1391
|
|
|
1392
|
+
### Transaction Log Validation
|
|
1393
|
+
|
|
1394
|
+
#### `validateTransactionLogStore(path, options?)`
|
|
1395
|
+
|
|
1396
|
+
Validates a transaction log store directory (`<dbDir>/transaction_logs/<name>`): every
|
|
1397
|
+
`<sequence>.txnlog` file is checked for a valid header and intact entry framing (using the same
|
|
1398
|
+
scan as open-time crash recovery), file-name and sequence continuity are checked, and the
|
|
1399
|
+
`txn.state` side file (when present) is checked for shape and a plausible flushed position.
|
|
1400
|
+
|
|
1401
|
+
Validation runs natively on a worker thread and does not require (or open) a database, so it works
|
|
1402
|
+
on the store of a closed database and on backup snapshots alike. It resolves with a report rather
|
|
1403
|
+
than throwing when the store is invalid — check `result.valid`. It rejects only when `path` does
|
|
1404
|
+
not exist or is not a directory.
|
|
1405
|
+
|
|
1406
|
+
```typescript
|
|
1407
|
+
const result = await validateTransactionLogStore('/path/to/db/transaction_logs/mylog');
|
|
1408
|
+
if (!result.valid) {
|
|
1409
|
+
console.error(result.errors);
|
|
1410
|
+
for (const file of result.files) {
|
|
1411
|
+
console.error(file.file, file.errors, file.warnings);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
```
|
|
1415
|
+
|
|
1416
|
+
Returns a `TransactionLogStoreValidation` object:
|
|
1417
|
+
|
|
1418
|
+
- `path: string` The validated store directory.
|
|
1419
|
+
- `valid: boolean` `true` when there are no store-level errors and every log file is valid.
|
|
1420
|
+
- `errors: string[]` Store-level errors (malformed file names, corrupt `txn.state`).
|
|
1421
|
+
- `warnings: string[]` Store-level warnings (sequence gaps, unexpected files).
|
|
1422
|
+
- `files: TransactionLogFileValidation[]` Per-file results, ordered by sequence number.
|
|
1423
|
+
- `file: string` File name, e.g. `"3.txnlog"`.
|
|
1424
|
+
- `sequence: number` Sequence number parsed from the file name.
|
|
1425
|
+
- `size: number` On-disk file size in bytes.
|
|
1426
|
+
- `entries: number` Number of well-formed entries.
|
|
1427
|
+
- `validBytes: number` End offset of the validly framed data.
|
|
1428
|
+
- `valid: boolean` `true` when the file has no errors.
|
|
1429
|
+
- `errors: string[]` Bad header, unsupported version, or mid-file framing corruption.
|
|
1430
|
+
- `warnings: string[]` Torn tail (recoverable on next open) and per-entry anomalies.
|
|
1431
|
+
|
|
1432
|
+
Options:
|
|
1433
|
+
|
|
1434
|
+
- `strict?: boolean` Report conditions that indicate an incomplete snapshot — a torn tail, a
|
|
1435
|
+
sequence gap, or a `txn.state` flushed position beyond the newest log file or beyond its file's
|
|
1436
|
+
actual size — as errors instead of warnings. A torn tail on a live store is a normal crash
|
|
1437
|
+
artifact that open-time recovery truncates, but a backup snapshot must be clean end to end —
|
|
1438
|
+
`backups.verify()` uses `strict: true`. Defaults to `false`.
|
|
1439
|
+
|
|
1440
|
+
Validating a store that is being actively appended to can spuriously report a torn tail for the
|
|
1441
|
+
current log file — the tail of an in-flight append is indistinguishable from a crash artifact.
|
|
1442
|
+
|
|
1341
1443
|
### `currentThreadId(): number`
|
|
1342
1444
|
|
|
1343
1445
|
Returns the current thread ID.
|
|
@@ -1419,10 +1521,26 @@ Backups use RocksDB's `BackupEngine` to capture consistent, incremental, checksu
|
|
|
1419
1521
|
snapshots of a database. A backup covers the **entire database** — every column family, the
|
|
1420
1522
|
manifest, and (by default) the write-ahead log — so it is not scoped to an individual `Store`.
|
|
1421
1523
|
|
|
1524
|
+
A backup can be written to a local **directory** (incremental, with a management API) or streamed to
|
|
1525
|
+
a **`WritableStream`** as a tar archive with no intermediate copy on disk. See
|
|
1526
|
+
[docs/backups.md](docs/backups.md) for a full guide covering both modes, restore, checkpoints, and
|
|
1527
|
+
caveats.
|
|
1528
|
+
|
|
1422
1529
|
Creating a backup is an instance method (`db.backup()`) because it needs a live database. The
|
|
1423
1530
|
remaining operations act on a backup directory and do not require an open database, so they are
|
|
1424
1531
|
grouped under the `backups` namespace export.
|
|
1425
1532
|
|
|
1533
|
+
> **Only one backup per directory may be in-flight at a time.** RocksDB has no cross-engine lock on
|
|
1534
|
+
> a backup directory, so the writing operations — `db.backup()`, `backups.delete()`, and
|
|
1535
|
+
> `backups.purge()` — take an on-disk lock (a `.backup.lock` file) for the directory.
|
|
1536
|
+
> `backups.restore()` takes the same lock in **shared** mode: concurrent restores run in parallel,
|
|
1537
|
+
> but a writer and a restore on the same directory exclude each other, so a `purge` cannot delete
|
|
1538
|
+
> the backup a restore is copying from. A conflicting operation on the same directory, whether from
|
|
1539
|
+
> the same process, a `worker_thread`, or a separate process, **rejects** with a "locked" error
|
|
1540
|
+
> rather than corrupting the backup; retry once the in-flight operation finishes. Operations on
|
|
1541
|
+
> _different_ directories run in parallel, and the read-only operations (`list`, `verify`) are not
|
|
1542
|
+
> locked.
|
|
1543
|
+
|
|
1426
1544
|
```typescript
|
|
1427
1545
|
import { RocksDatabase, backups } from '@harperfast/rocksdb-js';
|
|
1428
1546
|
|
|
@@ -1444,21 +1562,74 @@ re-copied.
|
|
|
1444
1562
|
When the database was opened with `disableWAL`, the memtable is flushed before the backup by
|
|
1445
1563
|
default so unflushed data is not lost; otherwise flushing follows `options.flushBeforeBackup`.
|
|
1446
1564
|
|
|
1565
|
+
`backupDir` must not be the database directory itself or a path beneath it — backing up into the
|
|
1566
|
+
live database directory would write backup files on top of RocksDB's own files. Such a call rejects
|
|
1567
|
+
before anything is written.
|
|
1568
|
+
|
|
1447
1569
|
```typescript
|
|
1448
1570
|
const id = await db.backup('/path/to/backups', { metadata: 'nightly-2026-06-04' });
|
|
1449
1571
|
```
|
|
1450
1572
|
|
|
1451
1573
|
`BackupOptions`:
|
|
1452
1574
|
|
|
1453
|
-
| Option | Type | Default | Description
|
|
1454
|
-
| ------------------------- | --------- | ---------------------- |
|
|
1455
|
-
| `
|
|
1456
|
-
| `
|
|
1457
|
-
| `
|
|
1458
|
-
| `
|
|
1459
|
-
| `
|
|
1460
|
-
| `
|
|
1461
|
-
| `
|
|
1575
|
+
| Option | Type | Default | Description |
|
|
1576
|
+
| ------------------------- | --------- | ---------------------- | ------------------------------------------------------------------------------------ |
|
|
1577
|
+
| `backupLogFiles` | `boolean` | `true` | Include write-ahead log files in the backup. |
|
|
1578
|
+
| `checkDiskSpace` | `boolean` | `true` | Preflight the destination volume and reject if it lacks room for the backup. |
|
|
1579
|
+
| `flushBeforeBackup` | `boolean` | `true` if WAL disabled | Flush the memtable before backing up. |
|
|
1580
|
+
| `maxBackgroundOperations` | `number` | `1` | Number of background threads used to copy files. |
|
|
1581
|
+
| `metadata` | `string` | `''` | Application metadata stored with the backup, returned by `list()`. |
|
|
1582
|
+
| `shareFilesWithChecksum` | `boolean` | `true` | Distinguish shared files by checksum to avoid cross-database clashes. |
|
|
1583
|
+
| `shareTableFiles` | `boolean` | `true` | Share files between backups to enable incremental backups. |
|
|
1584
|
+
| `sync` | `boolean` | `true` | `fsync` backup files (including the transaction log snapshot) for crash consistency. |
|
|
1585
|
+
| `transactionLogs` | `boolean` | `false` | Snapshot the transaction log store into `<backupDir>/transaction_logs/<backupId>/`. |
|
|
1586
|
+
|
|
1587
|
+
When `transactionLogs` is enabled, the log snapshot is staged and atomically renamed into
|
|
1588
|
+
`<backupDir>/transaction_logs/<backupId>/` only after every file has been copied (and fsynced, per
|
|
1589
|
+
`sync`), so a crash mid-backup can never leave a partial log snapshot for a listed backup id — a
|
|
1590
|
+
backup either has its complete snapshot or none. The snapshot is captured just after the RocksDB
|
|
1591
|
+
engine snapshot, so restored logs may run slightly ahead of the restored key-value data (never
|
|
1592
|
+
behind it), which is safe for redo-style logs replayed against the restored data.
|
|
1593
|
+
|
|
1594
|
+
`checkDiskSpace` (on by default) rejects with an I/O error — its message reports the available and
|
|
1595
|
+
required bytes — before any files are copied, so a full destination fails fast instead of partway
|
|
1596
|
+
through (a mid-copy failure can leave zero usable backups). The required size is estimated
|
|
1597
|
+
conservatively as a full copy of the database's live files, plus the transaction-log snapshot when
|
|
1598
|
+
`transactionLogs` is set; since a backup only ever copies files, this can never under-estimate.
|
|
1599
|
+
Incremental backups to a right-sized volume may therefore be over-rejected — set `checkDiskSpace:
|
|
1600
|
+
false` to skip the check. It also self-disables when free space can't be determined (e.g. some
|
|
1601
|
+
network filesystems report `0`), so it never blocks a backup on an untrustworthy number. Only
|
|
1602
|
+
directory-target backups are checked; the streaming backup path is unaffected.
|
|
1603
|
+
|
|
1604
|
+
### `db.backup(stream: WritableStream<Uint8Array>, options?: BackupStreamOptions): Promise<void>`
|
|
1605
|
+
|
|
1606
|
+
Streams a consistent snapshot of the entire database to `stream` as a tar archive, with **no
|
|
1607
|
+
intermediate copy written to disk**, and resolves once the stream has been fully written and closed.
|
|
1608
|
+
Backpressure is honored end to end, so a slow consumer (e.g. an upload) paces the backup rather than
|
|
1609
|
+
buffering it in memory. The archive unpacks with any tar tool into a directory that opens as a
|
|
1610
|
+
RocksDB database.
|
|
1611
|
+
|
|
1612
|
+
```typescript
|
|
1613
|
+
import { createWriteStream } from 'node:fs';
|
|
1614
|
+
import { Writable } from 'node:stream';
|
|
1615
|
+
|
|
1616
|
+
await db.backup(Writable.toWeb(createWriteStream('/path/to/backup.tar')));
|
|
1617
|
+
// Restore: `tar -xf backup.tar -C /restored`, then open '/restored'.
|
|
1618
|
+
|
|
1619
|
+
// Or gzip it (`tar -xzf backup.tar.gz` to restore):
|
|
1620
|
+
await db.backup(Writable.toWeb(createWriteStream('/path/to/backup.tar.gz')), { gzip: true });
|
|
1621
|
+
```
|
|
1622
|
+
|
|
1623
|
+
`BackupStreamOptions`:
|
|
1624
|
+
|
|
1625
|
+
| Option | Type | Default | Description |
|
|
1626
|
+
| ------------------- | --------- | ---------------------- | ------------------------------------------------------------------- |
|
|
1627
|
+
| `flushBeforeBackup` | `boolean` | `true` if WAL disabled | Flush the memtable before streaming. |
|
|
1628
|
+
| `gzip` | `boolean` | `false` | Gzip-compress the archive, producing a `.tar.gz` instead of `.tar`. |
|
|
1629
|
+
|
|
1630
|
+
Stream backups are always full snapshots (no incremental sharing), have no `backups.*` management
|
|
1631
|
+
API, and **cannot be resumed** — a failed transfer must be restarted from the beginning. See
|
|
1632
|
+
[docs/backups.md](docs/backups.md#stream-backups) for details.
|
|
1462
1633
|
|
|
1463
1634
|
### `backups.restore(backupDir: string, dbDir: string, options?: RestoreOptions): Promise<void>`
|
|
1464
1635
|
|
|
@@ -1466,6 +1637,20 @@ Restores a backup from `backupDir` into `dbDir` (creating parent directories as
|
|
|
1466
1637
|
database must **not** be open at `dbDir`, and the default restore mode is **destructive** — it
|
|
1467
1638
|
purges `dbDir` before restoring. Restoring into the backup directory itself is rejected.
|
|
1468
1639
|
|
|
1640
|
+
The restore holds the backup directory's `.backup.lock` in shared mode for its duration, so it
|
|
1641
|
+
rejects with a "locked" error while a writer (`db.backup()`, `backups.delete()`, `backups.purge()`)
|
|
1642
|
+
is in flight — and writers reject while the restore runs. Concurrent restores from the same
|
|
1643
|
+
directory are allowed.
|
|
1644
|
+
|
|
1645
|
+
Because a restore only reads the backup, the shared lock needs no write access to `backupDir`:
|
|
1646
|
+
restoring from a **read-only backup directory** (an immutable/WORM store or a read-only-mounted
|
|
1647
|
+
volume) works — it locks the existing `.backup.lock` read-only, and if the media is read-only for
|
|
1648
|
+
every process (`EROFS`), the lock degrades to a no-op, since nothing can write the directory and so
|
|
1649
|
+
no writer can be racing the restore. A mere permission denial is _not_ treated this way — it means
|
|
1650
|
+
only the current identity is blocked, so a more-privileged writer could still be running, and the
|
|
1651
|
+
restore fails loudly rather than skipping coordination. Writers always need write access and fail on
|
|
1652
|
+
a read-only directory.
|
|
1653
|
+
|
|
1469
1654
|
```typescript
|
|
1470
1655
|
// Restore the latest backup.
|
|
1471
1656
|
await backups.restore('/path/to/backups', '/path/to/restored-db');
|
|
@@ -1507,11 +1692,17 @@ backup references them, so this is not equivalent to deleting files manually.
|
|
|
1507
1692
|
|
|
1508
1693
|
Deletes all but the newest `keepCount` backups.
|
|
1509
1694
|
|
|
1510
|
-
### `backups.verify(backupDir: string, backupId: number, options?: { verifyWithChecksum?: boolean }): Promise<void>`
|
|
1695
|
+
### `backups.verify(backupDir: string, backupId: number, options?: { verifyWithChecksum?: boolean; verifyTransactionLogs?: boolean }): Promise<void>`
|
|
1511
1696
|
|
|
1512
1697
|
Verifies a backup's file sizes, and optionally their checksums (which requires reading all
|
|
1513
1698
|
backed-up data). Resolves if the backup is intact and rejects otherwise.
|
|
1514
1699
|
|
|
1700
|
+
When the backup was created with `transactionLogs: true`, the backup's transaction log snapshot
|
|
1701
|
+
(`<backupDir>/transaction_logs/<backupId>/`) is also validated with
|
|
1702
|
+
[`validateTransactionLogStore`](#validatetransactionlogstorepath-options) in strict mode — every
|
|
1703
|
+
log file's header and entry framing must be intact (snapshots are copied on committed entry
|
|
1704
|
+
boundaries, so even a torn tail fails verification). Set `verifyTransactionLogs: false` to skip.
|
|
1705
|
+
|
|
1515
1706
|
```typescript
|
|
1516
1707
|
await backups.verify('/path/to/backups', 1, { verifyWithChecksum: true });
|
|
1517
1708
|
```
|
|
@@ -1671,8 +1862,9 @@ Available commands:
|
|
|
1671
1862
|
- `backups <dir> [subcommand]` Manage database backups; with no subcommand or `ls`/`list`, lists the backups in
|
|
1672
1863
|
`<dir>`. Subcommands: `backup` (create a backup of the open database), `restore <backup-id>`
|
|
1673
1864
|
(restore into the open database after confirmation; unavailable in read-only mode),
|
|
1674
|
-
`verify <backup-id>` (checksum verification
|
|
1675
|
-
(delete all but the newest backups). `delete` and
|
|
1865
|
+
`verify <backup-id>` (checksum verification, including the backup's transaction log snapshot),
|
|
1866
|
+
`delete <backup-id>`, and `purge <keep-count>` (delete all but the newest backups). `delete` and
|
|
1867
|
+
`purge` report the recovered disk space.
|
|
1676
1868
|
- `clear` Clear all data in the current column family
|
|
1677
1869
|
- `columns` List column families
|
|
1678
1870
|
- `compact` Compact the current column family
|
|
@@ -1690,6 +1882,7 @@ Available commands:
|
|
|
1690
1882
|
- `repl` Open a JS sub-REPL; "db" refers to the current column family
|
|
1691
1883
|
- `stats` Show the statistics for the current column family
|
|
1692
1884
|
- `use [column]` Create a new column family or switch to an existing one
|
|
1885
|
+
- `verify-logs [name]` Validate the transaction log store files (all stores if no name)
|
|
1693
1886
|
|
|
1694
1887
|
## Development
|
|
1695
1888
|
|
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) {
|