resplite 1.0.4 → 1.0.6
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 +115 -1
- package/package.json +3 -2
- package/src/migration/index.js +170 -0
package/README.md
CHANGED
|
@@ -35,6 +35,24 @@ OK
|
|
|
35
35
|
"bar"
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
### Standalone server script (fixed port)
|
|
39
|
+
|
|
40
|
+
Run this as a persistent background process (`node server.js`). RESPLite will listen on port 6380 and stay up until the process is killed.
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
// server.js
|
|
44
|
+
import { createRESPlite } from 'resplite/embed';
|
|
45
|
+
|
|
46
|
+
const srv = await createRESPlite({ port: 6380, db: './data.db' });
|
|
47
|
+
console.log(`RESPLite listening on ${srv.host}:${srv.port}`);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then connect from any other script or process:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
redis-cli -p 6380 PING
|
|
54
|
+
```
|
|
55
|
+
|
|
38
56
|
### Environment variables
|
|
39
57
|
|
|
40
58
|
| Variable | Default | Description |
|
|
@@ -296,9 +314,43 @@ npm run import-from-redis -- --db ./migrated.db --host 127.0.0.1 --port 6379
|
|
|
296
314
|
npm run import-from-redis -- --db ./migrated.db --pragma-template performance
|
|
297
315
|
```
|
|
298
316
|
|
|
317
|
+
### Redis with authentication
|
|
318
|
+
|
|
319
|
+
Migration supports Redis instances protected by a password. Use a Redis URL that includes the password (or username and password for Redis 6+ ACL):
|
|
320
|
+
|
|
321
|
+
- **Password only:** `redis://:PASSWORD@host:port`
|
|
322
|
+
- **Username and password:** `redis://username:PASSWORD@host:port`
|
|
323
|
+
|
|
324
|
+
Examples:
|
|
325
|
+
|
|
326
|
+
```bash
|
|
327
|
+
# One-shot import from authenticated Redis
|
|
328
|
+
npm run import-from-redis -- --db ./migrated.db --redis-url "redis://:mysecret@127.0.0.1:6379"
|
|
329
|
+
|
|
330
|
+
# SPEC_F flow: use --from with the full URL (or set RESPLITE_IMPORT_FROM)
|
|
331
|
+
npx resplite-import preflight --from "redis://:mysecret@10.0.0.10:6379" --to ./resplite.db
|
|
332
|
+
npx resplite-dirty-tracker start --run-id run_001 --from "redis://:mysecret@10.0.0.10:6379" --to ./resplite.db
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
For one-shot import, authentication is only available when using `--redis-url`; the `--host` / `--port` options do not support a password.
|
|
336
|
+
|
|
299
337
|
### Minimal-downtime migration (SPEC_F)
|
|
300
338
|
|
|
301
|
-
For large datasets (~30 GB), use the Dirty Key Registry flow so the bulk of the migration runs online and only a short cutover is needed
|
|
339
|
+
For large datasets (~30 GB), use the Dirty Key Registry flow so the bulk of the migration runs online and only a short cutover is needed.
|
|
340
|
+
|
|
341
|
+
**Enable keyspace notifications in Redis** (required for the dirty-key tracker). Either run at runtime:
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
redis-cli CONFIG SET notify-keyspace-events Kgxe
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Or add to `redis.conf` and restart Redis:
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
notify-keyspace-events Kgxe
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
(`K` = keyspace, `g` = generic, `x` = expired; `e` = keyevent. This lets the tracker see key changes and expirations.)
|
|
302
354
|
|
|
303
355
|
1. **Preflight** – Check Redis, key count, type distribution, and that keyspace notifications are enabled:
|
|
304
356
|
```bash
|
|
@@ -338,6 +390,68 @@ For large datasets (~30 GB), use the Dirty Key Registry flow so the bulk of the
|
|
|
338
390
|
|
|
339
391
|
Then start RespLite with the migrated DB: `RESPLITE_DB=./resplite.db npm start`.
|
|
340
392
|
|
|
393
|
+
### Programmatic migration API
|
|
394
|
+
|
|
395
|
+
As an alternative to the CLI, the full migration flow is available as a JavaScript API via `resplite/migration`. Useful for embedding the migration inside your own scripts or automation pipelines.
|
|
396
|
+
|
|
397
|
+
```javascript
|
|
398
|
+
import { createMigration } from 'resplite/migration';
|
|
399
|
+
|
|
400
|
+
const m = createMigration({
|
|
401
|
+
from: 'redis://127.0.0.1:6379', // source Redis URL (default)
|
|
402
|
+
to: './resplite.db', // destination SQLite DB path (required)
|
|
403
|
+
runId: 'my-migration-1', // unique run ID (required for bulk/status/applyDirty)
|
|
404
|
+
|
|
405
|
+
// optional — same defaults as the CLI:
|
|
406
|
+
scanCount: 1000,
|
|
407
|
+
batchKeys: 200,
|
|
408
|
+
batchBytes: 64 * 1024 * 1024, // 64 MB
|
|
409
|
+
maxRps: 0, // 0 = unlimited
|
|
410
|
+
pragmaTemplate: 'default',
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Step 0 — Preflight: inspect Redis before starting
|
|
414
|
+
const info = await m.preflight();
|
|
415
|
+
console.log('keys (estimate):', info.keyCountEstimate);
|
|
416
|
+
console.log('type distribution:', info.typeDistribution);
|
|
417
|
+
console.log('recommended params:', info.recommended);
|
|
418
|
+
|
|
419
|
+
// Step 1 — Bulk import (checkpointed, resumable)
|
|
420
|
+
await m.bulk({
|
|
421
|
+
resume: false, // true to resume a previous run
|
|
422
|
+
onProgress: (r) => console.log(
|
|
423
|
+
`scanned=${r.scanned_keys} migrated=${r.migrated_keys} errors=${r.error_keys}`
|
|
424
|
+
),
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
// Check status at any point (synchronous, no Redis needed)
|
|
428
|
+
const { run, dirty } = m.status();
|
|
429
|
+
console.log('bulk status:', run.status, '— dirty counts:', dirty);
|
|
430
|
+
|
|
431
|
+
// Step 2 — Apply dirty keys that changed in Redis during bulk
|
|
432
|
+
await m.applyDirty();
|
|
433
|
+
|
|
434
|
+
// Step 3 — Verify a sample of keys match between Redis and the destination
|
|
435
|
+
const result = await m.verify({ samplePct: 0.5, maxSample: 10000 });
|
|
436
|
+
console.log(`verified ${result.sampled} keys — mismatches: ${result.mismatches.length}`);
|
|
437
|
+
|
|
438
|
+
// Disconnect Redis when done
|
|
439
|
+
await m.close();
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
The dirty-key tracker (to capture writes during bulk) still runs as a separate process via `npx resplite-dirty-tracker`. The API above handles everything else in a single script.
|
|
443
|
+
|
|
444
|
+
#### Low-level re-exports
|
|
445
|
+
|
|
446
|
+
If you need more control, the individual functions and registry helpers are also exported:
|
|
447
|
+
|
|
448
|
+
```javascript
|
|
449
|
+
import {
|
|
450
|
+
runPreflight, runBulkImport, runApplyDirty, runVerify,
|
|
451
|
+
getRun, getDirtyCounts, createRun, setRunStatus, logError,
|
|
452
|
+
} from 'resplite/migration';
|
|
453
|
+
```
|
|
454
|
+
|
|
341
455
|
## Benchmark (Redis vs RESPLite)
|
|
342
456
|
|
|
343
457
|
Compare throughput of local Redis and RESPLite with the same workload (PING, SET/GET, hashes, sets, lists, zsets, etc.):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "resplite",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "A RESP2 server with practical Redis compatibility, backed by SQLite",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
},
|
|
11
11
|
"exports": {
|
|
12
12
|
".": "./src/index.js",
|
|
13
|
-
"./embed": "./src/embed.js"
|
|
13
|
+
"./embed": "./src/embed.js",
|
|
14
|
+
"./migration": "./src/migration/index.js"
|
|
14
15
|
},
|
|
15
16
|
"scripts": {
|
|
16
17
|
"start": "node src/index.js",
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Programmatic migration API (SPEC_F §F.9).
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { createMigration } from 'resplite/migration';
|
|
6
|
+
*
|
|
7
|
+
* const m = createMigration({
|
|
8
|
+
* from: 'redis://127.0.0.1:6379',
|
|
9
|
+
* to: './data.db',
|
|
10
|
+
* runId: 'my-migration-1',
|
|
11
|
+
* });
|
|
12
|
+
*
|
|
13
|
+
* const info = await m.preflight();
|
|
14
|
+
* await m.bulk({ onProgress: console.log });
|
|
15
|
+
* const status = m.status();
|
|
16
|
+
* await m.applyDirty();
|
|
17
|
+
* const result = await m.verify();
|
|
18
|
+
* await m.close();
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { createClient } from 'redis';
|
|
22
|
+
import { openDb } from '../storage/sqlite/db.js';
|
|
23
|
+
import { runPreflight } from './preflight.js';
|
|
24
|
+
import { runBulkImport } from './bulk.js';
|
|
25
|
+
import { runApplyDirty } from './apply-dirty.js';
|
|
26
|
+
import { runVerify } from './verify.js';
|
|
27
|
+
import { getRun, getDirtyCounts } from './registry.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {object} MigrationOptions
|
|
31
|
+
* @property {string} [from='redis://127.0.0.1:6379'] - Source Redis URL.
|
|
32
|
+
* @property {string} to - Destination SQLite DB path.
|
|
33
|
+
* @property {string} [runId] - Unique run identifier (required for bulk/status/applyDirty).
|
|
34
|
+
* @property {string} [pragmaTemplate='default'] - PRAGMA preset.
|
|
35
|
+
* @property {number} [scanCount=1000]
|
|
36
|
+
* @property {number} [maxRps=0] - Max requests/s (0 = unlimited).
|
|
37
|
+
* @property {number} [batchKeys=200]
|
|
38
|
+
* @property {number} [batchBytes=67108864] - 64 MB default.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create a migration controller bound to a source Redis and destination DB.
|
|
43
|
+
*
|
|
44
|
+
* @param {MigrationOptions} options
|
|
45
|
+
* @returns {{
|
|
46
|
+
* preflight(): Promise<import('./preflight.js').PreflightResult>,
|
|
47
|
+
* bulk(opts?: { resume?: boolean, onProgress?: function }): Promise<object>,
|
|
48
|
+
* status(): { run: object, dirty: object } | null,
|
|
49
|
+
* applyDirty(opts?: { batchKeys?: number, maxRps?: number }): Promise<object>,
|
|
50
|
+
* verify(opts?: { samplePct?: number, maxSample?: number }): Promise<object>,
|
|
51
|
+
* close(): Promise<void>,
|
|
52
|
+
* }}
|
|
53
|
+
*/
|
|
54
|
+
export function createMigration({
|
|
55
|
+
from = 'redis://127.0.0.1:6379',
|
|
56
|
+
to,
|
|
57
|
+
runId,
|
|
58
|
+
pragmaTemplate = 'default',
|
|
59
|
+
scanCount = 1000,
|
|
60
|
+
maxRps = 0,
|
|
61
|
+
batchKeys = 200,
|
|
62
|
+
batchBytes = 64 * 1024 * 1024,
|
|
63
|
+
} = {}) {
|
|
64
|
+
if (!to) throw new Error('createMigration: "to" (db path) is required');
|
|
65
|
+
|
|
66
|
+
let _client = null;
|
|
67
|
+
|
|
68
|
+
async function getClient() {
|
|
69
|
+
if (_client) return _client;
|
|
70
|
+
_client = createClient({ url: from });
|
|
71
|
+
_client.on('error', (err) => {
|
|
72
|
+
/* non-fatal connection errors; callers surface them on next await */
|
|
73
|
+
void err;
|
|
74
|
+
});
|
|
75
|
+
await _client.connect();
|
|
76
|
+
return _client;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function requireRunId() {
|
|
80
|
+
if (!runId) throw new Error('createMigration: "runId" is required for this operation');
|
|
81
|
+
return runId;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
/**
|
|
86
|
+
* Step 0 — Preflight: inspect the source Redis instance.
|
|
87
|
+
* Returns key count estimate, type distribution, keyspace-events config,
|
|
88
|
+
* and recommended import parameters.
|
|
89
|
+
*/
|
|
90
|
+
async preflight() {
|
|
91
|
+
const client = await getClient();
|
|
92
|
+
return runPreflight(client);
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Step 1 — Bulk import: SCAN all keys from Redis into the destination DB.
|
|
97
|
+
* Supports resume (checkpoint-based) and optional progress callback.
|
|
98
|
+
*
|
|
99
|
+
* @param {{ resume?: boolean, onProgress?: (run: object) => void }} [opts]
|
|
100
|
+
*/
|
|
101
|
+
async bulk({ resume = false, onProgress } = {}) {
|
|
102
|
+
const id = requireRunId();
|
|
103
|
+
const client = await getClient();
|
|
104
|
+
return runBulkImport(client, to, id, {
|
|
105
|
+
sourceUri: from,
|
|
106
|
+
pragmaTemplate,
|
|
107
|
+
scan_count: scanCount,
|
|
108
|
+
max_rps: maxRps,
|
|
109
|
+
batch_keys: batchKeys,
|
|
110
|
+
batch_bytes: batchBytes,
|
|
111
|
+
resume,
|
|
112
|
+
onProgress,
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Step 2 — Status: read run metadata and dirty-key counts from the DB.
|
|
118
|
+
* Synchronous — no Redis connection needed.
|
|
119
|
+
*
|
|
120
|
+
* @returns {{ run: object, dirty: object } | null}
|
|
121
|
+
*/
|
|
122
|
+
status() {
|
|
123
|
+
const id = requireRunId();
|
|
124
|
+
const db = openDb(to, { pragmaTemplate });
|
|
125
|
+
const run = getRun(db, id);
|
|
126
|
+
if (!run) return null;
|
|
127
|
+
const dirty = getDirtyCounts(db, id);
|
|
128
|
+
return { run, dirty };
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Step 3 — Apply dirty: reconcile keys that changed in Redis during bulk import.
|
|
133
|
+
*
|
|
134
|
+
* @param {{ batchKeys?: number, maxRps?: number }} [opts]
|
|
135
|
+
*/
|
|
136
|
+
async applyDirty({ batchKeys: bk = batchKeys, maxRps: rps = maxRps } = {}) {
|
|
137
|
+
const id = requireRunId();
|
|
138
|
+
const client = await getClient();
|
|
139
|
+
return runApplyDirty(client, to, id, {
|
|
140
|
+
pragmaTemplate,
|
|
141
|
+
batch_keys: bk,
|
|
142
|
+
max_rps: rps,
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Step 4 — Verify: sample keys from Redis and compare with the destination DB.
|
|
148
|
+
*
|
|
149
|
+
* @param {{ samplePct?: number, maxSample?: number }} [opts]
|
|
150
|
+
* @returns {Promise<{ sampled: number, matched: number, mismatches: Array<{ key: string, reason: string }> }>}
|
|
151
|
+
*/
|
|
152
|
+
async verify({ samplePct = 0.5, maxSample = 10000 } = {}) {
|
|
153
|
+
const client = await getClient();
|
|
154
|
+
return runVerify(client, to, { pragmaTemplate, samplePct, maxSample });
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Disconnect from Redis. Call when done with all migration operations.
|
|
159
|
+
*/
|
|
160
|
+
async close() {
|
|
161
|
+
if (_client) {
|
|
162
|
+
await _client.quit().catch(() => {});
|
|
163
|
+
_client = null;
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export { runPreflight, runBulkImport, runApplyDirty, runVerify };
|
|
170
|
+
export { getRun, getDirtyCounts, createRun, setRunStatus, logError } from './registry.js';
|