@delali/sirannon-db 0.2.3-next.37 → 0.2.3-next.39
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/dist/backup/index.d.ts +6 -84
- package/dist/backup/index.mjs +6 -584
- package/dist/backup-scheduler/index.d.ts +58 -8
- package/dist/backup-scheduler/index.mjs +3 -2
- package/dist/{change-tracker-DXHPFe9J.d.ts → change-tracker-uG9gWvsY.d.ts} +1 -1
- package/dist/chunk-2QCPVE67.mjs +143 -0
- package/dist/chunk-63VBFSCS.mjs +525 -0
- package/dist/chunk-7SC2TZAS.mjs +41 -0
- package/dist/{chunk-72I5UI5C.mjs → chunk-DVS4QSIZ.mjs} +51 -23
- package/dist/{chunk-BQTVNI5H.mjs → chunk-DXXCLTFF.mjs} +205 -45
- package/dist/{chunk-BQFQ65OL.mjs → chunk-LXCGPZPA.mjs} +9 -1
- package/dist/{chunk-ZKDBGX7H.mjs → chunk-OJQHM4MT.mjs} +365 -357
- package/dist/chunk-U445A3C4.mjs +88 -0
- package/dist/chunk-W77EEVOT.mjs +156 -0
- package/dist/client/index.d.ts +8 -8
- package/dist/client/topology.d.ts +4 -4
- package/dist/{client-base-CGBpjZUB.d.ts → client-base-C-B8u5Lm.d.ts} +2 -2
- package/dist/core/index.d.ts +9 -9
- package/dist/core/index.mjs +228 -48
- package/dist/{database-955qag_W.d.ts → database-BlhUdJAO.d.ts} +46 -39
- package/dist/{chain-queries-D5aZ-8v2.d.ts → database-backup-C090lMHc.d.ts} +52 -2
- package/dist/driver/better-sqlite3.d.ts +1 -1
- package/dist/driver/better-sqlite3.mjs +9 -6
- package/dist/driver/bun.d.ts +1 -1
- package/dist/driver/expo.d.ts +1 -1
- package/dist/driver/node.d.ts +1 -1
- package/dist/driver/node.mjs +9 -6
- package/dist/driver/wa-sqlite.d.ts +1 -1
- package/dist/file-migrations/index.d.ts +1 -1
- package/dist/{protocol-BfiujBsE.d.ts → protocol-QekTGoKn.d.ts} +2 -2
- package/dist/replication/index.d.ts +7 -7
- package/dist/restore-options-C9MW9gpm.d.ts +83 -0
- package/dist/server/index.d.ts +102 -9
- package/dist/server/index.mjs +355 -3
- package/dist/{server-options-D4VmfTHw.d.ts → server-options-DeK6ZG8u.d.ts} +8 -1
- package/dist/{sirannon-DaGUawHs.d.ts → sirannon-C9cTgFrw.d.ts} +48 -5
- package/dist/transport/grpc.d.ts +3 -3
- package/dist/transport/memory.d.ts +3 -3
- package/dist/{types-BEjS69rs.d.ts → types-C1yBGTdx.d.ts} +2 -2
- package/dist/{types-Xb0L3o3w.d.ts → types-DApb7dvw.d.ts} +266 -23
- package/package.json +8 -7
- package/dist/chunk-GBM4AU24.mjs +0 -42
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { BackupManager, BackupScheduler, DEFAULT_PIECE_BYTES, sendFileInPieces } from './chunk-
|
|
3
|
-
import {
|
|
4
|
-
import { DEFAULT_CHAIN_NAME, readBackupChains, readChainHeadAt, readChainHeads, appendChainRecord, appendChainHead, isBackupChainPosition } from './chunk-YVF2ZJTE.mjs';
|
|
1
|
+
import { verifyBackupRecord } from './chunk-7SC2TZAS.mjs';
|
|
2
|
+
import { BackupManager, BackupScheduler, DEFAULT_PIECE_BYTES, sendFileInPieces } from './chunk-DXXCLTFF.mjs';
|
|
3
|
+
import { DEFAULT_BACKUP_NAME_PREFIX, defaultStagingDir, DEFAULT_CAPTURE_INTERVAL_MS, DEFAULT_FULL_COPY_INTERVAL_MS } from './chunk-LXCGPZPA.mjs';
|
|
5
4
|
import { WORKER_CANCELLED_CODE, deserializeError } from './chunk-YLAVDZCS.mjs';
|
|
5
|
+
import { readCycleState, writeCycleState, removeCycleState, stagedCapturePath, captureAndCheckpointTurn } from './chunk-W77EEVOT.mjs';
|
|
6
|
+
import { checkpointLog } from './chunk-U445A3C4.mjs';
|
|
7
|
+
import { logPathFor } from './chunk-2QCPVE67.mjs';
|
|
8
|
+
import { destinationWithDeadline, DEFAULT_DESTINATION_TIMEOUT_MS } from './chunk-DVS4QSIZ.mjs';
|
|
9
|
+
import { DEFAULT_CHAIN_NAME, readBackupChains, appendChainRecord, appendChainHead, readChainHeadAt, readChainHeads } from './chunk-YVF2ZJTE.mjs';
|
|
6
10
|
import { randomHex } from './chunk-V5FUJEYY.mjs';
|
|
7
11
|
import { SirannonError, ExtensionError } from './chunk-PBRXXISQ.mjs';
|
|
8
12
|
import { Worker } from 'worker_threads';
|
|
@@ -10,7 +14,7 @@ import { existsSync } from 'fs';
|
|
|
10
14
|
import { resolve, dirname, join, sep } from 'path';
|
|
11
15
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
12
16
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
13
|
-
import { mkdir, rm,
|
|
17
|
+
import { mkdir, rm, stat } from 'fs/promises';
|
|
14
18
|
import { createRequire } from 'module';
|
|
15
19
|
|
|
16
20
|
// src/core/driver/extension.ts
|
|
@@ -403,196 +407,6 @@ function clearPendingTimers(entry) {
|
|
|
403
407
|
if (entry.timer) clearTimeout(entry.timer);
|
|
404
408
|
if (entry.graceTimer) clearTimeout(entry.graceTimer);
|
|
405
409
|
}
|
|
406
|
-
var READ_CHUNK_BYTES = 4 * 1024 * 1024;
|
|
407
|
-
async function openForReading(path) {
|
|
408
|
-
try {
|
|
409
|
-
return await open(path, "r");
|
|
410
|
-
} catch (err) {
|
|
411
|
-
if (err.code === "ENOENT") return void 0;
|
|
412
|
-
throw err;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
async function readLogFileHeader(logPath) {
|
|
416
|
-
const file = await openForReading(logPath);
|
|
417
|
-
if (!file) return void 0;
|
|
418
|
-
try {
|
|
419
|
-
const buffer = Buffer.allocUnsafe(LOG_HEADER_BYTES);
|
|
420
|
-
const { bytesRead } = await file.read(buffer, 0, LOG_HEADER_BYTES, 0);
|
|
421
|
-
if (bytesRead < LOG_HEADER_BYTES) return void 0;
|
|
422
|
-
return readLogHeader(buffer);
|
|
423
|
-
} finally {
|
|
424
|
-
await file.close();
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
async function scanLogFrames(logPath, header, from) {
|
|
428
|
-
const stopped = {
|
|
429
|
-
lastCommitFrame: from.frame,
|
|
430
|
-
endOffset: logFrameOffset(from.frame + 1, header.frameBytes),
|
|
431
|
-
checksum: from.checksum
|
|
432
|
-
};
|
|
433
|
-
const file = await openForReading(logPath);
|
|
434
|
-
if (!file) return stopped;
|
|
435
|
-
try {
|
|
436
|
-
const { size } = await file.stat();
|
|
437
|
-
const framesInFile = Math.max(0, Math.floor((size - LOG_HEADER_BYTES) / header.frameBytes));
|
|
438
|
-
const framesPerChunk = Math.max(1, Math.floor(READ_CHUNK_BYTES / header.frameBytes));
|
|
439
|
-
const buffer = Buffer.allocUnsafe(framesPerChunk * header.frameBytes);
|
|
440
|
-
const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
441
|
-
let running = from.checksum;
|
|
442
|
-
let found = stopped;
|
|
443
|
-
let frame = from.frame + 1;
|
|
444
|
-
while (frame <= framesInFile) {
|
|
445
|
-
const frames = Math.min(framesPerChunk, framesInFile - frame + 1);
|
|
446
|
-
const wanted = frames * header.frameBytes;
|
|
447
|
-
let filled = 0;
|
|
448
|
-
while (filled < wanted) {
|
|
449
|
-
const { bytesRead } = await file.read(
|
|
450
|
-
buffer,
|
|
451
|
-
filled,
|
|
452
|
-
wanted - filled,
|
|
453
|
-
logFrameOffset(frame, header.frameBytes) + filled
|
|
454
|
-
);
|
|
455
|
-
if (bytesRead === 0) break;
|
|
456
|
-
filled += bytesRead;
|
|
457
|
-
}
|
|
458
|
-
const readable = Math.floor(filled / header.frameBytes);
|
|
459
|
-
if (readable === 0) break;
|
|
460
|
-
for (let inChunk = 0; inChunk < readable; inChunk++) {
|
|
461
|
-
const read = readValidLogFrame(view, inChunk * header.frameBytes, header, running);
|
|
462
|
-
if (!read) return found;
|
|
463
|
-
running = read.checksum;
|
|
464
|
-
if (read.frame.databasePages !== 0) {
|
|
465
|
-
found = {
|
|
466
|
-
lastCommitFrame: frame + inChunk,
|
|
467
|
-
endOffset: logFrameOffset(frame + inChunk + 1, header.frameBytes),
|
|
468
|
-
checksum: running
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
frame += readable;
|
|
473
|
-
}
|
|
474
|
-
return found;
|
|
475
|
-
} finally {
|
|
476
|
-
await file.close();
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
async function copyLogRange(logPath, startOffset, endOffset, destPath) {
|
|
480
|
-
const source = await open(logPath, "r");
|
|
481
|
-
try {
|
|
482
|
-
const dest = await open(destPath, "w");
|
|
483
|
-
try {
|
|
484
|
-
const buffer = Buffer.allocUnsafe(Math.min(READ_CHUNK_BYTES, Math.max(endOffset - startOffset, 1)));
|
|
485
|
-
let at = startOffset;
|
|
486
|
-
let written = 0;
|
|
487
|
-
while (at < endOffset) {
|
|
488
|
-
const wanted = Math.min(buffer.byteLength, endOffset - at);
|
|
489
|
-
const { bytesRead } = await source.read(buffer, 0, wanted, at);
|
|
490
|
-
if (bytesRead === 0) {
|
|
491
|
-
throw new SirannonError(
|
|
492
|
-
`The write-ahead log '${logPath}' ends at byte ${at} while the frames captured from it run to byte ${endOffset}, so it lost frames while Sirannon was reading it. Another connection checkpointed the log. Route every write and every checkpoint through Sirannon, and take a fresh full copy so a new chain starts from a known state.`,
|
|
493
|
-
"BACKUP_LOG_REWOUND"
|
|
494
|
-
);
|
|
495
|
-
}
|
|
496
|
-
let put = 0;
|
|
497
|
-
while (put < bytesRead) {
|
|
498
|
-
const { bytesWritten } = await dest.write(buffer, put, bytesRead - put);
|
|
499
|
-
if (bytesWritten === 0) {
|
|
500
|
-
throw new SirannonError(
|
|
501
|
-
`The capture of '${logPath}' could write no more than ${written + put} of its ${endOffset - startOffset} bytes into '${destPath}'. Check the free space and the permissions on that directory.`,
|
|
502
|
-
"BACKUP_ERROR"
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
put += bytesWritten;
|
|
506
|
-
}
|
|
507
|
-
at += bytesRead;
|
|
508
|
-
written += bytesRead;
|
|
509
|
-
}
|
|
510
|
-
return written;
|
|
511
|
-
} finally {
|
|
512
|
-
await dest.close();
|
|
513
|
-
}
|
|
514
|
-
} finally {
|
|
515
|
-
await source.close();
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
function cursorChecksum(cursor) {
|
|
519
|
-
return { first: cursor.checksum1, second: cursor.checksum2 };
|
|
520
|
-
}
|
|
521
|
-
function sameLog(header, cursor) {
|
|
522
|
-
return header.salt1 === cursor.salt1 && header.salt2 === cursor.salt2;
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
// src/core/backup/cycle-capture.ts
|
|
526
|
-
function stagedCapturePath(stagingDir, sequence) {
|
|
527
|
-
return join(stagingDir, `capture-${sequence}.wal`);
|
|
528
|
-
}
|
|
529
|
-
function rewoundError(request, detail) {
|
|
530
|
-
return new SirannonError(
|
|
531
|
-
`The write-ahead log of '${request.sourcePath}' ${detail}, so the frames written before it did are in no backup. Another connection checkpointed the log, or the database file was replaced. Route every write and every checkpoint through Sirannon, and take a fresh full copy so a new chain starts from a known state.`,
|
|
532
|
-
"BACKUP_LOG_REWOUND"
|
|
533
|
-
);
|
|
534
|
-
}
|
|
535
|
-
async function captureLogFrames(request) {
|
|
536
|
-
const startedAt = Date.now();
|
|
537
|
-
const cursor = request.cursor;
|
|
538
|
-
const header = await readLogFileHeader(request.logPath);
|
|
539
|
-
const holdsEveryCapturedFrame = cursor !== null && header !== void 0 && sameLog(header, cursor);
|
|
540
|
-
const newLogAllowed = cursor === null || cursor.checkpointed || request.expectNewLog;
|
|
541
|
-
if (!holdsEveryCapturedFrame && !newLogAllowed && cursor) {
|
|
542
|
-
throw rewoundError(
|
|
543
|
-
request,
|
|
544
|
-
header ? `restarted at checkpoint sequence ${header.logSequence} while the chain reaches frame ${cursor.lastFrame} of sequence ${cursor.logSequence}` : `holds no frames while the chain reaches frame ${cursor.lastFrame} of sequence ${cursor.logSequence}`
|
|
545
|
-
);
|
|
546
|
-
}
|
|
547
|
-
if (!header) return void 0;
|
|
548
|
-
const from = cursor && holdsEveryCapturedFrame ? { frame: cursor.lastFrame, checksum: cursorChecksum(cursor) } : { frame: 0, checksum: header.checksum };
|
|
549
|
-
const scan = await scanLogFrames(request.logPath, header, from);
|
|
550
|
-
if (scan.lastCommitFrame <= from.frame) {
|
|
551
|
-
if (from.frame > 0 && scan.lastCommitFrame < from.frame) {
|
|
552
|
-
throw rewoundError(
|
|
553
|
-
request,
|
|
554
|
-
`now ends at frame ${scan.lastCommitFrame} while the chain reaches frame ${from.frame}`
|
|
555
|
-
);
|
|
556
|
-
}
|
|
557
|
-
return void 0;
|
|
558
|
-
}
|
|
559
|
-
const firstFrame = from.frame + 1;
|
|
560
|
-
const startOffset = firstFrame === 1 ? 0 : logFrameOffset(firstFrame, header.frameBytes);
|
|
561
|
-
const byteLength = await copyLogRange(
|
|
562
|
-
request.logPath,
|
|
563
|
-
startOffset,
|
|
564
|
-
scan.endOffset,
|
|
565
|
-
stagedCapturePath(request.stagingDir, request.sequence)
|
|
566
|
-
);
|
|
567
|
-
const capturedAt = Date.now();
|
|
568
|
-
return {
|
|
569
|
-
name: `${request.namePrefix}-${request.chainId}-${String(request.sequence).padStart(6, "0")}.wal`,
|
|
570
|
-
runId: randomHex(8),
|
|
571
|
-
sequence: request.sequence,
|
|
572
|
-
position: {
|
|
573
|
-
logSequence: header.logSequence,
|
|
574
|
-
salt1: header.salt1,
|
|
575
|
-
salt2: header.salt2,
|
|
576
|
-
firstFrame,
|
|
577
|
-
lastFrame: scan.lastCommitFrame
|
|
578
|
-
},
|
|
579
|
-
cursor: {
|
|
580
|
-
logSequence: header.logSequence,
|
|
581
|
-
salt1: header.salt1,
|
|
582
|
-
salt2: header.salt2,
|
|
583
|
-
lastFrame: scan.lastCommitFrame,
|
|
584
|
-
checksum1: scan.checksum.first,
|
|
585
|
-
checksum2: scan.checksum.second,
|
|
586
|
-
checkpointed: false
|
|
587
|
-
},
|
|
588
|
-
startedAt,
|
|
589
|
-
capturedAt,
|
|
590
|
-
copyMs: capturedAt - startedAt,
|
|
591
|
-
frameCount: scan.lastCommitFrame - from.frame,
|
|
592
|
-
byteLength,
|
|
593
|
-
pageSize: header.pageSize
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
410
|
|
|
597
411
|
// src/core/backup/preferred-node.ts
|
|
598
412
|
function preferredBackupNode(membership, preference) {
|
|
@@ -726,15 +540,209 @@ async function logGrownPastLimit(logPath, maxBytes, databaseId) {
|
|
|
726
540
|
"BACKUP_CHAIN_BROKEN"
|
|
727
541
|
);
|
|
728
542
|
}
|
|
543
|
+
async function chainLostFromList(request, chainName, state) {
|
|
544
|
+
return chainMissingFromList(request.destination, chainName, state.chainId, request.databaseId, state.headIndex);
|
|
545
|
+
}
|
|
729
546
|
|
|
730
|
-
// src/core/backup/cycle-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
547
|
+
// src/core/backup/cycle-callbacks.ts
|
|
548
|
+
function reportQuietly(callback, value) {
|
|
549
|
+
if (!callback) return;
|
|
550
|
+
try {
|
|
551
|
+
callback(value);
|
|
552
|
+
} catch {
|
|
553
|
+
}
|
|
736
554
|
}
|
|
737
555
|
|
|
556
|
+
// src/core/backup/cycle-status.ts
|
|
557
|
+
var BackupCycleStatusRecorder = class {
|
|
558
|
+
turnRunning = false;
|
|
559
|
+
turnStartedAt = 0;
|
|
560
|
+
latestProgress;
|
|
561
|
+
latestRun;
|
|
562
|
+
latestSkip;
|
|
563
|
+
latestError;
|
|
564
|
+
/** Marks a turn as under way and clears the progress the previous turn recorded. */
|
|
565
|
+
turnStarted() {
|
|
566
|
+
this.turnRunning = true;
|
|
567
|
+
this.turnStartedAt = Date.now();
|
|
568
|
+
this.latestProgress = void 0;
|
|
569
|
+
}
|
|
570
|
+
/** Marks the turn as finished. A caller then reads the outcome from the last run, skip, or failure. */
|
|
571
|
+
turnFinished() {
|
|
572
|
+
this.turnRunning = false;
|
|
573
|
+
this.turnStartedAt = 0;
|
|
574
|
+
this.latestProgress = void 0;
|
|
575
|
+
}
|
|
576
|
+
/** Records how far the turn under way has got. */
|
|
577
|
+
progressed(progress) {
|
|
578
|
+
this.latestProgress = progress;
|
|
579
|
+
}
|
|
580
|
+
/** Records what a finished turn wrote. */
|
|
581
|
+
ran(report) {
|
|
582
|
+
this.latestRun = report;
|
|
583
|
+
}
|
|
584
|
+
/** Records a turn the cycle passed over. */
|
|
585
|
+
skipped(skip) {
|
|
586
|
+
this.latestSkip = skip;
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Records a failure, under the code a caller matches on, along with how far
|
|
590
|
+
* the run had got and which chain it was extending.
|
|
591
|
+
*
|
|
592
|
+
* @param err - What the turn failed with.
|
|
593
|
+
* @param chainId - The chain the cycle was extending, where it held one.
|
|
594
|
+
*/
|
|
595
|
+
failed(err, chainId) {
|
|
596
|
+
const at = Date.now();
|
|
597
|
+
this.latestError = {
|
|
598
|
+
code: err instanceof SirannonError ? err.code : "BACKUP_ERROR",
|
|
599
|
+
message: err.message,
|
|
600
|
+
at,
|
|
601
|
+
durationMs: this.turnStartedAt === 0 ? 0 : at - this.turnStartedAt,
|
|
602
|
+
...chainId === void 0 ? {} : { chainId },
|
|
603
|
+
...this.latestProgress === void 0 ? {} : { progress: this.latestProgress }
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Reads everything recorded so far.
|
|
608
|
+
*
|
|
609
|
+
* @param chainId - The chain the cycle is extending, where it has one.
|
|
610
|
+
* @returns What the cycle is doing, and what its recent turns produced.
|
|
611
|
+
*/
|
|
612
|
+
read(chainId) {
|
|
613
|
+
return {
|
|
614
|
+
running: this.turnRunning,
|
|
615
|
+
...chainId === void 0 ? {} : { chainId },
|
|
616
|
+
...this.latestProgress === void 0 ? {} : { progress: this.latestProgress },
|
|
617
|
+
...this.latestRun === void 0 ? {} : { lastRun: this.latestRun },
|
|
618
|
+
...this.latestSkip === void 0 ? {} : { lastSkip: this.latestSkip },
|
|
619
|
+
...this.latestError === void 0 ? {} : { lastError: this.latestError }
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
// src/core/backup/cycle-reporting.ts
|
|
625
|
+
var BackupTurnLog = class {
|
|
626
|
+
constructor(audience, logPath) {
|
|
627
|
+
this.audience = audience;
|
|
628
|
+
this.logPath = logPath;
|
|
629
|
+
}
|
|
630
|
+
record = new BackupCycleStatusRecorder();
|
|
631
|
+
announced = /* @__PURE__ */ new Set();
|
|
632
|
+
/**
|
|
633
|
+
* Records a finished run and passes it to the caller.
|
|
634
|
+
*
|
|
635
|
+
* @param report - What that run produced.
|
|
636
|
+
*/
|
|
637
|
+
ran(report) {
|
|
638
|
+
this.record.ran(report);
|
|
639
|
+
reportQuietly(this.audience.onRun, report);
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Records how far the run under way has reached and passes that to the caller.
|
|
643
|
+
*
|
|
644
|
+
* @param progress - The counters that run has reached.
|
|
645
|
+
*/
|
|
646
|
+
progressed(progress) {
|
|
647
|
+
this.record.progressed(progress);
|
|
648
|
+
reportQuietly(this.audience.onProgress, progress);
|
|
649
|
+
}
|
|
650
|
+
/** Marks a turn as under way, and forgets whatever failures the previous turn announced. */
|
|
651
|
+
turnStarted() {
|
|
652
|
+
this.announced.clear();
|
|
653
|
+
this.record.turnStarted();
|
|
654
|
+
}
|
|
655
|
+
/** Marks the turn as finished. */
|
|
656
|
+
turnFinished() {
|
|
657
|
+
this.record.turnFinished();
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Records a failure as the turn's outcome and passes it to the caller.
|
|
661
|
+
*
|
|
662
|
+
* @param err - What the turn failed with.
|
|
663
|
+
* @param chainId - The chain to record it against, where the cycle holds one.
|
|
664
|
+
*/
|
|
665
|
+
failed(err, chainId) {
|
|
666
|
+
this.announced.add(err);
|
|
667
|
+
const failure = toBackupError(err);
|
|
668
|
+
this.record.failed(failure, chainId);
|
|
669
|
+
reportQuietly(this.audience.onError, failure);
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Answers whether this turn has already passed one failure to the caller.
|
|
673
|
+
*
|
|
674
|
+
* @param err - The failure to look for.
|
|
675
|
+
* @returns Whether the caller has already heard about that failure.
|
|
676
|
+
*/
|
|
677
|
+
hasAnnounced(err) {
|
|
678
|
+
return this.announced.has(err);
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Records a turn the cycle passed over and passes it to the caller, stating
|
|
682
|
+
* how much log this node was still holding as it skipped.
|
|
683
|
+
*
|
|
684
|
+
* @param skip - Why the cycle passed the turn over.
|
|
685
|
+
*/
|
|
686
|
+
async skipped(skip) {
|
|
687
|
+
const held = await uncapturedLogBytes(this.logPath);
|
|
688
|
+
const passed = held === void 0 ? skip : { ...skip, uncapturedLogBytes: held };
|
|
689
|
+
this.record.skipped(passed);
|
|
690
|
+
reportQuietly(this.audience.onSkip, passed);
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Reads everything recorded so far.
|
|
694
|
+
*
|
|
695
|
+
* @param chainId - The chain the cycle is extending, where it has one.
|
|
696
|
+
* @returns What the cycle is doing, and what its recent turns produced.
|
|
697
|
+
*/
|
|
698
|
+
read(chainId) {
|
|
699
|
+
return this.record.read(chainId);
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
// src/core/backup/cycle-scheduling.ts
|
|
704
|
+
var BackupCycleTimer = class {
|
|
705
|
+
handle = null;
|
|
706
|
+
/**
|
|
707
|
+
* Starts repeating. At an interval of zero or less the cycle takes a turn
|
|
708
|
+
* only when somebody asks it to.
|
|
709
|
+
*
|
|
710
|
+
* @param intervalMs - Milliseconds between one turn and the next.
|
|
711
|
+
* @param tick - Called on every interval.
|
|
712
|
+
*/
|
|
713
|
+
arm(intervalMs, tick) {
|
|
714
|
+
this.disarm();
|
|
715
|
+
if (intervalMs <= 0) return;
|
|
716
|
+
this.handle = setInterval(tick, intervalMs);
|
|
717
|
+
this.handle.unref?.();
|
|
718
|
+
}
|
|
719
|
+
/** Stops repeating. */
|
|
720
|
+
disarm() {
|
|
721
|
+
if (!this.handle) return;
|
|
722
|
+
clearInterval(this.handle);
|
|
723
|
+
this.handle = null;
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
var SerialTurns = class {
|
|
727
|
+
inFlight = Promise.resolve();
|
|
728
|
+
/**
|
|
729
|
+
* Starts an operation once every turn asked for before it has settled.
|
|
730
|
+
*
|
|
731
|
+
* @param op - The turn to take.
|
|
732
|
+
* @returns Whatever that turn produced.
|
|
733
|
+
*/
|
|
734
|
+
run(op) {
|
|
735
|
+
const turn = this.inFlight.then(op, op);
|
|
736
|
+
this.inFlight = turn.then(
|
|
737
|
+
() => {
|
|
738
|
+
},
|
|
739
|
+
() => {
|
|
740
|
+
}
|
|
741
|
+
);
|
|
742
|
+
return turn;
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
|
|
738
746
|
// src/core/backup/cycle-standdown.ts
|
|
739
747
|
async function sendBeforeStandDown(cycle) {
|
|
740
748
|
try {
|
|
@@ -762,43 +770,13 @@ async function releaseChainPastLogLimit(cycle) {
|
|
|
762
770
|
cycle.report(lost);
|
|
763
771
|
return true;
|
|
764
772
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
return
|
|
772
|
-
}
|
|
773
|
-
function isPendingCapture(value) {
|
|
774
|
-
const pending = value;
|
|
775
|
-
return typeof pending?.name === "string" && typeof pending.runId === "string" && isWholeNumber(pending.sequence) && pending.sequence >= 1 && isBackupChainPosition(pending.position) && isCursor(pending.cursor) && isWholeNumber(pending.startedAt) && isWholeNumber(pending.capturedAt) && isWholeNumber(pending.copyMs) && isWholeNumber(pending.frameCount) && isWholeNumber(pending.byteLength) && isWholeNumber(pending.pageSize);
|
|
776
|
-
}
|
|
777
|
-
function isCycleState(value) {
|
|
778
|
-
const state = value;
|
|
779
|
-
return typeof state?.chainName === "string" && typeof state.chainId === "string" && isWholeNumber(state.chainStartedAt) && (state.headIndex === void 0 || isWholeNumber(state.headIndex)) && isWholeNumber(state.records) && (state.cursor === null || isCursor(state.cursor)) && (state.pending === null || isPendingCapture(state.pending)) && typeof state.closedCleanly === "boolean";
|
|
780
|
-
}
|
|
781
|
-
function cycleStatePath(stagingDir) {
|
|
782
|
-
return join(stagingDir, STATE_FILE_NAME);
|
|
783
|
-
}
|
|
784
|
-
async function readCycleState(stagingDir) {
|
|
785
|
-
try {
|
|
786
|
-
const text = await readFile(cycleStatePath(stagingDir), "utf8");
|
|
787
|
-
const state = JSON.parse(text);
|
|
788
|
-
return isCycleState(state) ? state : void 0;
|
|
789
|
-
} catch {
|
|
790
|
-
return void 0;
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
async function writeCycleState(stagingDir, state) {
|
|
794
|
-
const path = cycleStatePath(stagingDir);
|
|
795
|
-
await mkdir(dirname(path), { recursive: true });
|
|
796
|
-
const staged = `${path}.writing`;
|
|
797
|
-
await writeFile(staged, JSON.stringify(state), "utf8");
|
|
798
|
-
await rename(staged, path);
|
|
799
|
-
}
|
|
800
|
-
async function removeCycleState(stagingDir) {
|
|
801
|
-
await rm(cycleStatePath(stagingDir), { force: true });
|
|
773
|
+
async function confirmChainStillListed(grip) {
|
|
774
|
+
if (grip.verified || !grip.state) return grip.verified;
|
|
775
|
+
const lost = await chainLostFromList(grip.request, grip.chainName, grip.state);
|
|
776
|
+
if (!lost) return true;
|
|
777
|
+
await grip.discardState();
|
|
778
|
+
grip.report(lost);
|
|
779
|
+
return false;
|
|
802
780
|
}
|
|
803
781
|
async function startChain(request, chainName, namePrefix, previousChainId) {
|
|
804
782
|
const chainId = randomHex(8);
|
|
@@ -814,7 +792,8 @@ async function startChain(request, chainName, namePrefix, previousChainId) {
|
|
|
814
792
|
...request.restartLimit === void 0 ? {} : { restartLimit: request.restartLimit },
|
|
815
793
|
...request.stallTimeoutMs === void 0 ? {} : { stallTimeoutMs: request.stallTimeoutMs },
|
|
816
794
|
...request.destinationTimeoutMs === void 0 ? {} : { destinationTimeoutMs: request.destinationTimeoutMs },
|
|
817
|
-
...request.noProgressStepLimit === void 0 ? {} : { noProgressStepLimit: request.noProgressStepLimit }
|
|
795
|
+
...request.noProgressStepLimit === void 0 ? {} : { noProgressStepLimit: request.noProgressStepLimit },
|
|
796
|
+
...request.onProgress === void 0 ? {} : { onProgress: request.onProgress }
|
|
818
797
|
});
|
|
819
798
|
const base = {
|
|
820
799
|
kind: "full",
|
|
@@ -845,8 +824,15 @@ async function transferCapture(request, chainName, chainId, pending, recordIndex
|
|
|
845
824
|
pending.name,
|
|
846
825
|
pieceBytes,
|
|
847
826
|
request.fingerprint ?? true,
|
|
848
|
-
() => {
|
|
849
|
-
|
|
827
|
+
(piecesWritten, bytesWritten) => request.onProgress?.({
|
|
828
|
+
runId: pending.runId,
|
|
829
|
+
phase: "transfer",
|
|
830
|
+
totalPages: pending.frameCount,
|
|
831
|
+
remainingPages: 0,
|
|
832
|
+
restarts: 0,
|
|
833
|
+
piecesWritten,
|
|
834
|
+
bytesWritten
|
|
835
|
+
})
|
|
850
836
|
);
|
|
851
837
|
const finishedAt = Date.now();
|
|
852
838
|
const record = {
|
|
@@ -907,20 +893,24 @@ async function sendStagedCapture(request, chainName, stagingDir, state) {
|
|
|
907
893
|
request.onRun?.(report);
|
|
908
894
|
return report;
|
|
909
895
|
}
|
|
896
|
+
async function beginReplacementChain(request, chainName, namePrefix, stagingDir, previousChainId) {
|
|
897
|
+
const started = await startChain(request, chainName, namePrefix, previousChainId);
|
|
898
|
+
const state = {
|
|
899
|
+
chainName,
|
|
900
|
+
chainId: started.chainId,
|
|
901
|
+
chainStartedAt: started.startedAt,
|
|
902
|
+
headIndex: started.headIndex,
|
|
903
|
+
records: 1,
|
|
904
|
+
cursor: null,
|
|
905
|
+
pending: null,
|
|
906
|
+
closedCleanly: false
|
|
907
|
+
};
|
|
908
|
+
await writeCycleState(stagingDir, state);
|
|
909
|
+
return { state, report: started.report };
|
|
910
|
+
}
|
|
910
911
|
|
|
911
912
|
// src/core/backup/cycle.ts
|
|
912
913
|
var BackupCycle = class {
|
|
913
|
-
/** @internal */
|
|
914
|
-
constructor(request) {
|
|
915
|
-
this.request = request;
|
|
916
|
-
this.chainName = request.chainName ?? DEFAULT_CHAIN_NAME;
|
|
917
|
-
this.namePrefix = request.namePrefix ?? DEFAULT_BACKUP_NAME_PREFIX;
|
|
918
|
-
this.stagingDir = request.stagingDir ?? defaultStagingDir(request.sourcePath);
|
|
919
|
-
this.logPath = `${request.sourcePath}-wal`;
|
|
920
|
-
this.intervalMs = request.intervalMs ?? DEFAULT_CAPTURE_INTERVAL_MS;
|
|
921
|
-
this.fullCopyIntervalMs = request.fullCopyIntervalMs ?? DEFAULT_FULL_COPY_INTERVAL_MS;
|
|
922
|
-
this.preferredNode = request.preferredNode ?? "replica";
|
|
923
|
-
}
|
|
924
914
|
chainName;
|
|
925
915
|
namePrefix;
|
|
926
916
|
stagingDir;
|
|
@@ -928,15 +918,34 @@ var BackupCycle = class {
|
|
|
928
918
|
intervalMs;
|
|
929
919
|
fullCopyIntervalMs;
|
|
930
920
|
preferredNode;
|
|
931
|
-
timer =
|
|
921
|
+
timer = new BackupCycleTimer();
|
|
932
922
|
state = null;
|
|
933
|
-
|
|
923
|
+
queued = null;
|
|
924
|
+
turns = new SerialTurns();
|
|
934
925
|
started = false;
|
|
935
926
|
verified = false;
|
|
936
927
|
busy = false;
|
|
937
928
|
captured = false;
|
|
938
929
|
sendRefused = false;
|
|
939
930
|
stopped = false;
|
|
931
|
+
request;
|
|
932
|
+
turnLog;
|
|
933
|
+
/** @internal */
|
|
934
|
+
constructor(request) {
|
|
935
|
+
this.logPath = logPathFor(request.sourcePath);
|
|
936
|
+
this.turnLog = new BackupTurnLog(request, this.logPath);
|
|
937
|
+
this.request = {
|
|
938
|
+
...request,
|
|
939
|
+
onRun: (report) => this.turnLog.ran(report),
|
|
940
|
+
onProgress: (progress) => this.turnLog.progressed(progress)
|
|
941
|
+
};
|
|
942
|
+
this.chainName = request.chainName ?? DEFAULT_CHAIN_NAME;
|
|
943
|
+
this.namePrefix = request.namePrefix ?? DEFAULT_BACKUP_NAME_PREFIX;
|
|
944
|
+
this.stagingDir = request.stagingDir ?? defaultStagingDir(request.sourcePath);
|
|
945
|
+
this.intervalMs = request.intervalMs ?? DEFAULT_CAPTURE_INTERVAL_MS;
|
|
946
|
+
this.fullCopyIntervalMs = request.fullCopyIntervalMs ?? DEFAULT_FULL_COPY_INTERVAL_MS;
|
|
947
|
+
this.preferredNode = request.preferredNode ?? "replica";
|
|
948
|
+
}
|
|
940
949
|
/**
|
|
941
950
|
* Picks a chain up where the previous run left it, or starts a new one with a
|
|
942
951
|
* full copy, and then repeats on the interval. A chain picked up takes an
|
|
@@ -957,22 +966,33 @@ var BackupCycle = class {
|
|
|
957
966
|
this.report(err);
|
|
958
967
|
}
|
|
959
968
|
});
|
|
960
|
-
this.arm();
|
|
969
|
+
if (!this.stopped) this.timer.arm(this.intervalMs, () => void this.tick());
|
|
961
970
|
}
|
|
962
971
|
/**
|
|
963
|
-
*
|
|
964
|
-
* written since the previous turn, and checkpoints the log.
|
|
972
|
+
* Takes one turn now. A turn sends any capture still waiting, reads the
|
|
973
|
+
* frames written since the previous turn, and then checkpoints the log.
|
|
974
|
+
*
|
|
975
|
+
* One turn waits at a time. A call made while a turn is under way queues one
|
|
976
|
+
* behind it, and every call after that joins the queued turn, which reads the
|
|
977
|
+
* log once it begins and so covers their writes as well. Without that bound,
|
|
978
|
+
* a caller asking repeatedly during a long full copy would build a queue of
|
|
979
|
+
* turns with nothing left for any of them to capture.
|
|
965
980
|
*
|
|
966
|
-
* A turn on a node
|
|
967
|
-
* reports a skip
|
|
981
|
+
* A turn on a node whose group backs up from somewhere else writes nothing
|
|
982
|
+
* and reports a skip.
|
|
968
983
|
*
|
|
969
984
|
* @returns What the turn wrote, or undefined where it wrote nothing.
|
|
970
985
|
*/
|
|
971
986
|
runOnce() {
|
|
972
|
-
|
|
987
|
+
const queued = this.queued;
|
|
988
|
+
if (queued) return queued;
|
|
989
|
+
const turn = this.runTurn(async () => {
|
|
990
|
+
this.queued = null;
|
|
973
991
|
if (!await this.takesTheTurn()) return void 0;
|
|
974
992
|
return this.turnOrStartOver();
|
|
975
993
|
});
|
|
994
|
+
this.queued = turn;
|
|
995
|
+
return turn;
|
|
976
996
|
}
|
|
977
997
|
/**
|
|
978
998
|
* Stops the cycle. It captures the log one final time first, so the writes
|
|
@@ -982,10 +1002,7 @@ var BackupCycle = class {
|
|
|
982
1002
|
async stop() {
|
|
983
1003
|
if (this.stopped) return;
|
|
984
1004
|
this.stopped = true;
|
|
985
|
-
|
|
986
|
-
clearInterval(this.timer);
|
|
987
|
-
this.timer = null;
|
|
988
|
-
}
|
|
1005
|
+
this.timer.disarm();
|
|
989
1006
|
await this.runTurn(async () => {
|
|
990
1007
|
const state = this.state;
|
|
991
1008
|
if (!state) return;
|
|
@@ -1006,16 +1023,51 @@ var BackupCycle = class {
|
|
|
1006
1023
|
chains() {
|
|
1007
1024
|
return readBackupChains(this.request.destination, this.chainName);
|
|
1008
1025
|
}
|
|
1026
|
+
/**
|
|
1027
|
+
* Reads what the cycle is doing at this moment, and what its recent turns
|
|
1028
|
+
* produced.
|
|
1029
|
+
*
|
|
1030
|
+
* A caller following a long full copy reads this without having to wait on
|
|
1031
|
+
* the turn, and it answers between turns as well.
|
|
1032
|
+
*
|
|
1033
|
+
* @returns Whether a turn is under way, how far it has got, and the last run, skip, and failure.
|
|
1034
|
+
*/
|
|
1035
|
+
status() {
|
|
1036
|
+
return this.turnLog.read(this.state?.chainId);
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* Reads one of this chain's backups back out of the destination and compares
|
|
1040
|
+
* it against the record the backup that wrote it left behind.
|
|
1041
|
+
*
|
|
1042
|
+
* @param name - Name the backup is stored under.
|
|
1043
|
+
* @returns The pieces read, the bytes they add up to, and the digest where the backup recorded one.
|
|
1044
|
+
*/
|
|
1045
|
+
async verify(name) {
|
|
1046
|
+
return verifyBackupRecord(this.request.destination, await this.chains(), name);
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Runs one turn and passes whatever broke it to the operator. A deeper step
|
|
1050
|
+
* announces some failures itself and then raises them, so this announces a
|
|
1051
|
+
* failure only where no step has announced it already. The operator therefore
|
|
1052
|
+
* hears each failure exactly once. Sirannon empties a log the turn has
|
|
1053
|
+
* outgrown once all of that is done, and the report of writes reaching no
|
|
1054
|
+
* backup replaces whatever failure the turn recorded.
|
|
1055
|
+
*/
|
|
1009
1056
|
runTurn(op) {
|
|
1010
|
-
return this.
|
|
1057
|
+
return this.turns.run(async () => {
|
|
1011
1058
|
this.busy = true;
|
|
1012
1059
|
this.captured = false;
|
|
1013
1060
|
this.sendRefused = false;
|
|
1061
|
+
this.turnLog.turnStarted();
|
|
1014
1062
|
try {
|
|
1015
1063
|
return await op();
|
|
1064
|
+
} catch (err) {
|
|
1065
|
+
if (!this.turnLog.hasAnnounced(err)) this.report(err);
|
|
1066
|
+
throw err;
|
|
1016
1067
|
} finally {
|
|
1017
1068
|
if (!this.captured) await this.releaseLogPastLimit();
|
|
1018
1069
|
this.busy = false;
|
|
1070
|
+
this.turnLog.turnFinished();
|
|
1019
1071
|
}
|
|
1020
1072
|
});
|
|
1021
1073
|
}
|
|
@@ -1062,29 +1114,9 @@ var BackupCycle = class {
|
|
|
1062
1114
|
}
|
|
1063
1115
|
return false;
|
|
1064
1116
|
}
|
|
1065
|
-
/** Passes a skipped turn on,
|
|
1117
|
+
/** Passes a skipped turn on, where the cycle has a reason to give for it. */
|
|
1066
1118
|
async reportSkip(skip) {
|
|
1067
|
-
if (
|
|
1068
|
-
const held = await uncapturedLogBytes(this.logPath);
|
|
1069
|
-
try {
|
|
1070
|
-
this.request.onSkip(held === void 0 ? skip : { ...skip, uncapturedLogBytes: held });
|
|
1071
|
-
} catch {
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
serialise(op) {
|
|
1075
|
-
const run = this.inFlight.then(op, op);
|
|
1076
|
-
this.inFlight = run.then(
|
|
1077
|
-
() => {
|
|
1078
|
-
},
|
|
1079
|
-
() => {
|
|
1080
|
-
}
|
|
1081
|
-
);
|
|
1082
|
-
return run;
|
|
1083
|
-
}
|
|
1084
|
-
arm() {
|
|
1085
|
-
if (this.stopped || this.intervalMs <= 0) return;
|
|
1086
|
-
this.timer = setInterval(() => void this.tick(), this.intervalMs);
|
|
1087
|
-
this.timer.unref?.();
|
|
1119
|
+
if (skip) await this.turnLog.skipped(skip);
|
|
1088
1120
|
}
|
|
1089
1121
|
async tick() {
|
|
1090
1122
|
if (this.stopped) return;
|
|
@@ -1092,35 +1124,18 @@ var BackupCycle = class {
|
|
|
1092
1124
|
await this.reportSkip(previousRunStillActive());
|
|
1093
1125
|
return;
|
|
1094
1126
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
} catch (err) {
|
|
1098
|
-
this.report(err);
|
|
1099
|
-
}
|
|
1127
|
+
await this.runOnce().catch(() => {
|
|
1128
|
+
});
|
|
1100
1129
|
}
|
|
1101
|
-
/**
|
|
1102
|
-
* Checks that the destination still lists the chain the state file names,
|
|
1103
|
-
* including one this cycle started itself, because a record appended under a
|
|
1104
|
-
* chain no listing names is a record no restore reaches. A check that cannot
|
|
1105
|
-
* reach the destination leaves the chain unverified for the next turn to try
|
|
1106
|
-
* again before it appends anything.
|
|
1107
|
-
*/
|
|
1108
1130
|
async verifyChain() {
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
this.
|
|
1114
|
-
this.
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
);
|
|
1118
|
-
if (!lost) {
|
|
1119
|
-
this.verified = true;
|
|
1120
|
-
return;
|
|
1121
|
-
}
|
|
1122
|
-
await this.discardState();
|
|
1123
|
-
this.report(lost);
|
|
1131
|
+
this.verified = await confirmChainStillListed({
|
|
1132
|
+
request: this.request,
|
|
1133
|
+
chainName: this.chainName,
|
|
1134
|
+
state: this.state,
|
|
1135
|
+
verified: this.verified,
|
|
1136
|
+
discardState: () => this.discardState(),
|
|
1137
|
+
report: (err) => this.report(err)
|
|
1138
|
+
});
|
|
1124
1139
|
}
|
|
1125
1140
|
async discardState() {
|
|
1126
1141
|
const pending = this.state?.pending;
|
|
@@ -1131,14 +1146,19 @@ var BackupCycle = class {
|
|
|
1131
1146
|
* Runs one turn. A log that restarted before the capture reached it leaves a
|
|
1132
1147
|
* chain nothing can extend, so this starts a fresh one. The caller still gets
|
|
1133
1148
|
* the error: those writes are in no backup, and an operator has to hear that.
|
|
1149
|
+
* Where the replacement chain also fails to start, Sirannon records that
|
|
1150
|
+
* second failure against the chain the restart broke, because the cycle holds
|
|
1151
|
+
* no chain of its own by then and an operator reading the status has to know
|
|
1152
|
+
* which one they have lost.
|
|
1134
1153
|
*/
|
|
1135
1154
|
async turnOrStartOver() {
|
|
1136
1155
|
try {
|
|
1137
1156
|
return await this.turn();
|
|
1138
1157
|
} catch (err) {
|
|
1139
1158
|
if (startsAFreshChain(err)) {
|
|
1140
|
-
this.
|
|
1141
|
-
|
|
1159
|
+
const broken = this.state?.chainId;
|
|
1160
|
+
this.reportAgainstChain(err, broken);
|
|
1161
|
+
await this.replaceChain().catch((chainErr) => this.reportAgainstChain(chainErr, broken));
|
|
1142
1162
|
}
|
|
1143
1163
|
throw err;
|
|
1144
1164
|
}
|
|
@@ -1156,58 +1176,46 @@ var BackupCycle = class {
|
|
|
1156
1176
|
last = await this.sendWaitingCapture() ?? last;
|
|
1157
1177
|
return last;
|
|
1158
1178
|
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Passes a failure to the operator and records it as the turn's outcome.
|
|
1181
|
+
*
|
|
1182
|
+
* @param err - What the turn failed with.
|
|
1183
|
+
* @param chainId - The chain to record it against, which defaults to the one the cycle holds now.
|
|
1184
|
+
*/
|
|
1159
1185
|
report(err) {
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1186
|
+
this.turnLog.failed(err, this.state?.chainId);
|
|
1187
|
+
}
|
|
1188
|
+
/** Records a failure against a chain the cycle may already have let go of. */
|
|
1189
|
+
reportAgainstChain(err, chainId) {
|
|
1190
|
+
this.turnLog.failed(err, chainId);
|
|
1165
1191
|
}
|
|
1166
1192
|
async replaceChain() {
|
|
1167
1193
|
const previousChainId = this.state?.chainId;
|
|
1168
1194
|
await this.discardState();
|
|
1169
|
-
const
|
|
1195
|
+
const begun = await beginReplacementChain(
|
|
1196
|
+
this.request,
|
|
1197
|
+
this.chainName,
|
|
1198
|
+
this.namePrefix,
|
|
1199
|
+
this.stagingDir,
|
|
1200
|
+
previousChainId
|
|
1201
|
+
);
|
|
1170
1202
|
this.verified = false;
|
|
1171
|
-
this.state =
|
|
1172
|
-
chainName: this.chainName,
|
|
1173
|
-
chainId: started.chainId,
|
|
1174
|
-
chainStartedAt: started.startedAt,
|
|
1175
|
-
headIndex: started.headIndex,
|
|
1176
|
-
records: 1,
|
|
1177
|
-
cursor: null,
|
|
1178
|
-
pending: null,
|
|
1179
|
-
closedCleanly: false
|
|
1180
|
-
};
|
|
1181
|
-
await writeCycleState(this.stagingDir, this.state);
|
|
1203
|
+
this.state = begun.state;
|
|
1182
1204
|
this.captured = true;
|
|
1183
|
-
this.request.onRun?.(
|
|
1184
|
-
return
|
|
1205
|
+
this.request.onRun?.(begun.report);
|
|
1206
|
+
return begun.report;
|
|
1185
1207
|
}
|
|
1186
1208
|
async captureAndCheckpoint() {
|
|
1187
1209
|
const state = this.state;
|
|
1188
1210
|
if (!state) return;
|
|
1189
|
-
await
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
namePrefix: this.namePrefix,
|
|
1196
|
-
sequence: state.records,
|
|
1197
|
-
cursor: state.cursor,
|
|
1198
|
-
expectNewLog: state.closedCleanly
|
|
1199
|
-
});
|
|
1200
|
-
if (captured) {
|
|
1201
|
-
state.pending = captured;
|
|
1202
|
-
await writeCycleState(this.stagingDir, state);
|
|
1203
|
-
}
|
|
1204
|
-
const checkpointed = (await checkpointLog(this.request.acquireWriter())).emptied;
|
|
1205
|
-
const cursor = captured?.cursor ?? state.cursor;
|
|
1206
|
-
if (cursor) cursor.checkpointed = checkpointed;
|
|
1207
|
-
state.closedCleanly = false;
|
|
1208
|
-
await writeCycleState(this.stagingDir, state);
|
|
1209
|
-
this.captured = true;
|
|
1211
|
+
await captureAndCheckpointTurn({
|
|
1212
|
+
request: this.request,
|
|
1213
|
+
state,
|
|
1214
|
+
logPath: this.logPath,
|
|
1215
|
+
stagingDir: this.stagingDir,
|
|
1216
|
+
namePrefix: this.namePrefix
|
|
1210
1217
|
});
|
|
1218
|
+
this.captured = true;
|
|
1211
1219
|
}
|
|
1212
1220
|
/**
|
|
1213
1221
|
* Sends the capture staged against the chain this cycle holds. A turn offers
|
|
@@ -1314,7 +1322,7 @@ function nodeBackupEngine(streaming) {
|
|
|
1314
1322
|
copyToDestination: (conn, request) => manager.copyToDestination(conn, request),
|
|
1315
1323
|
streamsToDestination: () => manager.streamsToDestination(),
|
|
1316
1324
|
createCycle: (request) => createBackupCycle(request),
|
|
1317
|
-
schedule: (conn,
|
|
1325
|
+
schedule: (conn, request) => scheduler.schedule(conn, request)
|
|
1318
1326
|
};
|
|
1319
1327
|
}
|
|
1320
1328
|
|