[Bugfix] Make HWR filesystem lifecycle transitions fail-closed (#6692)

Signed-off-by: hsliuustc0106 <liuhongsheng4@huawei.com>
This commit is contained in:
Hongsheng Liu authored and GitHub committed 2026-08-28 09:30:17 +08:00
1 parent 4245fd6955
commit 6ec5ab11bd
3 files changed
+1845 -94

No files matched your search

+47
View File
@@ -257,6 +257,53 @@ Lookup checks the marker both before validation and after constructing the
lease so a weaker overlapping lookup cannot return a lease after a stronger
validation has denied the artifact.
If publishing the external deny marker fails, lookup returns a retryable
storage failure and marks the artifact directory with a persistent invalidation
mode before releasing the shared lock. Cooperating readers check that mode both
while opening and immediately before returning a lease. Lookup then makes a
bounded attempt to take the exclusive artifact lock and quarantine the entry;
the quarantine transition removes the internal mode marker. If a cooperating
builder replaces the marked inode while the locks are exchanged, containment
preserves that new publication instead of quarantining it. If neither the
external marker nor the directory-mode marker can be persisted, cross-process
exclusion cannot be guaranteed on an unavailable filesystem and the domain
must remain failed rather than treating the artifact as an ordinary invalid
cache entry.
New publications carry the internal invalidation mode through the staging
rename. Hardening clears it and is the logical authority transition for a
process crash; the temporary- and artifact-parent syncs then make the rename
durable, with an artifact-inode sync first making the hardened mode durable. A
process that dies before hardening leaves a persistently denied
entry, while one that dies after successful hardening leaves an authoritative
ready entry. Any synchronous failure after rename is marked invalid and
contained in quarantine before `PUBLICATION_FAILED` is returned, so that
nonretryable result cannot later resolve as a ready hit. If the filesystem
refuses both the inode marker and the quarantine move, the store instead returns
a retryable storage failure with `outcome_uncertain` details; as with deny-marker
double failure, cross-process exclusion cannot be guaranteed until storage
recovers. Once the artifact-parent sync succeeds, a later artifact- or
build-lock release error is logged and lookup resolves the committed entry
instead of reporting a contradictory publication failure. The same authority
rule applies when a competing publisher already installed byte-identical
semantic content; staging-cleanup errors are retried without downgrading that
validated ready entry to publication failure.
Before widening a hardened artifact for a lifecycle move, the store durably
sets the same invalidation mode. A process crash before rename therefore leaves
the writable source denied. After rename, the store re-hardens the destination
but retains the marker until both parent-directory syncs make the move durable;
only then does it durably clear the marker. An interrupted move therefore leaves
a marked transition that the next exact-key lifecycle operation re-hardens.
Explicit cleanup uses the locked move and retries exact-key
`.cleanup.*` tombstones left by an interrupted removal; a later build performs
the same tombstone reconciliation before producing replacement content.
An operational failure while inspecting a noncooperative competing publication
is a retryable storage failure. The competing entry remains authoritative and
is not mislabeled as corrupt or quarantined merely because its manifest could
not be read transiently.
`inspect_domain()` and `inspect_artifact()` return typed, read-only snapshots.
Inspection validation does not create a deny marker. Reported lock activity is
point-in-time evidence only; kernel locks are not a persistent owner registry.
File diff suppressed because it is too large. Load diff
+475 -80
View File
@@ -8,13 +8,14 @@ import contextlib
import errno
import hashlib
import json
import logging
import os
import shutil
import stat
import time
import uuid
from collections import defaultdict
from collections.abc import Callable
from collections.abc import Callable, Iterator
from pathlib import Path
from typing import Protocol
@@ -38,6 +39,8 @@ from ..protocols import BuildRequest, CoordinationScope, WeightProducer
from .locks import FileLock, FileLockTimeoutError, lock_is_active
from .writer import FilesystemArtifactWriter
logger = logging.getLogger(__name__)
_DOMAIN_FILE = "domain.json"
_DOMAIN_POLICY_FILE = "domain-policy.json"
_MANIFEST_FILE = "manifest.json"
@@ -45,6 +48,17 @@ _READY_FILE = "READY.json"
_MAX_METADATA_BYTES = 64 * 1024**2
_FILE_HASH_CHUNK_BYTES = 8 * 1024**2
_ARTIFACT_KEY_RE = re.compile(r"[0-9a-f]{64}")
_ARTIFACT_INVALIDATION_MODE = stat.S_ISVTX
_ARTIFACT_STRUCTURE_ERRNOS = frozenset(
{
errno.EACCES,
errno.EISDIR,
errno.ELOOP,
errno.ENOENT,
errno.ENOTDIR,
errno.EPERM,
}
)
_LOCAL_DISK_FILESYSTEM_TYPES = frozenset(
{
"aufs",
@@ -135,6 +149,153 @@ def _fsync_directory(path: Path) -> None:
os.close(fd)
def _storage_io_failure(
exc: OSError,
*,
operation: str,
details: object | None = None,
) -> HostWeightFailure:
if exc.errno in (errno.ENOSPC, errno.EDQUOT):
return _failure(
ResolutionStage.CAPACITY,
FailureCode.ENOSPC,
f"{operation}: {exc}",
retryable=True,
details=details,
)
return _failure(
ResolutionStage.DOMAIN,
FailureCode.DOMAIN_UNAVAILABLE,
f"{operation}: {exc}",
retryable=True,
details=details,
)
def _path_exists_without_following(path: Path) -> bool:
try:
path.lstat()
except FileNotFoundError:
return False
return True
def _os_error_details(exc: OSError) -> dict[str, object]:
return {
"exception_type": type(exc).__name__,
"errno": exc.errno,
"message": str(exc),
}
@contextlib.contextmanager
def _release_lock_after_authoritative_commit(
lock: FileLock,
*,
key: str,
committed: Callable[[], bool],
) -> Iterator[None]:
try:
yield
finally:
try:
lock.close()
except OSError as exc:
if not committed():
raise
logger.warning(
"artifact %s was authoritative before its build-lock release error: %s",
key,
exc,
)
def _harden_open_directory(fd: int, path: Path, mode: int) -> None:
"""Remove write bits from an open directory and make that change durable."""
try:
os.fchmod(fd, mode)
except OSError as fd_error:
try:
os.chmod(path, mode, follow_symlinks=False)
except OSError as path_error:
raise path_error from fd_error
actual_mode = stat.S_IMODE(os.fstat(fd).st_mode)
if actual_mode != mode:
raise OSError(
errno.EIO,
f"artifact directory {path} has mode {actual_mode:#o}, expected {mode:#o}",
)
os.fsync(fd)
def _artifact_has_invalidation_marker(path: Path) -> bool:
return bool(path.stat(follow_symlinks=False).st_mode & _ARTIFACT_INVALIDATION_MODE)
def _mark_open_artifact_invalidated(fd: int, path: Path) -> tuple[int, int]:
"""Persist a fail-closed marker and return the open inode identity."""
marked_mode = stat.S_IMODE(os.fstat(fd).st_mode) | _ARTIFACT_INVALIDATION_MODE
os.fchmod(fd, marked_mode)
marked_stat = os.fstat(fd)
if not marked_stat.st_mode & _ARTIFACT_INVALIDATION_MODE:
raise OSError(errno.EIO, f"artifact directory {path} was not marked invalid")
os.fsync(fd)
return marked_stat.st_dev, marked_stat.st_ino
def _mark_artifact_invalidated(path: Path) -> tuple[int, int]:
"""Persist a fail-closed marker without allocating a new directory entry."""
fd = os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW)
try:
return _mark_open_artifact_invalidated(fd, path)
finally:
os.close(fd)
def _move_artifact_locked(source: Path, destination: Path) -> None:
"""Move an artifact directory while its exclusive artifact lock is held.
Supported local filesystems may reject renaming the mode-0555 directory
used for a READY artifact. Widen the inode only while the lock excludes
readers, then restore an immutable mode through the same open descriptor.
"""
source_stat = source.lstat()
if stat.S_ISDIR(source_stat.st_mode):
fd = os.open(source, os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW)
try:
original_mode = stat.S_IMODE(os.fstat(fd).st_mode)
source_immutable_mode = original_mode & ~0o222
transition_immutable_mode = source_immutable_mode | _ARTIFACT_INVALIDATION_MODE
destination_immutable_mode = source_immutable_mode & ~_ARTIFACT_INVALIDATION_MODE
writable_mode = transition_immutable_mode | stat.S_IWUSR
current_path = source
try:
_harden_open_directory(fd, source, transition_immutable_mode)
os.fchmod(fd, writable_mode)
os.replace(source, destination)
current_path = destination
except BaseException as move_error:
try:
_harden_open_directory(fd, current_path, source_immutable_mode)
except BaseException as hardening_error:
raise hardening_error from move_error
raise
# Keep the invalidation marker durable until both directory
# entries are durable. If either parent fsync fails (or the
# process crashes first), a rename rollback must not expose an
# authoritative artifact with the marker already cleared.
_harden_open_directory(fd, destination, transition_immutable_mode)
_fsync_directory(source.parent)
_fsync_directory(destination.parent)
_harden_open_directory(fd, destination, destination_immutable_mode)
finally:
os.close(fd)
else:
os.replace(source, destination)
_fsync_directory(source.parent)
_fsync_directory(destination.parent)
def _write_atomic_json(path: Path, value: object, *, mode: int = 0o444) -> None:
data = canonical_json(value)
temporary = path.parent / f".{path.name}.{os.getpid()}.{uuid.uuid4().hex}.tmp"
@@ -206,7 +367,7 @@ def _sha256_fd(fd: int, size: int) -> str:
while offset < size:
chunk = os.pread(fd, min(_FILE_HASH_CHUNK_BYTES, size - offset), offset)
if not chunk:
raise OSError(errno.EIO, "artifact file ended while hashing")
raise ManifestValidationError("artifact file ended while hashing")
digest.update(chunk)
offset += len(chunk)
return digest.hexdigest()
@@ -532,10 +693,22 @@ class FilesystemHostWeightStore:
)
entry_path = self._entry_path(identity.key)
if not entry_path.exists():
try:
entry_exists = _path_exists_without_following(entry_path)
deny_exists = entry_exists and _path_exists_without_following(self._deny_path(identity.key))
except OSError as exc:
artifact_lock.close()
return StoreResult(
StoreStatus.FAILED,
failure=_storage_io_failure(
exc,
operation=f"failed to inspect artifact state for {identity.key}",
),
)
if not entry_exists:
artifact_lock.close()
return StoreResult(StoreStatus.MISS)
if self._deny_path(identity.key).exists():
if deny_exists:
artifact_lock.close()
return StoreResult(
StoreStatus.INVALID,
@@ -547,7 +720,14 @@ class FilesystemHostWeightStore:
)
try:
lease = self._open_lease(identity, entry_path, artifact_lock, validation)
if self._deny_path(identity.key).exists():
try:
denied_during_lookup = _path_exists_without_following(
self._deny_path(identity.key)
) or _artifact_has_invalidation_marker(entry_path)
except OSError:
lease.close()
raise
if denied_during_lookup:
lease.close()
return StoreResult(
StoreStatus.INVALID,
@@ -559,26 +739,49 @@ class FilesystemHostWeightStore:
)
return StoreResult(StoreStatus.HIT, lease=lease)
except HostWeightError as exc:
try:
if record_invalid:
with contextlib.suppress(OSError):
self._write_deny(identity.key, exc.failure)
finally:
failure = exc.failure
except OSError as exc:
if exc.errno not in _ARTIFACT_STRUCTURE_ERRNOS:
artifact_lock.close()
return StoreResult(StoreStatus.INVALID, failure=exc.failure)
except (OSError, SafetensorError, ValueError, ManifestValidationError) as exc:
return StoreResult(
StoreStatus.FAILED,
failure=_storage_io_failure(
exc,
operation=f"failed to read or validate artifact {identity.key}",
),
)
failure = _failure(
ResolutionStage.VALIDATION,
FailureCode.MANIFEST_CORRUPT,
f"artifact {identity.key} failed validation: {exc}",
)
try:
if record_invalid:
with contextlib.suppress(OSError):
self._write_deny(identity.key, failure)
finally:
artifact_lock.close()
return StoreResult(StoreStatus.INVALID, failure=failure)
except (SafetensorError, ValueError, ManifestValidationError) as exc:
failure = _failure(
ResolutionStage.VALIDATION,
FailureCode.MANIFEST_CORRUPT,
f"artifact {identity.key} failed validation: {exc}",
)
try:
if record_invalid:
self._write_deny(identity.key, failure)
except OSError as exc:
containment = self._contain_invalid_after_deny_failure(
identity.key,
artifact_lock,
deadline=deadline,
)
return StoreResult(
StoreStatus.FAILED,
failure=_storage_io_failure(
exc,
operation=f"failed to deny invalid artifact {identity.key}",
details={"containment": containment},
),
)
finally:
artifact_lock.close()
return StoreResult(StoreStatus.INVALID, failure=failure)
def _open_lease(
self,
@@ -592,6 +795,14 @@ class FilesystemHostWeightStore:
entry_fd = os.open(entry_path, os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW)
resources.append(_make_fd_closer(entry_fd))
entry_stat = os.fstat(entry_fd)
if entry_stat.st_mode & _ARTIFACT_INVALIDATION_MODE:
raise HostWeightError(
_failure(
ResolutionStage.LOOKUP,
FailureCode.ARTIFACT_DENIED,
f"artifact {identity.key} is marked invalid pending quarantine",
)
)
if self.integrity_policy.require_trusted_permissions and (
entry_stat.st_uid != os.geteuid() or entry_stat.st_mode & 0o222
):
@@ -820,17 +1031,25 @@ class FilesystemHostWeightStore:
failure=_failure(ResolutionStage.DOMAIN, FailureCode.DOMAIN_UNAVAILABLE, str(exc), retryable=True),
)
with build_lock:
ready_outcome_authoritative = False
with _release_lock_after_authoritative_commit(
build_lock,
key=identity.key,
committed=lambda: ready_outcome_authoritative,
):
recheck = self.lookup(identity, validation=validation, deadline=deadline)
if recheck.status is StoreStatus.HIT:
ready_outcome_authoritative = True
return StoreResult(StoreStatus.JOINED, lease=recheck.lease)
if recheck.status in {StoreStatus.TIMEOUT, StoreStatus.FAILED}:
return recheck
try:
with FileLock(self._artifact_lock_path(identity.key), exclusive=True, deadline=deadline):
if self._entry_path(identity.key).exists():
self._repair_quarantine_transitions_locked(identity.key)
if _path_exists_without_following(self._entry_path(identity.key)):
self._quarantine_locked(identity.key, reason="validation_failure")
self._remove_cleanup_tombstones_locked(identity.key)
self._remove_deny_locked(identity.key)
self._remove_stale_temps_locked(identity.key)
except FileLockTimeoutError as exc:
@@ -850,24 +1069,26 @@ class FilesystemHostWeightStore:
ResolutionStage.LIFECYCLE,
FailureCode.QUARANTINE_FAILED,
f"failed to prepare artifact rebuild: {exc}",
retryable=True,
),
)
staging = self.tmp_dir / f"{identity.key}.{os.getpid()}.{uuid.uuid4().hex}"
writer: FilesystemArtifactWriter | None = None
manifest: WeightManifest | None = None
producer_completed = False
try:
staging.mkdir(mode=0o700)
writer = FilesystemArtifactWriter(staging, capacity_check=self._check_capacity)
metadata = producer.produce(writer)
producer_completed = True
manifest, _ = writer.finalize(identity, metadata)
artifact_bytes = self._tree_bytes(staging)
self._check_capacity(artifact_bytes, 0)
# Keep the staging directory writable until the atomic rename.
# overlayfs rejects renaming a non-writable source directory;
# the artifact lock keeps readers out while the published
# directory is hardened below.
os.chmod(staging, 0o755)
# Keep staging writable for the atomic rename. The sticky bit
# is an internal invalidation marker until publication hardens
# the destination.
os.chmod(staging, 0o755 | _ARTIFACT_INVALIDATION_MODE)
_fsync_directory(staging)
except HostWeightError as exc:
if writer is not None:
@@ -881,7 +1102,7 @@ class FilesystemHostWeightStore:
if exc.errno in (errno.ENOSPC, errno.EDQUOT):
code = FailureCode.ENOSPC
stage = ResolutionStage.CAPACITY
elif writer is None:
elif writer is None or producer_completed:
code = FailureCode.DOMAIN_UNAVAILABLE
stage = ResolutionStage.DOMAIN
else:
@@ -893,7 +1114,7 @@ class FilesystemHostWeightStore:
stage,
code,
f"artifact production failed: {exc}",
retryable=stage is ResolutionStage.CAPACITY,
retryable=stage in {ResolutionStage.CAPACITY, ResolutionStage.DOMAIN},
),
)
except Exception as exc:
@@ -910,11 +1131,20 @@ class FilesystemHostWeightStore:
)
assert manifest is not None
publication_error: OSError | None = None
publication_containment: dict[str, object] | None = None
outcome_uncertain_error: OSError | None = None
competing_entry_error: OSError | None = None
try:
with FileLock(self._artifact_lock_path(identity.key), exclusive=True, deadline=None):
entry_path = self._entry_path(identity.key)
if entry_path.exists():
existing = WeightManifest.from_dict(_read_json_file(entry_path / _MANIFEST_FILE))
if _path_exists_without_following(entry_path):
try:
existing_document = _read_json_file(entry_path / _MANIFEST_FILE)
except OSError as exc:
competing_entry_error = exc
raise
existing = WeightManifest.from_dict(existing_document)
same_semantic_content = (
existing.identity.canonical_bytes == manifest.identity.canonical_bytes
and existing.artifact_content_sha256 == manifest.artifact_content_sha256
@@ -924,9 +1154,7 @@ class FilesystemHostWeightStore:
)
if not same_semantic_content:
collision = self.quarantine_dir / f"{identity.key}.identity-collision.{uuid.uuid4().hex}"
os.replace(staging, collision)
_fsync_directory(self.quarantine_dir)
_fsync_directory(self.tmp_dir)
_move_artifact_locked(staging, collision)
return StoreResult(
StoreStatus.FAILED,
failure=_failure(
@@ -935,27 +1163,73 @@ class FilesystemHostWeightStore:
"the same semantic identity produced different artifact content",
),
)
ready_outcome_authoritative = True
self._remove_tree(staging)
self._remove_deny_locked(identity.key)
else:
os.replace(staging, entry_path)
published = False
try:
os.replace(staging, entry_path)
published = True
os.chmod(entry_path, 0o555)
except OSError:
self._quarantine_locked(identity.key, reason="publication_hardening_failure")
_fsync_directory(entry_path)
_fsync_directory(self.tmp_dir)
self._remove_deny_locked(identity.key)
# The destination-parent sync is the filesystem
# publication commit.
_fsync_directory(self.artifacts_dir)
ready_outcome_authoritative = True
except OSError as exc:
publication_error = exc
if published:
try:
publication_containment = self._contain_failed_publication_locked(identity.key)
except OSError as containment_error:
outcome_uncertain_error = containment_error
raise
raise
_fsync_directory(self.artifacts_dir)
_fsync_directory(self.tmp_dir)
self._remove_deny_locked(identity.key)
except (OSError, ValueError, ManifestValidationError) as exc:
if not ready_outcome_authoritative:
self._discard_temp(identity.key, staging)
if competing_entry_error is not None:
return StoreResult(
StoreStatus.FAILED,
failure=_storage_io_failure(
competing_entry_error,
operation=f"failed to inspect competing publication {identity.key}",
details={
"outcome_uncertain": True,
"competing_entry": "preserved",
},
),
)
if outcome_uncertain_error is not None:
assert publication_error is not None
return StoreResult(
StoreStatus.FAILED,
failure=_storage_io_failure(
outcome_uncertain_error,
operation=f"failed to contain outcome-uncertain publication {identity.key}",
details={
"outcome_uncertain": True,
"publication_error": _os_error_details(publication_error),
"containment_error": _os_error_details(outcome_uncertain_error),
},
),
)
return StoreResult(
StoreStatus.FAILED,
failure=_failure(
ResolutionStage.LIFECYCLE,
FailureCode.PUBLICATION_FAILED,
f"atomic artifact publication failed: {exc}",
details={"containment": publication_containment}
if publication_containment is not None
else None,
),
)
logger.warning("artifact %s was authoritative before a post-authority error: %s", identity.key, exc)
self._discard_temp(identity.key, staging)
return StoreResult(
StoreStatus.FAILED,
failure=_failure(
ResolutionStage.LIFECYCLE,
FailureCode.PUBLICATION_FAILED,
f"atomic artifact publication failed: {exc}",
),
)
opened = self.lookup(identity, validation=validation, deadline=deadline)
if opened.status is not StoreStatus.HIT:
@@ -1021,25 +1295,142 @@ class FilesystemHostWeightStore:
def _remove_deny_locked(self, key: str) -> None:
path = self._deny_path(key)
if path.exists():
if _path_exists_without_following(path):
path.unlink()
_fsync_directory(self.deny_dir)
def _quarantine_locked(self, key: str, *, reason: str) -> Path | None:
entry = self._entry_path(key)
if not entry.exists():
if not _path_exists_without_following(entry):
return None
destination = self.quarantine_dir / f"{key}.{reason}.{uuid.uuid4().hex}"
os.replace(entry, destination)
_fsync_directory(self.artifacts_dir)
_fsync_directory(self.quarantine_dir)
_move_artifact_locked(entry, destination)
return destination
def _contain_failed_publication_locked(self, key: str) -> dict[str, object]:
"""Keep an outcome-uncertain publication from becoming a later HIT."""
containment: dict[str, object] = {}
marker_error: OSError | None = None
try:
_mark_artifact_invalidated(self._entry_path(key))
except OSError as exc:
marker_error = exc
containment["invalidation_marker_error"] = _os_error_details(exc)
else:
containment["invalidation_marker"] = "durable"
try:
destination = self._quarantine_locked(key, reason="publication_failure")
except OSError as quarantine_error:
containment["quarantine"] = "failed"
containment["quarantine_error"] = _os_error_details(quarantine_error)
if marker_error is not None:
raise quarantine_error from marker_error
else:
containment["quarantine"] = "already_absent" if destination is None else "completed"
return containment
def _contain_invalid_after_deny_failure(
self,
key: str,
artifact_lock: FileLock,
*,
deadline: float | None,
) -> dict[str, object]:
"""Attempt persistent exclusion when deny-marker publication fails."""
containment: dict[str, object] = {}
entry = self._entry_path(key)
entry_fd: int | None = None
invalidated_inode: tuple[int, int] | None = None
try:
entry_fd = os.open(entry, os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW)
entry_stat = os.fstat(entry_fd)
invalidated_inode = (entry_stat.st_dev, entry_stat.st_ino)
_mark_open_artifact_invalidated(entry_fd, entry)
except OSError as exc:
containment["invalidation_marker_error"] = _os_error_details(exc)
else:
containment["invalidation_marker"] = "durable"
try:
try:
artifact_lock.close()
except OSError as exc:
containment["shared_lock_release_error"] = _os_error_details(exc)
try:
exclusive_lock = FileLock(
self._artifact_lock_path(key),
exclusive=True,
deadline=deadline,
nonblocking=deadline is None,
)
except FileLockTimeoutError as exc:
containment["quarantine"] = "deferred_for_active_lease"
containment["exclusive_lock_error"] = _os_error_details(exc)
return containment
except OSError as exc:
containment["quarantine"] = "deferred_for_lock_error"
containment["exclusive_lock_error"] = _os_error_details(exc)
return containment
try:
with exclusive_lock:
if not _path_exists_without_following(entry):
containment["quarantine"] = "already_absent"
elif invalidated_inode is None:
containment["quarantine"] = "deferred_for_unverified_artifact"
else:
current_stat = entry.lstat()
current_inode = (current_stat.st_dev, current_stat.st_ino)
if current_inode != invalidated_inode:
containment["quarantine"] = "already_replaced"
else:
destination = self._quarantine_locked(key, reason="validation_failure")
containment["quarantine"] = "already_absent" if destination is None else "completed"
except OSError as exc:
containment["quarantine"] = "failed"
containment["quarantine_error"] = _os_error_details(exc)
return containment
finally:
if entry_fd is not None:
try:
os.close(entry_fd)
except OSError as exc:
containment["artifact_fd_close_error"] = _os_error_details(exc)
def _remove_stale_temps_locked(self, key: str) -> None:
for path in self.tmp_dir.glob(f"{key}.*"):
self._remove_tree(path)
_fsync_directory(self.tmp_dir)
def _remove_cleanup_tombstones_locked(self, key: str) -> None:
tombstones = tuple(self.quarantine_dir.glob(f"{key}.cleanup.*"))
for tombstone in tombstones:
self._remove_tree(tombstone)
if tombstones:
_fsync_directory(self.quarantine_dir)
def _repair_quarantine_transitions_locked(self, key: str) -> None:
repaired = False
for path in self.quarantine_dir.glob(f"{key}.*"):
path_stat = path.lstat()
if not stat.S_ISDIR(path_stat.st_mode) or not path_stat.st_mode & _ARTIFACT_INVALIDATION_MODE:
continue
fd = os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC | os.O_NOFOLLOW)
try:
mode = stat.S_IMODE(os.fstat(fd).st_mode)
_harden_open_directory(
fd,
path,
mode & ~0o222 & ~_ARTIFACT_INVALIDATION_MODE,
)
finally:
os.close(fd)
repaired = True
if repaired:
_fsync_directory(self.quarantine_dir)
def _discard_temp(self, key: str, path: Path) -> None:
with contextlib.suppress(OSError, FileLockTimeoutError):
with FileLock(self._artifact_lock_path(key), exclusive=True, deadline=None):
@@ -1050,7 +1441,7 @@ class FilesystemHostWeightStore:
def _remove_tree(path: Path) -> None:
if path.is_symlink() or path.is_file():
path.unlink(missing_ok=True)
elif path.exists():
elif _path_exists_without_following(path):
for directory, subdirectories, _ in os.walk(path, topdown=False, followlinks=False):
for name in subdirectories:
child = Path(directory) / name
@@ -1078,23 +1469,9 @@ class FilesystemHostWeightStore:
def cleanup(self, identity: WeightArtifactIdentity) -> HostWeightFailure | None:
"""Explicitly remove an inactive artifact without waiting for leases."""
try:
build_lock = FileLock(
self._build_lock_path(identity.key),
exclusive=True,
deadline=None,
nonblocking=True,
)
except FileLockTimeoutError:
return _failure(
ResolutionStage.LIFECYCLE,
FailureCode.ACTIVE_BUILD_TIMEOUT,
f"artifact {identity.key} has an active builder",
retryable=True,
)
with build_lock:
try:
artifact_lock = FileLock(
self._artifact_lock_path(identity.key),
build_lock = FileLock(
self._build_lock_path(identity.key),
exclusive=True,
deadline=None,
nonblocking=True,
@@ -1102,19 +1479,37 @@ class FilesystemHostWeightStore:
except FileLockTimeoutError:
return _failure(
ResolutionStage.LIFECYCLE,
FailureCode.ACTIVE_LEASE,
f"artifact {identity.key} has an active lease",
FailureCode.ACTIVE_BUILD_TIMEOUT,
f"artifact {identity.key} has an active builder",
retryable=True,
)
with artifact_lock:
entry = self._entry_path(identity.key)
if entry.exists():
removal = self.quarantine_dir / f"{identity.key}.cleanup.{uuid.uuid4().hex}"
os.replace(entry, removal)
_fsync_directory(self.artifacts_dir)
self._remove_tree(removal)
_fsync_directory(self.quarantine_dir)
self._remove_deny_locked(identity.key)
with build_lock:
try:
artifact_lock = FileLock(
self._artifact_lock_path(identity.key),
exclusive=True,
deadline=None,
nonblocking=True,
)
except FileLockTimeoutError:
return _failure(
ResolutionStage.LIFECYCLE,
FailureCode.ACTIVE_LEASE,
f"artifact {identity.key} has an active lease",
retryable=True,
)
with artifact_lock:
self._repair_quarantine_transitions_locked(identity.key)
self._quarantine_locked(identity.key, reason="cleanup")
self._remove_cleanup_tombstones_locked(identity.key)
self._remove_deny_locked(identity.key)
except OSError as exc:
return _failure(
ResolutionStage.LIFECYCLE,
FailureCode.QUARANTINE_FAILED,
f"failed to clean up artifact {identity.key}: {exc}",
retryable=True,
)
return None
def inspect_domain(self) -> DomainInspection: