Bound /readyz so kubelet probes cannot hang the Service
/readyz walked the hash chain and opened pooled connections with no libpq connect_timeout, so a 2s kubelet probe never saw a response and the pod stayed unready. Informed Decision accept is blocked on that. Probe health() only, under a 1.5s budget, publish last-known tamper_evidence, and fail TCP handshake in 1s. Integrity stays on /v1/integrity. Assistant: grok Assistant-Session: 01a0a182-bab7-7f11-b32b-d06f3af52082
This commit is contained in:
parent
e7e054d8d6
commit
b0e6792cf0
9 changed files with 276 additions and 18 deletions
|
|
@ -51,6 +51,11 @@ from audit_core.sqlite_backend import SQLiteAuditBackend
|
|||
|
||||
MAX_BODY_BYTES = 256 * 1024
|
||||
|
||||
# Kubelet readiness timeoutSeconds is 2. Stay under that even if a backend
|
||||
# call hangs: a timed-out probe looks the same as down, and a hung worker
|
||||
# cannot answer the next one.
|
||||
DEFAULT_READY_BUDGET_SECONDS = 1.5
|
||||
|
||||
log = logging.getLogger("audit_core.ingestion")
|
||||
|
||||
|
||||
|
|
@ -102,7 +107,8 @@ class IngestionApplication:
|
|||
senders: SenderRegistry | str,
|
||||
require_custody_class: str | None = None,
|
||||
) -> None:
|
||||
policy = backend.retention_policy
|
||||
getter = getattr(backend, "readiness_policy", None)
|
||||
policy = getter() if callable(getter) else backend.retention_policy
|
||||
if require_custody_class and not custody_class_satisfies(
|
||||
policy.custody_class, require_custody_class
|
||||
):
|
||||
|
|
@ -129,6 +135,9 @@ class IngestionApplication:
|
|||
self.backend = backend
|
||||
self.senders = senders
|
||||
self.counters = Counters()
|
||||
self._ready_budget = float(
|
||||
os.environ.get("AUDIT_CORE_READY_BUDGET_SECONDS", DEFAULT_READY_BUDGET_SECONDS)
|
||||
)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
try:
|
||||
|
|
@ -474,20 +483,51 @@ class IngestionApplication:
|
|||
raise ValueError("truncated_body")
|
||||
return raw
|
||||
|
||||
def _declared_policy(self):
|
||||
"""Custody class and recovery fields without a chain walk.
|
||||
|
||||
Postgres derives ``tamper_evidence`` by walking the hash chain.
|
||||
``/readyz`` must not do that: kubelet gives it two seconds, and a
|
||||
hung walk takes the pod out of the Service even when custody is
|
||||
reachable. Prefer ``readiness_policy`` when the backend offers it.
|
||||
"""
|
||||
getter = getattr(self.backend, "readiness_policy", None)
|
||||
if callable(getter):
|
||||
return getter()
|
||||
return self.backend.retention_policy
|
||||
|
||||
def _readiness(self, start_response):
|
||||
try:
|
||||
health = getattr(self.backend, "health", None)
|
||||
if callable(health):
|
||||
health()
|
||||
except BackendUnavailableError as exc:
|
||||
log.error("readiness failed: %s", exc)
|
||||
box: dict[str, Any] = {}
|
||||
done = threading.Event()
|
||||
|
||||
def run() -> None:
|
||||
try:
|
||||
health = getattr(self.backend, "health", None)
|
||||
if callable(health):
|
||||
health()
|
||||
box["policy"] = self._declared_policy()
|
||||
except Exception as exc: # probe must classify, not hang or 500
|
||||
box["error"] = exc
|
||||
finally:
|
||||
done.set()
|
||||
|
||||
worker = threading.Thread(target=run, name="audit-readyz", daemon=True)
|
||||
worker.start()
|
||||
if not done.wait(timeout=self._ready_budget):
|
||||
log.error("readiness exceeded %.3ss budget", self._ready_budget)
|
||||
return self._json(
|
||||
start_response, HTTPStatus.SERVICE_UNAVAILABLE, {"status": "unavailable"}
|
||||
)
|
||||
error = box.get("error")
|
||||
if error is not None:
|
||||
log.error("readiness failed: %s", error)
|
||||
return self._json(
|
||||
start_response, HTTPStatus.SERVICE_UNAVAILABLE, {"status": "unavailable"}
|
||||
)
|
||||
return self._json(
|
||||
start_response,
|
||||
HTTPStatus.OK,
|
||||
self.backend.retention_policy.as_readiness(),
|
||||
box["policy"].as_readiness(),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -716,6 +756,12 @@ def build_backend() -> IdempotentAuditBackend:
|
|||
statement_timeout_ms=int(
|
||||
os.environ.get("AUDIT_CORE_DB_STATEMENT_TIMEOUT_MS", "30000")
|
||||
),
|
||||
connect_timeout_s=int(
|
||||
os.environ.get("AUDIT_CORE_DB_CONNECT_TIMEOUT_S", "1")
|
||||
),
|
||||
health_timeout_s=float(
|
||||
os.environ.get("AUDIT_CORE_DB_HEALTH_TIMEOUT_S", "1")
|
||||
),
|
||||
# Production mounts the runtime role, which cannot CREATE TABLE.
|
||||
# Schema changes run as a Job with the migration lease
|
||||
# (AUDIT-WP-0005-T02). Local and brokered use keep the default.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue