Initial curated ServiceMap source from 7a0a61232d05ba9097432abef6f7f4102a64d0b8
This commit is contained in:
commit
105251b9c8
96 files changed
+23587
No files matched your search
@@ -0,0 +1,167 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .models import HealthStatus, Service
|
||||
from .store import Store
|
||||
|
||||
SEVERITY = {
|
||||
HealthStatus.MAINTENANCE: 0,
|
||||
HealthStatus.UNMONITORED: 0,
|
||||
HealthStatus.UNKNOWN: 0,
|
||||
HealthStatus.HEALTHY: 1,
|
||||
HealthStatus.RECOVERING: 2,
|
||||
HealthStatus.DEGRADED: 3,
|
||||
HealthStatus.DOWN: 4,
|
||||
}
|
||||
|
||||
|
||||
def _direct_status(service: Service, store: Store) -> HealthStatus:
|
||||
return store.service_health(service.id)
|
||||
|
||||
|
||||
def _dependency_status(service: Service, store: Store, visiting: set[str]) -> HealthStatus:
|
||||
if not service.depends_on:
|
||||
return HealthStatus.HEALTHY
|
||||
|
||||
states: list[HealthStatus] = []
|
||||
for dependency_id in service.depends_on:
|
||||
dependency = store.get_service(dependency_id)
|
||||
if dependency is None:
|
||||
states.append(HealthStatus.UNKNOWN)
|
||||
continue
|
||||
states.append(_effective_status(dependency, store, visiting))
|
||||
return max(states, key=lambda status: SEVERITY[status])
|
||||
|
||||
|
||||
def _effective_status(service: Service, store: Store, visiting: set[str]) -> HealthStatus:
|
||||
if service.id in visiting:
|
||||
return HealthStatus.UNKNOWN
|
||||
next_visiting = set(visiting)
|
||||
next_visiting.add(service.id)
|
||||
own = _direct_status(service, store)
|
||||
if own == HealthStatus.MAINTENANCE:
|
||||
return own
|
||||
dependency = _dependency_status(service, store, next_visiting)
|
||||
if own == HealthStatus.DOWN:
|
||||
return own
|
||||
if dependency == HealthStatus.DOWN:
|
||||
return HealthStatus.DEGRADED
|
||||
if dependency in {HealthStatus.DEGRADED, HealthStatus.RECOVERING} and own in {
|
||||
HealthStatus.HEALTHY,
|
||||
HealthStatus.RECOVERING,
|
||||
}:
|
||||
return HealthStatus.DEGRADED
|
||||
return own
|
||||
|
||||
|
||||
def dependency_status(service: Service, store: Store) -> HealthStatus:
|
||||
return _dependency_status(service, store, {service.id})
|
||||
|
||||
|
||||
def effective_status(service: Service, store: Store) -> HealthStatus:
|
||||
return _effective_status(service, store, set())
|
||||
|
||||
|
||||
def status_maps(
|
||||
services: list[Service],
|
||||
*,
|
||||
direct_statuses: dict[str, HealthStatus] | None = None,
|
||||
) -> tuple[dict[str, HealthStatus], dict[str, HealthStatus]]:
|
||||
"""Calculate the complete topology in memory for one bounded API read."""
|
||||
|
||||
by_id = {service.id: service for service in services}
|
||||
def legacy_direct_state(service: Service) -> HealthStatus:
|
||||
runtime_state = (service.runtime_state or "").casefold()
|
||||
runtime_health = (service.runtime_health or "").casefold()
|
||||
if not service.enabled:
|
||||
return HealthStatus.UNKNOWN
|
||||
if service.maintenance_enabled:
|
||||
return HealthStatus.MAINTENANCE
|
||||
if runtime_health == "unhealthy" or runtime_state in {"exited", "dead", "stopped"}:
|
||||
return HealthStatus.DOWN
|
||||
if runtime_state == "restarting":
|
||||
return HealthStatus.DEGRADED
|
||||
if not service.monitoring_enabled:
|
||||
if runtime_health == "healthy":
|
||||
return HealthStatus.HEALTHY
|
||||
return HealthStatus.UNMONITORED if service.monitoring_state.value in {"unmonitored", "observed"} else HealthStatus.UNKNOWN
|
||||
return service.state
|
||||
|
||||
direct = {
|
||||
service.id: (direct_statuses.get(service.id, HealthStatus.UNKNOWN) if direct_statuses else legacy_direct_state(service))
|
||||
for service in services
|
||||
}
|
||||
effective: dict[str, HealthStatus] = {}
|
||||
dependency: dict[str, HealthStatus] = {}
|
||||
|
||||
def visit(service_id: str, visiting: set[str]) -> HealthStatus:
|
||||
if service_id in effective:
|
||||
return effective[service_id]
|
||||
if service_id in visiting:
|
||||
return HealthStatus.UNKNOWN
|
||||
service = by_id.get(service_id)
|
||||
if service is None:
|
||||
return HealthStatus.UNKNOWN
|
||||
next_visiting = {*visiting, service_id}
|
||||
own = direct[service_id]
|
||||
if not service.depends_on:
|
||||
dependency[service_id] = HealthStatus.HEALTHY
|
||||
effective[service_id] = own
|
||||
return own
|
||||
dependencies = [visit(item, next_visiting) for item in service.depends_on]
|
||||
dependency_state = max(dependencies, key=lambda status: SEVERITY[status])
|
||||
dependency[service_id] = dependency_state
|
||||
if own == HealthStatus.MAINTENANCE or own == HealthStatus.DOWN:
|
||||
result = own
|
||||
elif dependency_state == HealthStatus.DOWN or (
|
||||
dependency_state in {HealthStatus.DEGRADED, HealthStatus.RECOVERING}
|
||||
and own in {HealthStatus.HEALTHY, HealthStatus.RECOVERING}
|
||||
):
|
||||
result = HealthStatus.DEGRADED
|
||||
else:
|
||||
result = own
|
||||
effective[service_id] = result
|
||||
return result
|
||||
|
||||
for service in services:
|
||||
visit(service.id, set())
|
||||
return effective, dependency
|
||||
|
||||
|
||||
def validate_dependencies(
|
||||
depends_on: list[str],
|
||||
store: Store,
|
||||
*,
|
||||
service_id: str | None = None,
|
||||
) -> None:
|
||||
if len(depends_on) != len(set(depends_on)):
|
||||
raise ValueError("Dependencies must be unique")
|
||||
if service_id and service_id in depends_on:
|
||||
raise ValueError("A service cannot depend on itself")
|
||||
|
||||
services = store.list_services()
|
||||
known_ids = {service.id for service in services}
|
||||
missing = [dependency_id for dependency_id in depends_on if dependency_id not in known_ids]
|
||||
if missing:
|
||||
raise ValueError(f"Unknown dependency ids: {', '.join(missing)}")
|
||||
|
||||
if service_id is None:
|
||||
return
|
||||
|
||||
graph = {service.id: list(service.depends_on) for service in services}
|
||||
graph[service_id] = list(depends_on)
|
||||
visiting: set[str] = set()
|
||||
visited: set[str] = set()
|
||||
|
||||
def visit(node: str) -> None:
|
||||
if node in visiting:
|
||||
raise ValueError("Dependency graph contains a cycle")
|
||||
if node in visited:
|
||||
return
|
||||
visiting.add(node)
|
||||
for dependency_id in graph.get(node, []):
|
||||
visit(dependency_id)
|
||||
visiting.remove(node)
|
||||
visited.add(node)
|
||||
|
||||
for node in graph:
|
||||
visit(node)
|
||||
Reference in new issue
Block a user