fix(simulation): reserve disk space before artifact import
This commit is contained in:
@@ -55,6 +55,7 @@ PROVIDER_JOB_STATES: Final = {
|
||||
}
|
||||
PROVIDER_POLL_TIMEOUT_SECONDS: Final = 2 * 60 * 60 + 5 * 60
|
||||
PROVIDER_UNAVAILABLE_RETRY_LIMIT: Final = 6
|
||||
IMPORT_DISK_RESERVE_BYTES: Final = 512 * 1024 * 1024
|
||||
|
||||
|
||||
class SimulationProjectError(RuntimeError):
|
||||
@@ -700,6 +701,7 @@ class SimulationProjectService:
|
||||
self._raise_if_cancelled(project_id)
|
||||
artifacts = _artifact_descriptors(result.get("artifacts"))
|
||||
artifacts_root = self.store.artifacts_root(project_id)
|
||||
self._ensure_import_capacity(project_id, artifacts, artifacts_root)
|
||||
for descriptor in artifacts:
|
||||
self._raise_if_cancelled(project_id)
|
||||
_retry_provider_unavailable(
|
||||
@@ -747,6 +749,55 @@ class SimulationProjectService:
|
||||
self._queued_ids.add(project_id)
|
||||
self._condition.notify_all()
|
||||
|
||||
def _ensure_import_capacity(
|
||||
self,
|
||||
project_id: str,
|
||||
artifacts: list[dict[str, Any]],
|
||||
artifacts_root: Path,
|
||||
) -> None:
|
||||
required_bytes = _remaining_import_bytes(artifacts_root, artifacts)
|
||||
available_bytes = shutil.disk_usage(artifacts_root).free
|
||||
if available_bytes >= required_bytes:
|
||||
return
|
||||
project = self.store.get(project_id)
|
||||
source = project.get("source")
|
||||
provider = project.get("provider")
|
||||
if (
|
||||
isinstance(source, dict)
|
||||
and source.get("kind") == "archive"
|
||||
and isinstance(provider, dict)
|
||||
and isinstance(provider.get("job_id"), str)
|
||||
):
|
||||
source_files = source.get("files")
|
||||
if isinstance(source_files, list) and len(source_files) == 1:
|
||||
source_file = source_files[0]
|
||||
if isinstance(source_file, dict):
|
||||
logical_path = source_file.get("logical_path")
|
||||
byte_length = source_file.get("byte_length")
|
||||
if isinstance(logical_path, str) and isinstance(byte_length, int):
|
||||
source_path = _confined_path(
|
||||
self.store.source_root(project_id),
|
||||
logical_path,
|
||||
)
|
||||
try:
|
||||
source_stat = source_path.stat()
|
||||
except OSError:
|
||||
source_stat = None
|
||||
if (
|
||||
source_stat is not None
|
||||
and source_path.is_file()
|
||||
and not source_path.is_symlink()
|
||||
and source_stat.st_size == byte_length
|
||||
):
|
||||
source_path.unlink()
|
||||
available_bytes = shutil.disk_usage(artifacts_root).free
|
||||
if available_bytes < required_bytes:
|
||||
raise SimulationProjectError(
|
||||
"Недостаточно места для импорта Gaussian-мира: "
|
||||
f"нужно {_human_bytes(required_bytes)}, "
|
||||
f"доступно {_human_bytes(available_bytes)}."
|
||||
)
|
||||
|
||||
def delete(self, project_id: str) -> None:
|
||||
project = self.store.get(project_id)
|
||||
with self._condition:
|
||||
@@ -857,6 +908,36 @@ def _artifact_descriptors(value: object) -> list[dict[str, Any]]:
|
||||
return descriptors
|
||||
|
||||
|
||||
def _remaining_import_bytes(
|
||||
artifacts_root: Path,
|
||||
artifacts: list[dict[str, Any]],
|
||||
) -> int:
|
||||
missing_bytes = 0
|
||||
replacement_scratch_bytes = 0
|
||||
for descriptor in artifacts:
|
||||
logical_path = str(descriptor["logical_path"])
|
||||
expected_bytes = int(descriptor["byte_length"])
|
||||
target = _confined_path(artifacts_root, logical_path)
|
||||
try:
|
||||
current = target.stat()
|
||||
except OSError:
|
||||
current = None
|
||||
if (
|
||||
current is not None
|
||||
and target.is_file()
|
||||
and not target.is_symlink()
|
||||
and current.st_size == expected_bytes
|
||||
):
|
||||
replacement_scratch_bytes = max(replacement_scratch_bytes, expected_bytes)
|
||||
else:
|
||||
missing_bytes += expected_bytes
|
||||
return missing_bytes + replacement_scratch_bytes + IMPORT_DISK_RESERVE_BYTES
|
||||
|
||||
|
||||
def _human_bytes(value: int) -> str:
|
||||
return f"{value / (1024**3):.2f} ГиБ"
|
||||
|
||||
|
||||
def _world_manifest(project_id: str, artifacts: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
def url_for(role: str) -> str | None:
|
||||
descriptor = next((item for item in artifacts if item["role"] == role), None)
|
||||
|
||||
Reference in New Issue
Block a user