M2: cursor-based resumable runs with merge-join against the parent

TreeWalker walks one or more roots in a stable segment-wise order with
exclusions and resumes after a cursor; ParentCursor merge-joins the
previous snapshot in the same order. BackupRun no longer keeps a file
list or the parent's entries in memory or in its state.
This commit is contained in:
2026-10-02 00:46:31 +02:00
parent b42acb41c5
commit 9cefea019d
7 changed files with 344 additions and 105 deletions
+57
View File
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
// SPDX-FileCopyrightText: 2026 Dillard Blom
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\NgBackup\Job;
use OCA\NgBackup\Repository\Repository;
/**
* Reads the previous snapshot's entries in walk order and answers "what did the parent have
* at this path?" for increasing paths (merge-join). Only the current entry is in memory; the
* number of entries consumed is part of the resumable state.
*/
final class ParentCursor {
private ?\Generator $entries = null;
private ?array $current = null;
private int $consumed = 0;
public function __construct(Repository $repo, ?string $snapshotId, int $skip = 0) {
if ($snapshotId === null) {
return;
}
$this->entries = $repo->entries($snapshotId);
$this->current = $this->entries->valid() ? $this->entries->current() : null;
while ($this->consumed < $skip && $this->current !== null) {
$this->advance();
}
}
/** @return array{p:string, s:int, m:int, b:list<string>}|null */
public function find(string $path): ?array {
while ($this->current !== null) {
$c = TreeWalker::compare($this->current['p'], $path);
if ($c === 0) {
return $this->current;
}
if ($c > 0) {
return null;
}
$this->advance();
}
return null;
}
public function position(): int {
return $this->consumed;
}
private function advance(): void {
$this->entries->next();
$this->consumed++;
$this->current = $this->entries->valid() ? $this->entries->current() : null;
}
}