resplite 1.2.0 → 1.2.4
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 +277 -215
- package/package.json +1 -1
- package/spec/SPEC_F.md +94 -0
- package/src/cli/resplite-import.js +77 -18
- package/src/embed.js +10 -0
- package/src/index.js +58 -20
- package/src/migration/bulk.js +71 -38
- package/src/migration/index.js +35 -4
- package/src/migration/migrate-search.js +457 -0
- package/src/server/tcp-server.js +6 -1
- package/test/unit/migrate-search.test.js +497 -0
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ OK
|
|
|
87
87
|
|
|
88
88
|
### Standalone server script (fixed port)
|
|
89
89
|
|
|
90
|
-
Run this as a persistent background process (`node server.js`). RESPLite will listen on port 6380 and stay up until the process receives SIGINT (Ctrl+C) or SIGTERM; then it closes the server and exits cleanly.
|
|
90
|
+
Run this as a persistent background process (`node server.js`). RESPLite will listen on port 6380 and stay up until the process receives SIGINT (Ctrl+C) or SIGTERM; then it closes the server and exits cleanly. If you kill the process (e.g. SIGKILL or force quit), all client connections are closed as well — with the default configuration the server runs in the same process, so when the process exits the TCP server and its connections are torn down.
|
|
91
91
|
|
|
92
92
|
```javascript
|
|
93
93
|
// server.js
|
|
@@ -96,13 +96,6 @@ import { createRESPlite } from 'resplite/embed';
|
|
|
96
96
|
const srv = await createRESPlite({ port: 6380, db: './data.db' });
|
|
97
97
|
console.log(`RESPLite listening on ${srv.host}:${srv.port}`);
|
|
98
98
|
|
|
99
|
-
async function shutdown() {
|
|
100
|
-
await srv.close();
|
|
101
|
-
process.exit(0);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
process.on('SIGINT', shutdown);
|
|
105
|
-
process.on('SIGTERM', shutdown);
|
|
106
99
|
```
|
|
107
100
|
|
|
108
101
|
Then connect from any other script or process:
|
|
@@ -181,6 +174,222 @@ const srv = await createRESPlite({
|
|
|
181
174
|
| `onCommandError` | A command failed (wrong type, invalid args, or handler threw). |
|
|
182
175
|
| `onSocketError` | The connection socket emitted an error (e.g. `ECONNRESET`). |
|
|
183
176
|
|
|
177
|
+
## Migration from Redis
|
|
178
|
+
|
|
179
|
+
RESPLite is a good fit for migrating **non-replicated Redis** instances that have **grown large** (e.g. tens of GB) and where RESPLite's latency is acceptable. The flow (dirty-key tracker, bulk import, cutover) is designed for that scenario with minimal downtime.
|
|
180
|
+
|
|
181
|
+
Migration supports two modes:
|
|
182
|
+
|
|
183
|
+
### Programmatic migration API (JavaScript)
|
|
184
|
+
|
|
185
|
+
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.
|
|
186
|
+
|
|
187
|
+
```javascript
|
|
188
|
+
import { createMigration } from 'resplite/migration';
|
|
189
|
+
|
|
190
|
+
const m = createMigration({
|
|
191
|
+
from: 'redis://127.0.0.1:6379', // source Redis URL (default)
|
|
192
|
+
to: './resplite.db', // destination SQLite DB path (required)
|
|
193
|
+
runId: 'my-migration-1', // unique run ID (required for bulk/status/applyDirty)
|
|
194
|
+
|
|
195
|
+
// optional — same defaults as the CLI:
|
|
196
|
+
scanCount: 1000,
|
|
197
|
+
batchKeys: 200,
|
|
198
|
+
batchBytes: 64 * 1024 * 1024, // 64 MB
|
|
199
|
+
maxRps: 0, // 0 = unlimited
|
|
200
|
+
pragmaTemplate: 'default',
|
|
201
|
+
|
|
202
|
+
// If your Redis deployment renamed CONFIG for security:
|
|
203
|
+
// configCommand: 'MYCONFIG',
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Step 0 — Preflight: inspect Redis before starting
|
|
207
|
+
const info = await m.preflight();
|
|
208
|
+
console.log('keys (estimate):', info.keyCountEstimate);
|
|
209
|
+
console.log('type distribution:', info.typeDistribution);
|
|
210
|
+
console.log('notify-keyspace-events:', info.notifyKeyspaceEvents);
|
|
211
|
+
console.log('CONFIG available:', info.configCommandAvailable); // false if renamed
|
|
212
|
+
console.log('recommended params:', info.recommended);
|
|
213
|
+
|
|
214
|
+
// Step 0b — Enable keyspace notifications (required for dirty-key tracking)
|
|
215
|
+
// Reads the current value and merges the new flags — existing flags are preserved.
|
|
216
|
+
const ks = await m.enableKeyspaceNotifications();
|
|
217
|
+
// → { ok: true, previous: '', applied: 'KEA' }
|
|
218
|
+
// If CONFIG is renamed and configCommand was not set, ok=false and error explains how to fix it.
|
|
219
|
+
|
|
220
|
+
// Step 1 — Bulk import (checkpointed, resumable). Same script to start or continue.
|
|
221
|
+
// Use keyCountEstimate from preflight to show progress % (estimate; actual count may change).
|
|
222
|
+
const total = info.keyCountEstimate || 1;
|
|
223
|
+
await m.bulk({
|
|
224
|
+
onProgress: (r) => {
|
|
225
|
+
const pct = total ? ((r.scanned_keys / total) * 100).toFixed(1) : '—';
|
|
226
|
+
console.log(
|
|
227
|
+
`scanned=${r.scanned_keys} migrated=${r.migrated_keys} errors=${r.error_keys} progress=${pct}%`
|
|
228
|
+
);
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Check status at any point (synchronous, no Redis needed)
|
|
233
|
+
const { run, dirty } = m.status();
|
|
234
|
+
console.log('bulk status:', run.status, '— dirty counts:', dirty);
|
|
235
|
+
|
|
236
|
+
// Step 2 — Apply dirty keys that changed in Redis during bulk
|
|
237
|
+
await m.applyDirty();
|
|
238
|
+
|
|
239
|
+
// Step 3 — Verify a sample of keys match between Redis and the destination
|
|
240
|
+
const result = await m.verify({ samplePct: 0.5, maxSample: 10000 });
|
|
241
|
+
console.log(`verified ${result.sampled} keys — mismatches: ${result.mismatches.length}`);
|
|
242
|
+
|
|
243
|
+
// Disconnect Redis when done
|
|
244
|
+
await m.close();
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Automatic resume (default)**
|
|
248
|
+
`resume` defaults to `true`. It doesn't matter whether it's the first run or a resume: the same script works for both starting and continuing. The first run starts from cursor 0; if the process is interrupted (Ctrl+C, crash, etc.), running the script again continues from the last checkpoint. You don't need to pass `resume: false` on the first run or change anything to resume.
|
|
249
|
+
|
|
250
|
+
**Graceful shutdown**
|
|
251
|
+
On SIGINT (Ctrl+C) or SIGTERM, the bulk importer checkpoints progress, sets the run status to `aborted`, closes the SQLite database cleanly (so WAL is checkpointed and the file is not left open), then exits. You can safely interrupt a long-running bulk and resume later.
|
|
252
|
+
|
|
253
|
+
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.
|
|
254
|
+
|
|
255
|
+
#### Renamed CONFIG command
|
|
256
|
+
|
|
257
|
+
If your Redis instance has the `CONFIG` command renamed (a common hardening practice), pass the new name to `createMigration`:
|
|
258
|
+
|
|
259
|
+
```javascript
|
|
260
|
+
const m = createMigration({
|
|
261
|
+
from: 'redis://10.0.0.10:6379',
|
|
262
|
+
to: './resplite.db',
|
|
263
|
+
runId: 'run_001',
|
|
264
|
+
configCommand: 'MYCONFIG', // the renamed command
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// preflight will use MYCONFIG GET notify-keyspace-events
|
|
268
|
+
const info = await m.preflight();
|
|
269
|
+
// info.configCommandAvailable → false if the name is wrong
|
|
270
|
+
|
|
271
|
+
// enableKeyspaceNotifications will use MYCONFIG SET notify-keyspace-events KEA
|
|
272
|
+
const result = await m.enableKeyspaceNotifications({ value: 'KEA' });
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The same flag is available in the CLI:
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
npx resplite-dirty-tracker start --run-id run_001 --to ./resplite.db \
|
|
279
|
+
--from redis://10.0.0.10:6379 --config-command MYCONFIG
|
|
280
|
+
```
|
|
281
|
+
### Simple one-shot import
|
|
282
|
+
|
|
283
|
+
For small datasets or when downtime is acceptable:
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
# Default: redis://127.0.0.1:6379 → ./data.db
|
|
287
|
+
npm run import-from-redis -- --db ./migrated.db
|
|
288
|
+
|
|
289
|
+
# Custom Redis URL
|
|
290
|
+
npm run import-from-redis -- --db ./migrated.db --redis-url redis://127.0.0.1:6379
|
|
291
|
+
|
|
292
|
+
# Or host/port
|
|
293
|
+
npm run import-from-redis -- --db ./migrated.db --host 127.0.0.1 --port 6379
|
|
294
|
+
|
|
295
|
+
# Optional: PRAGMA template for the target DB
|
|
296
|
+
npm run import-from-redis -- --db ./migrated.db --pragma-template performance
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Redis with authentication
|
|
300
|
+
|
|
301
|
+
Migration supports Redis instances protected by a password. Use a Redis URL that includes the password (or username and password for Redis 6+ ACL):
|
|
302
|
+
|
|
303
|
+
- **Password only:** `redis://:PASSWORD@host:port`
|
|
304
|
+
- **Username and password:** `redis://username:PASSWORD@host:port`
|
|
305
|
+
|
|
306
|
+
Examples:
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
# One-shot import from authenticated Redis
|
|
310
|
+
npm run import-from-redis -- --db ./migrated.db --redis-url "redis://:mysecret@127.0.0.1:6379"
|
|
311
|
+
|
|
312
|
+
# flow: use --from with the full URL (or set RESPLITE_IMPORT_FROM)
|
|
313
|
+
npx resplite-import preflight --from "redis://:mysecret@10.0.0.10:6379" --to ./resplite.db
|
|
314
|
+
npx resplite-dirty-tracker start --run-id run_001 --from "redis://:mysecret@10.0.0.10:6379" --to ./resplite.db
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
For one-shot import, authentication is only available when using `--redis-url`; the `--host` / `--port` options do not support a password.
|
|
318
|
+
|
|
319
|
+
**Search indices (FT.\*)**
|
|
320
|
+
The KV bulk migration imports only the Redis keyspace (strings, hashes, sets, lists, zsets). RediSearch index schemas and documents are migrated separately with the `migrate-search` step — see [Migrating RediSearch indices](#migrating-redisearch-indices) below.
|
|
321
|
+
|
|
322
|
+
### Minimal-downtime migration
|
|
323
|
+
|
|
324
|
+
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.
|
|
325
|
+
|
|
326
|
+
**Enable keyspace notifications in Redis** (required for the dirty-key tracker). Either run at runtime:
|
|
327
|
+
|
|
328
|
+
```bash
|
|
329
|
+
redis-cli CONFIG SET notify-keyspace-events KEA
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Or add to `redis.conf` and restart Redis:
|
|
333
|
+
|
|
334
|
+
```
|
|
335
|
+
notify-keyspace-events KEA
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
(`K` = keyspace prefix, `E` = keyevent prefix, `A` = all event types — lets the tracker see every key change and expiration.)
|
|
339
|
+
|
|
340
|
+
> **Renamed CONFIG command?** Some Redis deployments rename `CONFIG` for security. Pass `--config-command <name>` to the CLI tools, or the `configCommand` option to the JS API — see below.
|
|
341
|
+
|
|
342
|
+
1. **Preflight** – Check Redis, key count, type distribution, and that keyspace notifications are enabled:
|
|
343
|
+
```bash
|
|
344
|
+
npx resplite-import preflight --from redis://10.0.0.10:6379 --to ./resplite.db
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
2. **Start dirty-key tracker** – Captures keys modified during bulk (requires `notify-keyspace-events` in Redis):
|
|
348
|
+
```bash
|
|
349
|
+
npx resplite-dirty-tracker start --run-id run_001 --from redis://10.0.0.10:6379 --to ./resplite.db
|
|
350
|
+
# If CONFIG was renamed:
|
|
351
|
+
npx resplite-dirty-tracker start --run-id run_001 --from redis://10.0.0.10:6379 --to ./resplite.db --config-command MYCONFIG
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
3. **Bulk import** – SCAN and copy all keys; progress is checkpointed and resumable (resume is default; re-run the same command to continue after a stop):
|
|
355
|
+
```bash
|
|
356
|
+
npx resplite-import bulk --run-id run_001 --from redis://10.0.0.10:6379 --to ./resplite.db \
|
|
357
|
+
--scan-count 1000 --max-rps 2000 --batch-keys 200 --batch-bytes 64MB
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
4. **Monitor** – Check run and dirty-key counts:
|
|
361
|
+
```bash
|
|
362
|
+
npx resplite-import status --run-id run_001 --to ./resplite.db
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
5. **Cutover** – Freeze app writes to Redis, then apply remaining dirty keys:
|
|
366
|
+
```bash
|
|
367
|
+
npx resplite-import apply-dirty --run-id run_001 --from redis://10.0.0.10:6379 --to ./resplite.db
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
6. **Stop tracker and switch** – Stop the tracker and point clients to RespLite:
|
|
371
|
+
```bash
|
|
372
|
+
npx resplite-dirty-tracker stop --run-id run_001 --to ./resplite.db
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
7. **Verify** – Optional sampling check between Redis and destination:
|
|
376
|
+
```bash
|
|
377
|
+
npx resplite-import verify --run-id run_001 --from redis://10.0.0.10:6379 --to ./resplite.db --sample 0.5%
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Then start RespLite with the migrated DB: `RESPLITE_DB=./resplite.db npm start`.
|
|
381
|
+
|
|
382
|
+
#### Low-level re-exports
|
|
383
|
+
|
|
384
|
+
If you need more control, the individual functions and registry helpers are also exported:
|
|
385
|
+
|
|
386
|
+
```javascript
|
|
387
|
+
import {
|
|
388
|
+
runPreflight, runBulkImport, runApplyDirty, runVerify,
|
|
389
|
+
getRun, getDirtyCounts, createRun, setRunStatus, logError,
|
|
390
|
+
} from 'resplite/migration';
|
|
391
|
+
```
|
|
392
|
+
|
|
184
393
|
### Strings, TTL, and key operations
|
|
185
394
|
|
|
186
395
|
```javascript
|
|
@@ -352,9 +561,66 @@ await c2.quit();
|
|
|
352
561
|
await srv2.close();
|
|
353
562
|
```
|
|
354
563
|
|
|
355
|
-
|
|
564
|
+
### Migrating RediSearch indices
|
|
565
|
+
|
|
566
|
+
If your Redis source uses **RediSearch** (Redis Stack or the `redis/search` module), run `migrate-search` after (or during) the KV bulk import. It reads index schemas with `FT.INFO`, creates them in RespLite, and imports documents by scanning the matching hash keys.
|
|
567
|
+
|
|
568
|
+
**CLI:**
|
|
569
|
+
|
|
570
|
+
```bash
|
|
571
|
+
# Migrate all indices
|
|
572
|
+
npx resplite-import migrate-search \
|
|
573
|
+
--from redis://10.0.0.10:6379 \
|
|
574
|
+
--to ./resplite.db
|
|
575
|
+
|
|
576
|
+
# Migrate specific indices only
|
|
577
|
+
npx resplite-import migrate-search \
|
|
578
|
+
--from redis://10.0.0.10:6379 \
|
|
579
|
+
--to ./resplite.db \
|
|
580
|
+
--index products \
|
|
581
|
+
--index articles
|
|
582
|
+
|
|
583
|
+
# Options
|
|
584
|
+
# --scan-count N SCAN COUNT hint (default 500)
|
|
585
|
+
# --max-rps N throttle Redis reads
|
|
586
|
+
# --batch-docs N docs per SQLite transaction (default 200)
|
|
587
|
+
# --max-suggestions N cap for suggestion import (default 10000)
|
|
588
|
+
# --no-skip overwrite if the index already exists in RespLite
|
|
589
|
+
# --no-suggestions skip suggestion import
|
|
590
|
+
```
|
|
356
591
|
|
|
357
|
-
|
|
592
|
+
**Programmatic API:**
|
|
593
|
+
|
|
594
|
+
```javascript
|
|
595
|
+
const m = createMigration({ from, to, runId });
|
|
596
|
+
|
|
597
|
+
const result = await m.migrateSearch({
|
|
598
|
+
onlyIndices: ['products', 'articles'], // omit to migrate all
|
|
599
|
+
batchDocs: 200,
|
|
600
|
+
maxSuggestions: 10000,
|
|
601
|
+
skipExisting: true, // default
|
|
602
|
+
withSuggestions: true, // default
|
|
603
|
+
onProgress: (r) => console.log(r.name, r.docsImported, r.warnings),
|
|
604
|
+
});
|
|
605
|
+
// result.indices: [{ name, created, skipped, docsImported, docsSkipped, docErrors, sugsImported, warnings, error? }]
|
|
606
|
+
// result.aborted: true if interrupted by SIGINT/SIGTERM
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
**What gets migrated:**
|
|
610
|
+
|
|
611
|
+
| RediSearch type | RespLite | Notes |
|
|
612
|
+
|---|---|---|
|
|
613
|
+
| TEXT | TEXT | Direct |
|
|
614
|
+
| TAG | TEXT | Values preserved; TAG filtering lost |
|
|
615
|
+
| NUMERIC | TEXT | Stored as string; numeric range queries not supported |
|
|
616
|
+
| GEO, VECTOR, … | skipped | Warning emitted per field |
|
|
617
|
+
|
|
618
|
+
- Only **HASH**-based indices are supported. JSON (RedisJSON) indices are skipped.
|
|
619
|
+
- A `payload` field is added automatically if none of the source fields maps to it.
|
|
620
|
+
- Suggestions are imported via `FT.SUGGET "" MAX n WITHSCORES` (no cursor; capped at `maxSuggestions`).
|
|
621
|
+
- Graceful shutdown: Ctrl+C finishes the current document, closes SQLite cleanly, and exits with a non-zero code.
|
|
622
|
+
|
|
623
|
+
## Compatibility matrix
|
|
358
624
|
|
|
359
625
|
### Supported (v1)
|
|
360
626
|
|
|
@@ -383,210 +649,6 @@ RESPLite implements **47 core Redis commands** (~19% of the ~246 commands in Red
|
|
|
383
649
|
|
|
384
650
|
Unsupported commands return: `ERR command not supported yet`.
|
|
385
651
|
|
|
386
|
-
## Migration from Redis
|
|
387
|
-
|
|
388
|
-
RESPLite is a good fit for migrating **non-replicated Redis** instances that have **grown large** (e.g. tens of GB) and where RESPLite’s latency is acceptable. The SPEC_F flow (dirty-key tracker, bulk import, cutover) is designed for that scenario with minimal downtime.
|
|
389
|
-
|
|
390
|
-
Migration supports two modes:
|
|
391
|
-
|
|
392
|
-
### Simple one-shot import (legacy)
|
|
393
|
-
|
|
394
|
-
For small datasets or when downtime is acceptable:
|
|
395
|
-
|
|
396
|
-
```bash
|
|
397
|
-
# Default: redis://127.0.0.1:6379 → ./data.db
|
|
398
|
-
npm run import-from-redis -- --db ./migrated.db
|
|
399
|
-
|
|
400
|
-
# Custom Redis URL
|
|
401
|
-
npm run import-from-redis -- --db ./migrated.db --redis-url redis://127.0.0.1:6379
|
|
402
|
-
|
|
403
|
-
# Or host/port
|
|
404
|
-
npm run import-from-redis -- --db ./migrated.db --host 127.0.0.1 --port 6379
|
|
405
|
-
|
|
406
|
-
# Optional: PRAGMA template for the target DB
|
|
407
|
-
npm run import-from-redis -- --db ./migrated.db --pragma-template performance
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
### Redis with authentication
|
|
411
|
-
|
|
412
|
-
Migration supports Redis instances protected by a password. Use a Redis URL that includes the password (or username and password for Redis 6+ ACL):
|
|
413
|
-
|
|
414
|
-
- **Password only:** `redis://:PASSWORD@host:port`
|
|
415
|
-
- **Username and password:** `redis://username:PASSWORD@host:port`
|
|
416
|
-
|
|
417
|
-
Examples:
|
|
418
|
-
|
|
419
|
-
```bash
|
|
420
|
-
# One-shot import from authenticated Redis
|
|
421
|
-
npm run import-from-redis -- --db ./migrated.db --redis-url "redis://:mysecret@127.0.0.1:6379"
|
|
422
|
-
|
|
423
|
-
# SPEC_F flow: use --from with the full URL (or set RESPLITE_IMPORT_FROM)
|
|
424
|
-
npx resplite-import preflight --from "redis://:mysecret@10.0.0.10:6379" --to ./resplite.db
|
|
425
|
-
npx resplite-dirty-tracker start --run-id run_001 --from "redis://:mysecret@10.0.0.10:6379" --to ./resplite.db
|
|
426
|
-
```
|
|
427
|
-
|
|
428
|
-
For one-shot import, authentication is only available when using `--redis-url`; the `--host` / `--port` options do not support a password.
|
|
429
|
-
|
|
430
|
-
### Minimal-downtime migration (SPEC_F)
|
|
431
|
-
|
|
432
|
-
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.
|
|
433
|
-
|
|
434
|
-
**Enable keyspace notifications in Redis** (required for the dirty-key tracker). Either run at runtime:
|
|
435
|
-
|
|
436
|
-
```bash
|
|
437
|
-
redis-cli CONFIG SET notify-keyspace-events KEA
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
Or add to `redis.conf` and restart Redis:
|
|
441
|
-
|
|
442
|
-
```
|
|
443
|
-
notify-keyspace-events KEA
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
(`K` = keyspace prefix, `E` = keyevent prefix, `A` = all event types — lets the tracker see every key change and expiration.)
|
|
447
|
-
|
|
448
|
-
> **Renamed CONFIG command?** Some Redis deployments rename `CONFIG` for security. Pass `--config-command <name>` to the CLI tools, or the `configCommand` option to the JS API — see below.
|
|
449
|
-
|
|
450
|
-
1. **Preflight** – Check Redis, key count, type distribution, and that keyspace notifications are enabled:
|
|
451
|
-
```bash
|
|
452
|
-
npx resplite-import preflight --from redis://10.0.0.10:6379 --to ./resplite.db
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
2. **Start dirty-key tracker** – Captures keys modified during bulk (requires `notify-keyspace-events` in Redis):
|
|
456
|
-
```bash
|
|
457
|
-
npx resplite-dirty-tracker start --run-id run_001 --from redis://10.0.0.10:6379 --to ./resplite.db
|
|
458
|
-
# If CONFIG was renamed:
|
|
459
|
-
npx resplite-dirty-tracker start --run-id run_001 --from redis://10.0.0.10:6379 --to ./resplite.db --config-command MYCONFIG
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
3. **Bulk import** – SCAN and copy all keys; progress is checkpointed and resumable:
|
|
463
|
-
```bash
|
|
464
|
-
npx resplite-import bulk --run-id run_001 --from redis://10.0.0.10:6379 --to ./resplite.db \
|
|
465
|
-
--scan-count 1000 --max-rps 2000 --batch-keys 200 --batch-bytes 64MB --resume
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
4. **Monitor** – Check run and dirty-key counts:
|
|
469
|
-
```bash
|
|
470
|
-
npx resplite-import status --run-id run_001 --to ./resplite.db
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
5. **Cutover** – Freeze app writes to Redis, then apply remaining dirty keys:
|
|
474
|
-
```bash
|
|
475
|
-
npx resplite-import apply-dirty --run-id run_001 --from redis://10.0.0.10:6379 --to ./resplite.db
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
6. **Stop tracker and switch** – Stop the tracker and point clients to RespLite:
|
|
479
|
-
```bash
|
|
480
|
-
npx resplite-dirty-tracker stop --run-id run_001 --to ./resplite.db
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
7. **Verify** – Optional sampling check between Redis and destination:
|
|
484
|
-
```bash
|
|
485
|
-
npx resplite-import verify --run-id run_001 --from redis://10.0.0.10:6379 --to ./resplite.db --sample 0.5%
|
|
486
|
-
```
|
|
487
|
-
|
|
488
|
-
Then start RespLite with the migrated DB: `RESPLITE_DB=./resplite.db npm start`.
|
|
489
|
-
|
|
490
|
-
### Programmatic migration API
|
|
491
|
-
|
|
492
|
-
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.
|
|
493
|
-
|
|
494
|
-
```javascript
|
|
495
|
-
import { createMigration } from 'resplite/migration';
|
|
496
|
-
|
|
497
|
-
const m = createMigration({
|
|
498
|
-
from: 'redis://127.0.0.1:6379', // source Redis URL (default)
|
|
499
|
-
to: './resplite.db', // destination SQLite DB path (required)
|
|
500
|
-
runId: 'my-migration-1', // unique run ID (required for bulk/status/applyDirty)
|
|
501
|
-
|
|
502
|
-
// optional — same defaults as the CLI:
|
|
503
|
-
scanCount: 1000,
|
|
504
|
-
batchKeys: 200,
|
|
505
|
-
batchBytes: 64 * 1024 * 1024, // 64 MB
|
|
506
|
-
maxRps: 0, // 0 = unlimited
|
|
507
|
-
pragmaTemplate: 'default',
|
|
508
|
-
|
|
509
|
-
// If your Redis deployment renamed CONFIG for security:
|
|
510
|
-
// configCommand: 'MYCONFIG',
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
// Step 0 — Preflight: inspect Redis before starting
|
|
514
|
-
const info = await m.preflight();
|
|
515
|
-
console.log('keys (estimate):', info.keyCountEstimate);
|
|
516
|
-
console.log('type distribution:', info.typeDistribution);
|
|
517
|
-
console.log('notify-keyspace-events:', info.notifyKeyspaceEvents);
|
|
518
|
-
console.log('CONFIG available:', info.configCommandAvailable); // false if renamed
|
|
519
|
-
console.log('recommended params:', info.recommended);
|
|
520
|
-
|
|
521
|
-
// Step 0b — Enable keyspace notifications (required for dirty-key tracking)
|
|
522
|
-
// Reads the current value and merges the new flags — existing flags are preserved.
|
|
523
|
-
const ks = await m.enableKeyspaceNotifications();
|
|
524
|
-
// → { ok: true, previous: '', applied: 'KEA' }
|
|
525
|
-
// If CONFIG is renamed and configCommand was not set, ok=false and error explains how to fix it.
|
|
526
|
-
|
|
527
|
-
// Step 1 — Bulk import (checkpointed, resumable)
|
|
528
|
-
await m.bulk({
|
|
529
|
-
resume: false, // true to resume a previous run
|
|
530
|
-
onProgress: (r) => console.log(
|
|
531
|
-
`scanned=${r.scanned_keys} migrated=${r.migrated_keys} errors=${r.error_keys}`
|
|
532
|
-
),
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
// Check status at any point (synchronous, no Redis needed)
|
|
536
|
-
const { run, dirty } = m.status();
|
|
537
|
-
console.log('bulk status:', run.status, '— dirty counts:', dirty);
|
|
538
|
-
|
|
539
|
-
// Step 2 — Apply dirty keys that changed in Redis during bulk
|
|
540
|
-
await m.applyDirty();
|
|
541
|
-
|
|
542
|
-
// Step 3 — Verify a sample of keys match between Redis and the destination
|
|
543
|
-
const result = await m.verify({ samplePct: 0.5, maxSample: 10000 });
|
|
544
|
-
console.log(`verified ${result.sampled} keys — mismatches: ${result.mismatches.length}`);
|
|
545
|
-
|
|
546
|
-
// Disconnect Redis when done
|
|
547
|
-
await m.close();
|
|
548
|
-
```
|
|
549
|
-
|
|
550
|
-
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.
|
|
551
|
-
|
|
552
|
-
#### Renamed CONFIG command
|
|
553
|
-
|
|
554
|
-
If your Redis instance has the `CONFIG` command renamed (a common hardening practice), pass the new name to `createMigration`:
|
|
555
|
-
|
|
556
|
-
```javascript
|
|
557
|
-
const m = createMigration({
|
|
558
|
-
from: 'redis://10.0.0.10:6379',
|
|
559
|
-
to: './resplite.db',
|
|
560
|
-
runId: 'run_001',
|
|
561
|
-
configCommand: 'MYCONFIG', // the renamed command
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
// preflight will use MYCONFIG GET notify-keyspace-events
|
|
565
|
-
const info = await m.preflight();
|
|
566
|
-
// info.configCommandAvailable → false if the name is wrong
|
|
567
|
-
|
|
568
|
-
// enableKeyspaceNotifications will use MYCONFIG SET notify-keyspace-events KEA
|
|
569
|
-
const result = await m.enableKeyspaceNotifications({ value: 'KEA' });
|
|
570
|
-
```
|
|
571
|
-
|
|
572
|
-
The same flag is available in the CLI:
|
|
573
|
-
|
|
574
|
-
```bash
|
|
575
|
-
npx resplite-dirty-tracker start --run-id run_001 --to ./resplite.db \
|
|
576
|
-
--from redis://10.0.0.10:6379 --config-command MYCONFIG
|
|
577
|
-
```
|
|
578
|
-
|
|
579
|
-
#### Low-level re-exports
|
|
580
|
-
|
|
581
|
-
If you need more control, the individual functions and registry helpers are also exported:
|
|
582
|
-
|
|
583
|
-
```javascript
|
|
584
|
-
import {
|
|
585
|
-
runPreflight, runBulkImport, runApplyDirty, runVerify,
|
|
586
|
-
getRun, getDirtyCounts, createRun, setRunStatus, logError,
|
|
587
|
-
} from 'resplite/migration';
|
|
588
|
-
```
|
|
589
|
-
|
|
590
652
|
## Scripts
|
|
591
653
|
|
|
592
654
|
| Script | Description |
|
|
@@ -599,7 +661,7 @@ import {
|
|
|
599
661
|
| `npm run test:stress` | Stress tests |
|
|
600
662
|
| `npm run benchmark` | Comparative benchmark Redis vs RESPLite |
|
|
601
663
|
| `npm run import-from-redis` | One-shot import from Redis into a SQLite DB |
|
|
602
|
-
| `npx resplite-import` (preflight, bulk, status, apply-dirty, verify) | Migration CLI (
|
|
664
|
+
| `npx resplite-import` (preflight, bulk, status, apply-dirty, verify) | Migration CLI (minimal-downtime flow) |
|
|
603
665
|
| `npx resplite-dirty-tracker <start\|stop>` | Dirty-key tracker for migration cutover |
|
|
604
666
|
|
|
605
667
|
## Specification
|
package/package.json
CHANGED
package/spec/SPEC_F.md
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* Perfect change-data-capture guarantees equivalent to replication logs.
|
|
14
14
|
* Distributed migration across multiple import workers with strict ordering semantics.
|
|
15
15
|
* Full fidelity for unsupported Redis data types (streams, modules, Lua scripts, etc.).
|
|
16
|
+
* **Search indices (FT.\*):** Keyspace migration (`bulk` / `apply-dirty`) copies only the Redis KV data (strings, hashes, sets, lists, zsets). RediSearch index schemas and documents are migrated separately via the `migrate-search` step (§F.10).
|
|
16
17
|
|
|
17
18
|
---
|
|
18
19
|
|
|
@@ -258,6 +259,18 @@ Persist:
|
|
|
258
259
|
|
|
259
260
|
If interrupted, `--resume` restarts from the stored cursor.
|
|
260
261
|
|
|
262
|
+
## F.7.2.1 Graceful shutdown (SIGINT / SIGTERM)
|
|
263
|
+
|
|
264
|
+
When the bulk import process receives SIGINT or SIGTERM it must:
|
|
265
|
+
|
|
266
|
+
* Stop the import loop after the current key (no partial key).
|
|
267
|
+
* Write a final checkpoint (cursor and counters) so progress is persisted.
|
|
268
|
+
* Set run status to `aborted`.
|
|
269
|
+
* Close the SQLite database handle so WAL is checkpointed and the file is not left open.
|
|
270
|
+
* Remove signal handlers and exit (rethrow so the process exits non-zero).
|
|
271
|
+
|
|
272
|
+
This ensures the destination DB is always closed cleanly when the process is killed or interrupted; the next run with `--resume` continues from the last checkpoint.
|
|
273
|
+
|
|
261
274
|
## F.7.3 Throughput controls
|
|
262
275
|
|
|
263
276
|
The importer must support:
|
|
@@ -497,6 +510,87 @@ If dirty tracker disconnects:
|
|
|
497
510
|
|
|
498
511
|
---
|
|
499
512
|
|
|
513
|
+
# F.10 Search Index Migration (FT.* / RediSearch)
|
|
514
|
+
|
|
515
|
+
## F.10.1 Overview
|
|
516
|
+
|
|
517
|
+
When the source is a Redis instance with **RediSearch** (Redis Stack or the `redis/search` module), search indices can be migrated with the `migrate-search` step. This step is independent of the KV bulk import and can be run at any time (before or after `bulk`).
|
|
518
|
+
|
|
519
|
+
## F.10.2 Algorithm
|
|
520
|
+
|
|
521
|
+
For each index in the source:
|
|
522
|
+
|
|
523
|
+
1. **`FT._LIST`** → enumerate all index names.
|
|
524
|
+
2. **`FT.INFO <name>`** → read `index_definition` (key type, prefix patterns) and `attributes` (field names and types).
|
|
525
|
+
3. **Schema mapping** (see §F.10.3).
|
|
526
|
+
4. **`FT.CREATE`** in RespLite with the mapped schema. Skip if already exists (controlled by `skipExisting`).
|
|
527
|
+
5. **SCAN** keys matching each index prefix → **HGETALL** → `addDocument` in SQLite batches.
|
|
528
|
+
6. **`FT.SUGGET "" MAX n WITHSCORES`** → import suggestions into RespLite.
|
|
529
|
+
|
|
530
|
+
## F.10.3 Field type mapping
|
|
531
|
+
|
|
532
|
+
| RediSearch type | RespLite type | Notes |
|
|
533
|
+
|-----------------|---------------|-------|
|
|
534
|
+
| TEXT | TEXT | Direct mapping |
|
|
535
|
+
| TAG | TEXT | Values preserved as-is; TAG filtering semantics lost |
|
|
536
|
+
| NUMERIC | TEXT | Values stored as strings; numeric range queries not supported |
|
|
537
|
+
| GEO, VECTOR, … | — | Skipped with warning |
|
|
538
|
+
|
|
539
|
+
RespLite requires a `payload` TEXT field. If none of the source fields maps to `payload`, a `payload` field is added automatically and synthesised at import time by concatenating all other text values.
|
|
540
|
+
|
|
541
|
+
## F.10.4 Constraints
|
|
542
|
+
|
|
543
|
+
* Only **HASH**-based indices are supported (`key_type = HASH`). JSON indices (RedisJSON) are skipped with an error.
|
|
544
|
+
* Index names must match `[A-Za-z][A-Za-z0-9:_-]{0,63}`. Indices with invalid names are skipped with an error.
|
|
545
|
+
* `FT.SUGGET` has no cursor; suggestions are imported up to `maxSuggestions` (default 10 000).
|
|
546
|
+
* Document score is read from the `__score` or `score` hash field if present; defaults to `1.0`.
|
|
547
|
+
|
|
548
|
+
## F.10.5 Graceful shutdown
|
|
549
|
+
|
|
550
|
+
Same pattern as `bulk` (§F.7.2.1): SIGINT/SIGTERM finishes the current document, closes the SQLite DB cleanly, and exits with a non-zero code.
|
|
551
|
+
|
|
552
|
+
## F.10.6 CLI
|
|
553
|
+
|
|
554
|
+
```bash
|
|
555
|
+
# Migrate all RediSearch indices
|
|
556
|
+
resplite-import migrate-search \
|
|
557
|
+
--from redis://10.0.0.10:6379 \
|
|
558
|
+
--to ./resplite.db
|
|
559
|
+
|
|
560
|
+
# Migrate specific indices only
|
|
561
|
+
resplite-import migrate-search \
|
|
562
|
+
--from redis://10.0.0.10:6379 \
|
|
563
|
+
--to ./resplite.db \
|
|
564
|
+
--index products \
|
|
565
|
+
--index articles
|
|
566
|
+
|
|
567
|
+
# Options
|
|
568
|
+
# --scan-count N SCAN COUNT hint (default 500)
|
|
569
|
+
# --max-rps N throttle (default unlimited)
|
|
570
|
+
# --batch-docs N docs per SQLite transaction (default 200)
|
|
571
|
+
# --max-suggestions N cap for FT.SUGGET (default 10000)
|
|
572
|
+
# --no-skip overwrite if index already exists
|
|
573
|
+
# --no-suggestions skip suggestion import
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
## F.10.7 Programmatic API
|
|
577
|
+
|
|
578
|
+
```javascript
|
|
579
|
+
const m = createMigration({ from, to, runId });
|
|
580
|
+
|
|
581
|
+
const result = await m.migrateSearch({
|
|
582
|
+
onlyIndices: ['products', 'articles'], // omit for all
|
|
583
|
+
batchDocs: 200,
|
|
584
|
+
maxSuggestions: 10000,
|
|
585
|
+
skipExisting: true,
|
|
586
|
+
withSuggestions: true,
|
|
587
|
+
onProgress: (r) => console.log(r.name, r.docsImported, r.warnings),
|
|
588
|
+
});
|
|
589
|
+
// result: { indices: [{ name, created, skipped, docsImported, docsSkipped, docErrors, sugsImported, warnings, error? }], aborted }
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
---
|
|
593
|
+
|
|
500
594
|
# F.12 Operational Guidance (Large datasets)
|
|
501
595
|
|
|
502
596
|
* Use a dedicated Redis replica for reads if possible to reduce load on primary.
|