perf(delivery): bound copy I/O and streamline metadata scans
This commit is contained in:
@@ -408,6 +408,12 @@ failure. Further bundles receive full verification; separate commands start fres
|
||||
Fixed-size regular-file copies can use Linux kernel transfer with bounded chunks.
|
||||
Unsupported transfer falls back at the same offsets. Source size/EOF, file fsync
|
||||
and independent destination byte verification remain required.
|
||||
Verified installer and stopped-node recovery inventories can keep at most four
|
||||
file operations in flight. Each file retains its durability and byte checks;
|
||||
directory finalization and success receipts wait for all dispatched work. On
|
||||
failure, queued work is cancelled and running calls are joined before the error
|
||||
returns. Private partial data is retained for inspection. The original operator
|
||||
CPU, memory, PID and deadline limits still apply.
|
||||
Old configuration and maintenance receipts go into inactive
|
||||
`recovery/ID/history/`. The new boot's Sandbox preparation owns cgroup recreation.
|
||||
Unknown/nonempty targets and differing existing files are refused; partial restores
|
||||
|
||||
@@ -4,12 +4,63 @@ import errno
|
||||
import os
|
||||
import stat
|
||||
import time
|
||||
from typing import BinaryIO
|
||||
from collections.abc import Callable, Sequence
|
||||
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
|
||||
from itertools import islice
|
||||
from typing import BinaryIO, cast
|
||||
|
||||
from .policy import MAX_FILE_BYTES, DeliveryError
|
||||
|
||||
CHUNK = 1024 * 1024
|
||||
UNSUPPORTED = {errno.ENOSYS, errno.EINVAL, errno.EOPNOTSUPP, errno.EXDEV}
|
||||
IN_FLIGHT = 4
|
||||
|
||||
|
||||
def bounded_copies[Item, Result](
|
||||
rows: Sequence[Item],
|
||||
operation: Callable[[Item], Result],
|
||||
*,
|
||||
deadline: float | None = None,
|
||||
) -> list[Result]:
|
||||
"""Join a finite verified inventory, with at most four operations in flight."""
|
||||
|
||||
def remaining() -> float | None:
|
||||
if deadline is None:
|
||||
return None
|
||||
value = deadline - time.monotonic()
|
||||
if value <= 0:
|
||||
raise DeliveryError("copy deadline exceeded; private partial target retained")
|
||||
return value
|
||||
|
||||
remaining()
|
||||
if len(rows) < 2:
|
||||
return [operation(row) for row in rows]
|
||||
results: list[Result | None] = [None] * len(rows)
|
||||
source = iter(enumerate(rows))
|
||||
with ThreadPoolExecutor(max_workers=min(IN_FLIGHT, len(rows))) as executor:
|
||||
pending = {
|
||||
executor.submit(operation, row): index for index, row in islice(source, IN_FLIGHT)
|
||||
}
|
||||
try:
|
||||
while pending:
|
||||
ready, _ = wait(pending, timeout=remaining(), return_when=FIRST_COMPLETED)
|
||||
if not ready:
|
||||
raise DeliveryError("copy deadline exceeded; private partial target retained")
|
||||
# Observe every completed failure before dispatching more work.
|
||||
completed = [(pending.pop(future), future.result()) for future in ready]
|
||||
for index, result in completed:
|
||||
results[index] = result
|
||||
following = next(source, None)
|
||||
if following is not None:
|
||||
index, row = following
|
||||
remaining()
|
||||
pending[executor.submit(operation, row)] = index
|
||||
finally:
|
||||
for future in pending:
|
||||
future.cancel()
|
||||
# The executor joins running calls before this function can return
|
||||
# or propagate an error. No success receipt can outrun file I/O.
|
||||
return cast(list[Result], results)
|
||||
|
||||
|
||||
def _identity(info: os.stat_result) -> tuple[int, ...]:
|
||||
|
||||
@@ -235,7 +235,8 @@ def frozen_copy(
|
||||
target.mkdir(mode=0o700)
|
||||
for name in sorted(directories, key=lambda name: (name.count("/"), name)):
|
||||
(target / name).mkdir(mode=0o700)
|
||||
for record in records:
|
||||
|
||||
def copy(record: dict) -> None:
|
||||
# The complete sealed tree is read back immediately below. Avoid
|
||||
# hashing each destination both here and again in that inventory.
|
||||
_copy_verified(
|
||||
@@ -246,6 +247,8 @@ def frozen_copy(
|
||||
readback=False,
|
||||
expected_bytes=record["bytes"],
|
||||
)
|
||||
|
||||
file_copy.bounded_copies(records, copy)
|
||||
for name in sorted(directories, key=lambda name: name.count("/"), reverse=True):
|
||||
(target / name).chmod(0o555)
|
||||
sync_directory(target / name)
|
||||
|
||||
+47
-30
@@ -45,24 +45,24 @@ _MAX_SCOPED_METADATA = 2 * MAX_METADATA_BYTES
|
||||
|
||||
def _bundle_signature(bundle: Path) -> dict[str, tuple[int, ...]]:
|
||||
"""Observe topology and all metadata that can invalidate a prior byte read."""
|
||||
if not bundle.is_dir() or bundle.is_symlink():
|
||||
raise DeliveryError("bundle directory required")
|
||||
members = set()
|
||||
for path in bundle.iterdir():
|
||||
members.add(path.name)
|
||||
if len(members) > 2:
|
||||
raise DeliveryError("unexpected bundle member")
|
||||
if members != {"manifest.json", "payload"}:
|
||||
flags = os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC
|
||||
descriptor = os.open(bundle, flags)
|
||||
members = {}
|
||||
try:
|
||||
root = os.fstat(descriptor)
|
||||
with os.scandir(descriptor) as entries:
|
||||
for entry in entries:
|
||||
members[entry.name] = entry.stat(follow_symlinks=False)
|
||||
if len(members) > 2:
|
||||
raise DeliveryError("unexpected bundle member")
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
if set(members) != {"manifest.json", "payload"}:
|
||||
raise DeliveryError("unexpected bundle member")
|
||||
result = {}
|
||||
|
||||
def add(path: Path, *, directory: bool) -> None:
|
||||
info = path.lstat()
|
||||
if not (stat.S_ISDIR(info.st_mode) if directory else stat.S_ISREG(info.st_mode)):
|
||||
raise DeliveryError("bundle proof requires ordinary directories and files")
|
||||
if not directory and info.st_nlink != 1:
|
||||
raise DeliveryError("bundle proof requires single-link files")
|
||||
result[path.relative_to(bundle).as_posix()] = (
|
||||
def identity(info: os.stat_result) -> tuple[int, ...]:
|
||||
return (
|
||||
info.st_dev,
|
||||
info.st_ino,
|
||||
info.st_mode,
|
||||
@@ -73,25 +73,42 @@ def _bundle_signature(bundle: Path) -> dict[str, tuple[int, ...]]:
|
||||
info.st_mtime_ns,
|
||||
info.st_ctime_ns,
|
||||
)
|
||||
|
||||
def add(name: str, info: os.stat_result, *, directory: bool) -> None:
|
||||
if not (stat.S_ISDIR(info.st_mode) if directory else stat.S_ISREG(info.st_mode)):
|
||||
raise DeliveryError("bundle proof requires ordinary directories and files")
|
||||
if not directory and info.st_nlink != 1:
|
||||
raise DeliveryError("bundle proof requires single-link files")
|
||||
result[name] = identity(info)
|
||||
if len(result) > MAX_FILES + 3:
|
||||
raise DeliveryError("bundle proof entry count limit")
|
||||
|
||||
def failed(error: OSError) -> None:
|
||||
raise error
|
||||
|
||||
add(bundle, directory=True)
|
||||
add(bundle / "manifest.json", directory=False)
|
||||
add(".", root, directory=True)
|
||||
add("manifest.json", members["manifest.json"], directory=False)
|
||||
payload = bundle / "payload"
|
||||
add(payload, directory=True)
|
||||
for directory, names, files in os.walk(payload, followlinks=False, onerror=failed):
|
||||
for name in names:
|
||||
path = Path(directory) / name
|
||||
relative_name(path.relative_to(payload).as_posix() + "/.__directory__")
|
||||
add(path, directory=True)
|
||||
for name in files:
|
||||
path = Path(directory) / name
|
||||
relative_name(path.relative_to(payload).as_posix())
|
||||
add(path, directory=False)
|
||||
add("payload", members["payload"], directory=True)
|
||||
pending = [(str(payload), "", result["payload"])]
|
||||
while pending:
|
||||
directory, prefix, expected = pending.pop()
|
||||
# Close each iterator before visiting another directory. Wide or deep
|
||||
# verified inventories never require one open descriptor per directory.
|
||||
descriptor = os.open(directory, flags)
|
||||
try:
|
||||
if identity(os.fstat(descriptor)) != expected:
|
||||
raise DeliveryError("verified bundle changed during the operator operation")
|
||||
with os.scandir(descriptor) as entries:
|
||||
for entry in entries:
|
||||
name = prefix + entry.name
|
||||
info = entry.stat(follow_symlinks=False)
|
||||
is_directory = stat.S_ISDIR(info.st_mode)
|
||||
relative_name(name + "/.__directory__" if is_directory else name)
|
||||
add("payload/" + name, info, directory=is_directory)
|
||||
if is_directory:
|
||||
pending.append(
|
||||
(os.path.join(directory, entry.name), name + "/", identity(info))
|
||||
)
|
||||
finally:
|
||||
os.close(descriptor)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
+12
-10
@@ -121,7 +121,7 @@ def target(record: dict, recovery: Path) -> tuple[Path, int, int, int]:
|
||||
return path, 0, 0, 0o700 if "bytes" not in record else 0o600
|
||||
|
||||
|
||||
def member(path: Path, uid: int, gid: int, mode: int, *, directory: bool) -> None:
|
||||
def member(path: Path, uid: int, gid: int, mode: int, *, directory: bool) -> os.stat_result:
|
||||
info = path.lstat()
|
||||
if (
|
||||
(info.st_uid, info.st_gid, stat.S_IMODE(info.st_mode)) != (uid, gid, mode)
|
||||
@@ -129,6 +129,7 @@ def member(path: Path, uid: int, gid: int, mode: int, *, directory: bool) -> Non
|
||||
or (not directory and info.st_nlink != 1)
|
||||
):
|
||||
raise DeliveryError("existing recovery destination differs; no overwrite")
|
||||
return info
|
||||
|
||||
|
||||
def file_witness(info: os.stat_result) -> tuple[int, ...]:
|
||||
@@ -148,8 +149,7 @@ def file_witness(info: os.stat_result) -> tuple[int, ...]:
|
||||
def readback(
|
||||
path: Path, item: dict, uid: int, gid: int, mode: int, deadline: float
|
||||
) -> tuple[int, ...]:
|
||||
member(path, uid, gid, mode, directory=False)
|
||||
before = file_witness(path.lstat())
|
||||
before = file_witness(member(path, uid, gid, mode, directory=False))
|
||||
descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC)
|
||||
with os.fdopen(descriptor, "rb") as stream:
|
||||
if file_witness(os.fstat(stream.fileno())) != before:
|
||||
@@ -169,8 +169,7 @@ def readback(
|
||||
|
||||
|
||||
def unchanged_file(path: Path, witness: tuple[int, ...], uid: int, gid: int, mode: int) -> None:
|
||||
member(path, uid, gid, mode, directory=False)
|
||||
if file_witness(path.lstat()) != witness:
|
||||
if file_witness(member(path, uid, gid, mode, directory=False)) != witness:
|
||||
raise DeliveryError("verified recovery file changed before completion")
|
||||
|
||||
|
||||
@@ -290,12 +289,15 @@ def restore(recovery_id: str, backup_id: str, expected: str) -> dict:
|
||||
free = os.statvfs(Path("/var/lib/kf-onlinejudge"))
|
||||
if free.f_frsize * free.f_bavail < value["payload_bytes"] + 128 * 1024**2:
|
||||
raise DeliveryError("cold recovery destination reserve insufficient")
|
||||
verified = {}
|
||||
for item in value["files"]:
|
||||
|
||||
def copy(item: dict) -> tuple[int, ...]:
|
||||
path, uid, gid, mode = target(item, recovery)
|
||||
verified[item["path"]] = write_file(
|
||||
root / "payload" / item["path"], path, item, uid, gid, mode, deadline
|
||||
)
|
||||
return write_file(root / "payload" / item["path"], path, item, uid, gid, mode, deadline)
|
||||
|
||||
witnesses = file_copy.bounded_copies(value["files"], copy, deadline=deadline)
|
||||
verified = {
|
||||
item["path"]: witness for item, witness in zip(value["files"], witnesses, strict=True)
|
||||
}
|
||||
for item in sorted(
|
||||
value["directories"], key=lambda row: row["path"].count("/"), reverse=True
|
||||
):
|
||||
|
||||
Reference in New Issue
Block a user