Skip to content

Commit 7b83a1f

Browse files
committed
fix: reconcile stale backup runs
1 parent d8de29d commit 7b83a1f

5 files changed

Lines changed: 62 additions & 0 deletions

File tree

‎scripts/orbit-slice1-test-worker.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,16 @@ async function testRoute(request: Request, env: TestEnv): Promise<Response | nul
489489
const bucket = new MemoryR2();
490490
const testKey = btoa(String.fromCharCode(...new Uint8Array(32).fill(7)))
491491
.replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/u, '');
492+
await env.DB.batch([
493+
env.DB.prepare(`
494+
INSERT INTO backup_runs (id, backup_kind, status, started_at)
495+
VALUES ('slice5-stale-backup', 'daily', 'running', ?)
496+
`).bind(now - 31 * 60 * 1000),
497+
env.DB.prepare(`
498+
INSERT INTO backup_runs (id, backup_kind, status, started_at)
499+
VALUES ('slice5-fresh-backup', 'daily', 'running', ?)
500+
`).bind(now - 60 * 1000),
501+
]);
492502
const result = await runR2Backup({
493503
...env,
494504
BACKUPS: bucket,
@@ -503,10 +513,17 @@ async function testRoute(request: Request, env: TestEnv): Promise<Response | nul
503513
SELECT status, object_key, manifest_checksum, error_code
504514
FROM backup_runs WHERE id = ?
505515
`).bind(result.runId).first();
516+
const reconciled = await env.DB.prepare(`
517+
SELECT id, status, error_code, completed_at
518+
FROM backup_runs
519+
WHERE id IN ('slice5-stale-backup', 'slice5-fresh-backup')
520+
ORDER BY id
521+
`).all();
506522
return Response.json({
507523
objectCount: bucket.objects.size,
508524
retention,
509525
run: runs,
526+
reconciled: reconciled.results,
510527
objectKeyIsSafe: !result.objectKey.includes('nyx') && !result.objectKey.includes('samet'),
511528
checksumLength: result.objectChecksum.length,
512529
});

‎scripts/orbit-slice5-tests.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -903,6 +903,12 @@ describe('Orbit V6 Slice 5 dashboard and platform core', { concurrency: false },
903903
manifest_checksum: string;
904904
error_code: string | null;
905905
};
906+
reconciled: Array<{
907+
id: string;
908+
status: string;
909+
error_code: string | null;
910+
completed_at: number | null;
911+
}>;
906912
objectKeyIsSafe: boolean;
907913
checksumLength: number;
908914
};
@@ -914,6 +920,20 @@ describe('Orbit V6 Slice 5 dashboard and platform core', { concurrency: false },
914920
assert.ok(proof.run.object_key.startsWith('orbit-v6/daily/'));
915921
assert.ok(proof.run.manifest_checksum.length >= 40);
916922
assert.ok(proof.checksumLength >= 40);
923+
assert.deepEqual(proof.reconciled, [
924+
{
925+
id: 'slice5-fresh-backup',
926+
status: 'running',
927+
error_code: null,
928+
completed_at: null,
929+
},
930+
{
931+
id: 'slice5-stale-backup',
932+
status: 'failed',
933+
error_code: 'backup_run_stale_timeout',
934+
completed_at: NOW,
935+
},
936+
]);
917937
});
918938

919939
test('backup failures are owner-visible without exposing encryption material', async () => {

‎src/server/backup/r2-backup.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ const RETENTION: Record<Exclude<BackupKind, 'manual'>, number> = {
1818
weekly: 8,
1919
monthly: 6,
2020
};
21+
const STALE_BACKUP_RUN_MS = 30 * 60 * 1000;
22+
const STALE_BACKUP_ERROR_CODE = 'backup_run_stale_timeout';
2123

2224
function requireBackupBindings(env: OrbitBindings): { bucket: R2BucketLike; encryptionKey: string } {
2325
if (env.ORBIT_BACKUP_ENABLED !== 'true') {
@@ -68,6 +70,18 @@ export async function runR2Backup(
6870
actorAccountId: string | null = null,
6971
): Promise<{ runId: string; objectKey: string; manifestChecksum: string; objectChecksum: string }> {
7072
const repository = new D1PlatformRepository(env.DB);
73+
const reconciledRuns = await repository.failStaleBackupRuns({
74+
before: now - STALE_BACKUP_RUN_MS,
75+
errorCode: STALE_BACKUP_ERROR_CODE,
76+
now,
77+
});
78+
if (reconciledRuns > 0) {
79+
console.log(JSON.stringify({
80+
event: 'backup.reconciliation',
81+
status: 'failed_stale_runs',
82+
count: reconciledRuns,
83+
}));
84+
}
7185
const runId = createEntityId();
7286
const startedAt = Date.now();
7387
await repository.startBackupRun({ id: runId, kind, actorAccountId, now });

‎src/server/repositories/d1/d1-platform-repository.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,16 @@ export class D1PlatformRepository implements PlatformRepository {
199199
`).bind(input.errorCode.slice(0, 160), input.now, input.id).run();
200200
}
201201

202+
async failStaleBackupRuns(
203+
input: Parameters<PlatformRepository['failStaleBackupRuns']>[0],
204+
): Promise<number> {
205+
const result = await this.#db.prepare(`
206+
UPDATE backup_runs SET status = 'failed', error_code = ?, completed_at = ?
207+
WHERE status = 'running' AND started_at <= ?
208+
`).bind(input.errorCode.slice(0, 160), input.now, input.before).run();
209+
return result.meta?.changes ?? 0;
210+
}
211+
202212
async listBackupRuns(limit: number): Promise<BackupRunView[]> {
203213
const result = await this.#db.prepare(`
204214
SELECT id, backup_kind, status, object_key, manifest_checksum, schema_version,

‎src/server/repositories/platform-repository.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,5 +50,6 @@ export interface PlatformRepository {
5050
startBackupRun(input: { id: string; kind: BackupRunView['backupKind']; actorAccountId: string | null; now: number }): Promise<void>;
5151
finishBackupRun(input: { id: string; objectKey: string; manifestChecksum: string; schemaVersion: number; counts: Record<string, number>; now: number }): Promise<void>;
5252
failBackupRun(input: { id: string; errorCode: string; now: number }): Promise<void>;
53+
failStaleBackupRuns(input: { before: number; errorCode: string; now: number }): Promise<number>;
5354
listBackupRuns(limit: number): Promise<BackupRunView[]>;
5455
}

0 commit comments

Comments
 (0)