Publish curated Unraid-AppOps-Gateway source from a506550ef83ef2d81651e37c98a89f7ee330c730
This commit is contained in:
1 parent
2facb8de34
commit
767e66921e
68 files changed
+15924
-313
No files matched your search
@@ -0,0 +1,181 @@
|
||||
"""Bounded read-only CI statistics; absent measurements remain unknown."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
from datetime import UTC, datetime
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
||||
|
||||
from .deployment_broker import DeploymentBrokerClient
|
||||
|
||||
|
||||
class _StatisticsModel(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", allow_inf_nan=False, strict=True)
|
||||
|
||||
|
||||
class _Budget(_StatisticsModel):
|
||||
cpuLimit: float | int | None = Field(default=None, ge=0, le=20)
|
||||
memoryLimitBytes: int | None = Field(default=None, ge=0)
|
||||
activeJobLimit: int | None = Field(default=None, ge=0, le=6)
|
||||
heavyJobLimit: int | None = Field(default=None, ge=0, le=6)
|
||||
reservedCpu: float | int | None = Field(default=None, ge=0)
|
||||
reservedMemoryBytes: int | None = Field(default=None, ge=0)
|
||||
activeJobs: int | None = Field(default=None, ge=0)
|
||||
heavyJobs: int | None = Field(default=None, ge=0)
|
||||
|
||||
|
||||
class _Host(_StatisticsModel):
|
||||
cpuBusyPercent: float | int | None = Field(default=None, ge=0, le=100)
|
||||
loadRatio: float | int | None = Field(default=None, ge=0)
|
||||
availableMemoryBytes: int | None = Field(default=None, ge=0)
|
||||
cacheFreeBytes: int | None = Field(default=None, ge=0)
|
||||
|
||||
|
||||
class _Profile(_StatisticsModel):
|
||||
id: Literal["checks", "backend", "frontend", "security", "browser", "build"]
|
||||
displayName: str = Field(min_length=1, max_length=80)
|
||||
labels: list[str] = Field(max_length=8)
|
||||
runnerName: str = Field(min_length=1, max_length=80)
|
||||
status: str = Field(min_length=1, max_length=80)
|
||||
queueDepth: int | None = Field(default=None, ge=0)
|
||||
activeJobs: int | None = Field(default=None, ge=0)
|
||||
oldestQueuedSeconds: float | int | None = Field(default=None, ge=0)
|
||||
reason: str | None = Field(default=None, max_length=120)
|
||||
cpuLimit: float | int | None = Field(default=None, ge=0, le=8)
|
||||
memoryLimitBytes: int | None = Field(default=None, ge=0)
|
||||
cpuUsed: float | int | None = Field(default=None, ge=0)
|
||||
memoryUsedBytes: int | None = Field(default=None, ge=0)
|
||||
completedJobs: int = Field(default=0, ge=0)
|
||||
p50WaitSeconds: float | int | None = Field(default=None, ge=0)
|
||||
p95WaitSeconds: float | int | None = Field(default=None, ge=0)
|
||||
p50DurationSeconds: float | int | None = Field(default=None, ge=0)
|
||||
p95DurationSeconds: float | int | None = Field(default=None, ge=0)
|
||||
lastDurationSeconds: float | int | None = Field(default=None, ge=0)
|
||||
|
||||
|
||||
class _Window(_StatisticsModel):
|
||||
startAt: str | None = Field(default=None, max_length=40)
|
||||
endAt: str | None = Field(default=None, max_length=40)
|
||||
completedJobs: int = Field(default=0, ge=0)
|
||||
samples: int = Field(default=0, ge=0)
|
||||
coverage: Literal["bounded", "complete", "unavailable"] = "unavailable"
|
||||
|
||||
|
||||
class _History(_StatisticsModel):
|
||||
at: str = Field(min_length=1, max_length=40)
|
||||
activeJobs: int | None = Field(default=None, ge=0)
|
||||
queuedJobs: int | None = Field(default=None, ge=0)
|
||||
cpuUsed: float | int | None = Field(default=None, ge=0)
|
||||
memoryUsedBytes: int | None = Field(default=None, ge=0)
|
||||
|
||||
|
||||
class RunnerStatistics(_StatisticsModel):
|
||||
schemaVersion: Literal[1]
|
||||
status: Literal["healthy", "degraded", "unavailable", "not_configured"]
|
||||
scope: Literal["ci-fleet"]
|
||||
generatedAt: str = Field(min_length=1, max_length=40)
|
||||
budget: _Budget
|
||||
host: _Host
|
||||
profiles: list[_Profile] = Field(max_length=6)
|
||||
window: _Window
|
||||
history: list[_History] = Field(max_length=288)
|
||||
|
||||
|
||||
def _valid_timestamp(value: str) -> bool:
|
||||
try:
|
||||
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
return False
|
||||
return parsed.tzinfo is not None and 0 < parsed.timestamp() <= time.time() + 15
|
||||
|
||||
|
||||
def unavailable_statistics(*, configured: bool = True) -> dict[str, object]:
|
||||
return RunnerStatistics(
|
||||
schemaVersion=1,
|
||||
status="unavailable" if configured else "not_configured",
|
||||
scope="ci-fleet",
|
||||
generatedAt=datetime.now(UTC).isoformat(),
|
||||
budget=_Budget(),
|
||||
host=_Host(),
|
||||
profiles=[],
|
||||
window=_Window(),
|
||||
history=[],
|
||||
).model_dump(mode="json")
|
||||
|
||||
|
||||
def sanitize_statistics(payload: object) -> dict[str, object]:
|
||||
"""Accept only the public contract, never raw paths, credentials or jobs."""
|
||||
try:
|
||||
snapshot = RunnerStatistics.model_validate(payload)
|
||||
except (ValidationError, TypeError, ValueError):
|
||||
return unavailable_statistics()
|
||||
timestamps = [snapshot.generatedAt, *(sample.at for sample in snapshot.history)]
|
||||
timestamps.extend(value for value in (snapshot.window.startAt, snapshot.window.endAt) if value)
|
||||
if any(not _valid_timestamp(value) for value in timestamps):
|
||||
return unavailable_statistics()
|
||||
if len({profile.id for profile in snapshot.profiles}) != len(snapshot.profiles):
|
||||
return unavailable_statistics()
|
||||
for profile in snapshot.profiles:
|
||||
allowed_labels = {f"itworx-ci-{profile.id}"}
|
||||
if profile.id in {"backend", "frontend"}:
|
||||
allowed_labels.update({"linux-validation", "ubuntu-latest"})
|
||||
elif profile.id == "build":
|
||||
allowed_labels.add("docker-validation")
|
||||
if (
|
||||
profile.displayName != f"ITWorx CI · {profile.id.title()}"
|
||||
or profile.runnerName != f"itworx-ci-{profile.id}"
|
||||
or not profile.labels
|
||||
or len(set(profile.labels)) != len(profile.labels)
|
||||
or not set(profile.labels).issubset(allowed_labels)
|
||||
or re.fullmatch(r"[a-z][a-z0-9_-]{0,79}", profile.status) is None
|
||||
or (profile.reason is not None and re.fullmatch(r"[a-z][a-z0-9_-]{0,119}", profile.reason) is None)
|
||||
):
|
||||
return unavailable_statistics()
|
||||
sample_time = datetime.fromisoformat(snapshot.generatedAt.replace("Z", "+00:00")).timestamp()
|
||||
history_times = [datetime.fromisoformat(sample.at.replace("Z", "+00:00")).timestamp() for sample in snapshot.history]
|
||||
if history_times != sorted(set(history_times)) or any(at > sample_time + 15 for at in history_times):
|
||||
return unavailable_statistics()
|
||||
window = snapshot.window
|
||||
if (window.startAt is None) != (window.endAt is None):
|
||||
return unavailable_statistics()
|
||||
if window.startAt is not None and window.endAt is not None:
|
||||
start = datetime.fromisoformat(window.startAt.replace("Z", "+00:00")).timestamp()
|
||||
end = datetime.fromisoformat(window.endAt.replace("Z", "+00:00")).timestamp()
|
||||
if not start <= end <= sample_time + 15:
|
||||
return unavailable_statistics()
|
||||
elif window.coverage != "unavailable":
|
||||
return unavailable_statistics()
|
||||
if time.time() - sample_time > 90 and snapshot.status == "healthy":
|
||||
snapshot.status = "degraded"
|
||||
return snapshot.model_dump(mode="json")
|
||||
|
||||
|
||||
_lock = threading.Lock()
|
||||
_cached: dict[str, object] | None = None
|
||||
_cached_at = 0.0
|
||||
_CACHE_SECONDS = 30.0
|
||||
|
||||
|
||||
class RunnerStatisticsService:
|
||||
def snapshot(self) -> dict[str, object]:
|
||||
global _cached, _cached_at
|
||||
with _lock:
|
||||
now = time.monotonic()
|
||||
if _cached is not None and now - _cached_at < _CACHE_SECONDS:
|
||||
return copy.deepcopy(_cached)
|
||||
broker = DeploymentBrokerClient()
|
||||
if not broker.is_configured():
|
||||
snapshot = unavailable_statistics(configured=False)
|
||||
else:
|
||||
try:
|
||||
snapshot = sanitize_statistics(broker.runner_fleet_statistics())
|
||||
except Exception:
|
||||
snapshot = unavailable_statistics()
|
||||
_cached = snapshot
|
||||
_cached_at = time.monotonic()
|
||||
return copy.deepcopy(snapshot)
|
||||
Reference in new issue
Block a user