Phase 0: resumable time-boxed backup steps (spike F) and results

BackupRun is a resumable state machine: each step works until a deadline,
uploads its pack and tree segment, and returns the state; large files are
resumed at the next blob. Snapshot is written last.

Spike F: 200 MiB file resumed across steps in separate processes; with 6
random kill -9 the run completes and restores bit-identically.
spikes/RESULTS.md summarises phase 0 and the findings for the plan.
This commit is contained in:
2026-10-01 18:39:26 +02:00
parent d474050be0
commit 93fec0e8f3
4 changed files with 317 additions and 0 deletions
+33
View File
@@ -180,6 +180,39 @@ final class Repository {
}
}
/** Store one blob of file data in the current run (deduplicated); returns its id. */
public function storeData(string $data, array &$stats): string {
$this->runPacks ??= new PackWriter($this->backend, $this->cipher, $this->index);
return $this->storeBlob($data, $this->runPacks, $stats);
}
/**
* Write a snapshot from a meta record and the tree lines (JSON, one per file). Must be called
* after flushPacks(), so the snapshot only references stored blobs.
*
* @param iterable<string> $lines
*/
public function writeSnapshot(string $snapshotId, array $meta, iterable $lines): void {
$tree = fopen('php://temp/maxmemory:' . (4 * 1048576), 'w+b');
fwrite($tree, json_encode(['snapshot' => $snapshotId] + $meta, JSON_THROW_ON_ERROR) . "\n");
foreach ($lines as $line) {
if ($line !== '') {
fwrite($tree, $line . "\n");
}
}
rewind($tree);
$encrypted = fopen('php://temp/maxmemory:' . (4 * 1048576), 'w+b');
$this->cipher->encrypt($tree, $encrypted, 'snapshot:' . $snapshotId);
fclose($tree);
rewind($encrypted);
$this->backend->put('snapshots/' . $snapshotId, $encrypted);
}
/** @return array<string, array{s:int, m:int, b:list<string>}> */
public function snapshotEntries(string $snapshotId): array {
return $this->loadEntries($snapshotId);
}
/**
* @param list<string> $blobIds
* @return \Generator<string> verified blob contents, in order