Simplify python-tools task configuration
Build custom container images / build (map[base_image:php:8-fpm-alpine build_args:PHP_VERSION=8 context:php8-pgsql fingerprint_command:{ apk info -v | LC_ALL=C sort; find /usr/local/lib/php/extensions /usr/local/etc/php/conf.d -type f -exec sha256sum {} + | LC_ALL=C sort; } name:ph… (push) Successful in 42s
Build custom container images / build (map[base_image:postgres:18 build_args:PG_VERSION=18 POSTGIS_VERSION=3 VCHORD_VERSION=0.5.3 context:postgres fingerprint_command:{ dpkg-query -W -f='${binary:Package}=${Version}\n' | LC_ALL=C sort; find /usr/lib/postgresql -type f -exec sha256su… (push) Successful in 53s
Build custom container images / build (map[base_image:python:3 build_args:PYTHON_VERSION=3 context:python-tools fingerprint_command:{ dpkg-query -W -f='${binary:Package}=${Version}\n' | LC_ALL=C sort; pip freeze | LC_ALL=C sort; } name:python-tools oci_labels:org.opencontainers.ima… (push) Successful in 1m18s
Build custom container images / build (map[base_image:python:3.12-slim build_args:PYTHON_VERSION=3.12-slim context:linkki-tiedotus fingerprint_command:{ dpkg-query -W -f='${binary:Package}=${Version}\n' | LC_ALL=C sort; pip freeze | LC_ALL=C sort; } name:linkki-tiedotus oci_labels:… (push) Successful in 26s

This commit is contained in:
ajp_anton
2026-08-24 15:40:31 +00:00
parent 8ec3cde4f2
commit cb80d420c0
4 changed files with 135 additions and 47 deletions
+71 -36
View File
@@ -18,6 +18,10 @@ from .inputs import PendingUpload
from .store import Run, RunStore
ORPHANED_TASK_STATE_FILENAME = ".orphaned-groups.json"
ORPHANED_TASK_STATE_RETENTION_SECONDS = 14 * 86400
class RunManager:
def __init__(self, settings: Settings, catalog: TaskCatalog, store: RunStore) -> None:
self.settings = settings
@@ -112,36 +116,78 @@ class RunManager:
except OSError:
pass
def cleanup_transient_files(self, *, artifact_max_age_seconds: int = 86_400) -> None:
"""Remove uploads from interrupted runs and expired undownloaded artifacts."""
def cleanup_transient_files(
self,
*,
artifact_max_age_seconds: int = 86_400,
orphaned_task_state_retention_seconds: int = ORPHANED_TASK_STATE_RETENTION_SECONDS,
) -> None:
"""Remove transient run data and state for task groups removed long ago."""
runs_root = self.settings.state_root / "runs"
if not runs_root.is_dir():
return
cutoff = time.time() - artifact_max_age_seconds
for path in runs_root.iterdir():
if not path.is_dir():
continue
run_id_text, separator, kind = path.name.partition(".")
if not separator or not run_id_text.isdigit() or kind not in {"uploads", "artifacts"}:
continue
run = self.store.get(int(run_id_text))
if kind == "uploads" and (run is None or run.status not in {"queued", "running"}):
shutil.rmtree(path, ignore_errors=True)
elif kind == "artifacts":
try:
expired = path.stat().st_mtime < cutoff
except FileNotFoundError:
if runs_root.is_dir():
for path in runs_root.iterdir():
if not path.is_dir():
continue
if expired:
run_id_text, separator, kind = path.name.partition(".")
if not separator or not run_id_text.isdigit() or kind not in {"uploads", "artifacts"}:
continue
run = self.store.get(int(run_id_text))
if kind == "uploads" and (run is None or run.status not in {"queued", "running"}):
shutil.rmtree(path, ignore_errors=True)
for path in runs_root.glob("*.execution-input.json"):
run_id_text = path.name.removesuffix(".execution-input.json")
if not run_id_text.isdigit():
elif kind == "artifacts":
try:
expired = path.stat().st_mtime < cutoff
except FileNotFoundError:
continue
if expired:
shutil.rmtree(path, ignore_errors=True)
for path in runs_root.glob("*.execution-input.json"):
run_id_text = path.name.removesuffix(".execution-input.json")
if not run_id_text.isdigit():
continue
run = self.store.get(int(run_id_text))
if run is None or run.status not in {"queued", "running"}:
path.unlink(missing_ok=True)
self._cleanup_orphaned_task_state(orphaned_task_state_retention_seconds)
def _cleanup_orphaned_task_state(self, retention_seconds: int) -> None:
"""Delete state only after a group remains absent for a grace period."""
if retention_seconds < 0 or not self.settings.task_root.is_dir():
return
root = self.settings.state_root / "tasks"
if not root.is_dir():
return
marker_path = root / ORPHANED_TASK_STATE_FILENAME
try:
markers = json.loads(marker_path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
markers = {}
if not isinstance(markers, dict):
markers = {}
active_groups = {group.id for group in self.catalog.groups()}
current_time = time.time()
updated: dict[str, float] = {}
for path in root.iterdir():
if path.is_symlink() or not path.is_dir():
continue
run = self.store.get(int(run_id_text))
if run is None or run.status not in {"queued", "running"}:
path.unlink(missing_ok=True)
if path.name in active_groups:
continue
first_missing_at = markers.get(path.name, current_time)
if not isinstance(first_missing_at, (int, float)):
first_missing_at = current_time
if current_time - float(first_missing_at) >= retention_seconds:
shutil.rmtree(path, ignore_errors=True)
else:
updated[path.name] = float(first_missing_at)
if updated:
temporary = marker_path.with_suffix(marker_path.suffix + ".tmp")
temporary.write_text(json.dumps(updated, sort_keys=True) + "\n", encoding="utf-8")
temporary.replace(marker_path)
else:
marker_path.unlink(missing_ok=True)
def run_background_task(self, task: BackgroundTask) -> None:
"""Run a declared internal task without creating high-volume run history."""
@@ -335,22 +381,11 @@ class RunManager:
"PATH": "/usr/local/bin:/usr/bin:/bin",
"PYTHONUNBUFFERED": "1",
"SERVER_MAINTENANCE_CREDENTIALS": str(self.settings.credentials_root),
"SERVER_MAINTENANCE_STATE_ROOT": str(self.settings.state_root),
"SERVER_MAINTENANCE_STATE": str(task_state),
}
if self.settings.timezone_name:
environment["TZ"] = self.settings.timezone_name
for name in os.environ.get("SERVER_MAINTENANCE_TASK_ENV", "").split(","):
name = name.strip()
if not name:
continue
if not name.isidentifier() or name.startswith("SERVER_MAINTENANCE_"):
raise ValueError(
"SERVER_MAINTENANCE_TASK_ENV entries must be environment variable names "
"outside the SERVER_MAINTENANCE_ namespace"
)
value = os.environ.get(name)
if value is not None:
environment[name] = value
return environment
def _upload_root(self, run_id: int) -> Path: