QTZPOC-20
QTZPOC-20: Refactor model import workflow, introduce atomic model ID generation, and clarify MinIO expiry. Fuses bundle download, decryption, MLflow experiment creation, and artifact upload into a single activity to prevent cross-worker issues. Implements an atomic counter for MongoDB model IDs, replacing the max(id) + 1 scan to avoid collisions during concurrent imports. Adds comprehensive documentation for MinIO object expiry mechanisms and refines lifecycle rule application. Improves error handling with clearer responsibility classification and centralizes MLflow repository injection.
This commit is contained in:
@@ -180,8 +180,8 @@ class BundleReader:
|
||||
header = self._gate_2_header(encrypted)
|
||||
zip_path, kdf_seconds = self._gate_3_decrypt(encrypted, password, header, staging)
|
||||
try:
|
||||
self._gate_4_inspect(zip_path)
|
||||
self._gate_5_extract(zip_path, root)
|
||||
entries = self._gate_4_inspect(zip_path)
|
||||
self._gate_5_extract(zip_path, root, entries)
|
||||
metadata, parameters = self._gate_6_structure(root)
|
||||
self._gate_7_content_policy(root)
|
||||
except BaseException:
|
||||
@@ -282,7 +282,7 @@ class BundleReader:
|
||||
|
||||
# ------------------------------------------------------------------ gate 4
|
||||
|
||||
def _gate_4_inspect(self, zip_path: Path) -> None:
|
||||
def _gate_4_inspect(self, zip_path: Path) -> list[zipfile.ZipInfo]:
|
||||
"""Inspect the central directory only. Nothing is written to disk by this gate."""
|
||||
try:
|
||||
with zipfile.ZipFile(zip_path) as archive:
|
||||
@@ -319,6 +319,8 @@ class BundleReader:
|
||||
self._check_entry_name(entry.filename)
|
||||
self._check_entry_mode(entry)
|
||||
|
||||
return entries
|
||||
|
||||
@staticmethod
|
||||
def _check_entry_name(name: str) -> None:
|
||||
"""Reject absolute names and traversal, from the declared name alone."""
|
||||
@@ -346,11 +348,13 @@ class BundleReader:
|
||||
|
||||
# ------------------------------------------------------------------ gate 5
|
||||
|
||||
def _gate_5_extract(self, zip_path: Path, root: Path) -> None:
|
||||
def _gate_5_extract(self, zip_path: Path, root: Path, entries: list[zipfile.ZipInfo]) -> None:
|
||||
"""Extract into a fresh directory, containing every member by resolved path.
|
||||
|
||||
A pre-existing extraction root is a failure rather than something to clear: it means
|
||||
another import, or a previous attempt, owns that path.
|
||||
another import, or a previous attempt, owns that path. `entries` is the central-directory
|
||||
listing gate 4 already read; the archive is still opened here to read member bytes, but its
|
||||
`infolist()` is not read a second time.
|
||||
"""
|
||||
if root.exists():
|
||||
raise BundleGateError('extraction directory already exists', gate=5)
|
||||
@@ -358,7 +362,7 @@ class BundleReader:
|
||||
resolved_root = root.resolve()
|
||||
try:
|
||||
with zipfile.ZipFile(zip_path) as archive:
|
||||
for entry in archive.infolist():
|
||||
for entry in entries:
|
||||
self._extract_member(archive, entry, root, resolved_root)
|
||||
except BundleGateError:
|
||||
shutil.rmtree(root, ignore_errors=True)
|
||||
@@ -461,7 +465,7 @@ class BundleReader:
|
||||
|
||||
def _gate_7_content_policy(self, root: Path) -> None:
|
||||
"""Only the file kinds an MLflow artifact tree calls for may be present."""
|
||||
for path in sorted(root.rglob('*')):
|
||||
for path in root.rglob('*'):
|
||||
if path.is_dir():
|
||||
continue
|
||||
if not path.is_file():
|
||||
|
||||
Reference in New Issue
Block a user