ReMe/deploy/benchmark_docker/candidate.py
xyf2020 48585f7523
feat(docker): Add Docker benchmark sandbox and pooled runner (#608)
* Add Docker benchmark sandbox and pooled runner

* Fix benchmark worker reuse after Docker command timeout

* Simplify benchmark worker requests and preserve artifacts

* Harden benchmark sandbox reuse and isolate query state

* Deduplicate benchmark sandbox cleanup and artifact checks

* Report benchmark cleanup failures and preserve diagnostic artifacts

* Handle optional Docker dependencies in benchmark unit tests

* Keep benchmark Docker dependencies optional in CI

* Fix cancellation of thread-pool background jobs

* Finish background worker cleanup before propagating cancellation

* Revert background worker cancellation changes

* Fix benchmark worker plugin defaults before config validation

* Stop interrupted benchmark containers before collecting diagnostics

* Fix candidate exclusions and benchmark upload serialization

* Fix late Docker creation cleanup and preserve query artifacts

* Stream benchmark workspace archives and extract plan validation

---------

Co-authored-by: jinli.yl <jinli.yl@alibaba-inc.com>
2026-10-19 16:26:38 +08:00

186 lines
6.1 KiB
Python

"""Candidate-code inputs shared by one or more isolated case sandboxes."""
from __future__ import annotations
from dataclasses import dataclass
import fnmatch
import gzip
import hashlib
import io
import os
from pathlib import Path
from pathlib import PurePosixPath
import tarfile
DEFAULT_EXCLUDES = (
".git",
".reme",
".env",
".env.*",
".ssh",
".codex",
".claude",
".venv",
"venv",
"__pycache__",
".pytest_cache",
".mypy_cache",
".ruff_cache",
"datasets",
"benchmark",
"baselines",
"meta-reme",
"output",
"tests",
"cookbook",
"docs",
"github-pages",
"integrations",
"skills",
"reme_studio",
"node_modules",
"memory_workspaces",
"logs",
"artifacts",
"build",
"dist",
"*.egg-info",
"*.pyc",
"*.pyo",
"*.log",
)
def _is_excluded(relative_path: Path, patterns: tuple[str, ...]) -> bool:
"""Return whether any path component or full POSIX path is excluded."""
posix = relative_path.as_posix()
return any(
fnmatch.fnmatch(posix, pattern) or any(fnmatch.fnmatch(part, pattern) for part in relative_path.parts)
for pattern in patterns
)
def _normalized_info(path: Path, arcname: str) -> tarfile.TarInfo:
"""Create deterministic tar metadata for a regular source file."""
stat = path.stat()
info = tarfile.TarInfo(arcname)
info.size = stat.st_size
info.mode = stat.st_mode & 0o777
info.mtime = 0
info.uid = 0
info.gid = 0
info.uname = ""
info.gname = ""
return info
@dataclass(frozen=True, slots=True)
class SourceSnapshot:
"""A deterministic source archive reusable across many case sandboxes."""
archive: bytes
sha256: str
file_count: int
@classmethod
def from_directory(
cls,
source_dir: str | Path,
*,
excludes: tuple[str, ...] = DEFAULT_EXCLUDES,
) -> "SourceSnapshot":
"""Snapshot a candidate directory without runtime or VCS state.
Symbolic links are rejected instead of followed. This prevents a
candidate archive from silently capturing files outside its root.
"""
root = Path(source_dir).resolve(strict=True)
if not root.is_dir():
raise ValueError(f"candidate source is not a directory: {root}")
files: list[tuple[Path, Path]] = []
# Component matches exclude every descendant. Full-path matches may
# exclude only the directory itself, so they must not prune traversal.
directory_patterns = tuple(pattern for pattern in excludes if "/" not in pattern)
for current, directories, filenames in os.walk(root, topdown=True, followlinks=False):
directory = Path(current)
for name in directories[:]:
path = directory / name
if any(fnmatch.fnmatch(name, pattern) for pattern in directory_patterns):
directories.remove(name)
elif path.is_symlink() and not _is_excluded(path.relative_to(root), excludes):
raise ValueError(f"candidate source contains a symbolic link: {path.relative_to(root)}")
for name in filenames:
path = directory / name
relative = path.relative_to(root)
if _is_excluded(relative, excludes):
continue
if path.is_symlink():
raise ValueError(f"candidate source contains a symbolic link: {relative}")
if path.is_file():
files.append((relative, path))
raw = io.BytesIO()
with gzip.GzipFile(fileobj=raw, mode="wb", mtime=0) as compressed:
with tarfile.open(fileobj=compressed, mode="w") as archive:
for relative, path in sorted(files, key=lambda item: item[0].as_posix()):
info = _normalized_info(path, relative.as_posix())
with path.open("rb") as source:
archive.addfile(info, source)
payload = raw.getvalue()
return cls(
archive=payload,
sha256=hashlib.sha256(payload).hexdigest(),
file_count=len(files),
)
@dataclass(frozen=True, slots=True)
class SourceCandidate:
"""A changing source snapshot containing one or more Python projects.
``install_projects`` is a trusted, archive-relative installation plan. A
plugin directory merely being present in the snapshot is insufficient:
installing it is what publishes its ``reme.plugins`` entry point.
"""
snapshot: SourceSnapshot
base_image: str = "reme-benchmark-base:agentscope-2.0.9"
install_projects: tuple[str, ...] = (".",)
def __post_init__(self) -> None:
if not self.install_projects:
raise ValueError("source candidate requires at least one install project")
normalized: list[str] = []
for value in self.install_projects:
if not isinstance(value, str) or not value:
raise TypeError("candidate install projects must be non-empty strings")
path = PurePosixPath(value)
if path.is_absolute() or ".." in path.parts or "\\" in value:
raise ValueError(f"candidate install project must stay inside the archive: {value!r}")
name = path.as_posix()
if name in normalized:
raise ValueError(f"candidate install project is duplicated: {value!r}")
normalized.append(name)
if normalized[0] != ".":
raise ValueError("the candidate root project must be installed before plugin projects")
object.__setattr__(self, "install_projects", tuple(normalized))
@property
def candidate_id(self) -> str:
"""Stable identifier for the exact source snapshot."""
return self.snapshot.sha256
@dataclass(frozen=True, slots=True)
class ImageCandidate:
"""A ReMe candidate already installed in an immutable Docker image."""
image: str
candidate_id: str | None = None
@property
def resolved_candidate_id(self) -> str:
"""Return the caller-supplied digest/ID or the image reference."""
return self.candidate_id or self.image