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
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:
+71
-36
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user