Add bounded source reconciliation counts and clarify receiver dependencies
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a1046e-fdbf-7b93-a267-5f72b9b24526
This commit is contained in:
parent
1044a5f36d
commit
24a5ecd56b
8 changed files with 231 additions and 17 deletions
|
|
@ -4,9 +4,10 @@ from __future__ import annotations
|
|||
|
||||
import json
|
||||
from typing import Any, Callable
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
from .auth import Authenticator, DenyAllAuthenticator, Identity
|
||||
from .errors import ApprovalError, Forbidden
|
||||
from .errors import ApprovalError, Forbidden, Unprocessable
|
||||
from .store import Engine
|
||||
|
||||
FORBIDDEN_DECISION_KEYS = frozenset({"effect", "decision", "allow", "deny"})
|
||||
|
|
@ -105,6 +106,13 @@ class App:
|
|||
return 200, self.engine.storage_status(integrity=True)
|
||||
if path == "/v1/cadence" and method == "GET":
|
||||
self.identity(environ, "approval:observe")
|
||||
query = parse_qs(environ.get("QUERY_STRING", ""), keep_blank_values=True)
|
||||
if query:
|
||||
if set(query) != {"since", "until"} or any(
|
||||
len(values) != 1 or not values[0] for values in query.values()
|
||||
):
|
||||
raise Unprocessable("supply exactly one since and until")
|
||||
return 200, self.engine.reconciliation_counts(query["since"][0], query["until"][0])
|
||||
return 200, self.engine.transition_counts() | {"form": "heartbeat-or-reconciliation"}
|
||||
if path == "/v1/outbox/stats" and method == "GET":
|
||||
self.identity(environ, "approval:observe")
|
||||
|
|
@ -181,6 +189,7 @@ def call(
|
|||
body: dict[str, Any] | None = None,
|
||||
*,
|
||||
authorization: str | None = "Bearer test-token",
|
||||
query_string: str = "",
|
||||
) -> tuple[int, dict[str, Any]]:
|
||||
"""In-process WSGI helper for tests."""
|
||||
raw = json.dumps(body or {}).encode("utf-8") if body is not None else b""
|
||||
|
|
@ -189,7 +198,7 @@ def call(
|
|||
"PATH_INFO": path,
|
||||
"wsgi.input": _Bytes(raw),
|
||||
"CONTENT_LENGTH": str(len(raw)) if body is not None else "0",
|
||||
"QUERY_STRING": "",
|
||||
"QUERY_STRING": query_string,
|
||||
}
|
||||
if authorization is not None:
|
||||
environ["HTTP_AUTHORIZATION"] = authorization
|
||||
|
|
|
|||
|
|
@ -1055,6 +1055,51 @@ class Engine:
|
|||
counts[row["class"]] = row["n"]
|
||||
return counts
|
||||
|
||||
def reconciliation_counts(self, since: str, until: str) -> dict[str, Any]:
|
||||
"""One source snapshot; occurrence and local acknowledgment are distinct.
|
||||
|
||||
Audit Core counts receiver acceptance time. Neither local timestamp
|
||||
proves that time, especially after delayed delivery or a duplicate ack.
|
||||
"""
|
||||
bounds = []
|
||||
for value in (since, until):
|
||||
try:
|
||||
bound = parse_iso(value)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise Unprocessable("since/until must be timezone-aware timestamps") from exc
|
||||
if bound.tzinfo is None:
|
||||
raise Unprocessable("since/until must be timezone-aware timestamps")
|
||||
bounds.append(bound.astimezone(timezone.utc))
|
||||
start, end = bounds
|
||||
if start >= end:
|
||||
raise Unprocessable("since must precede until")
|
||||
try:
|
||||
# A single statement holds a consistent snapshot while the worker
|
||||
# acknowledges rows. No payload or identity data is selected.
|
||||
rows = self._conn().execute(
|
||||
"SELECT class, created_at, drained_at FROM outbox"
|
||||
).fetchall()
|
||||
except sqlite3.Error as exc:
|
||||
raise StoreUnavailable(str(exc)) from exc
|
||||
counts = {c: {"committed": 0, "drained": 0, "pending": 0,
|
||||
"acknowledged_in_window": 0} for c in EVENT_CLASSES}
|
||||
for row in rows:
|
||||
entry = counts[row["class"]]
|
||||
if start <= parse_iso(row["created_at"]) < end:
|
||||
entry["committed"] += 1
|
||||
entry["pending" if row["drained_at"] is None else "drained"] += 1
|
||||
if row["drained_at"] is not None and start <= parse_iso(row["drained_at"]) < end:
|
||||
entry["acknowledged_in_window"] += 1
|
||||
return {
|
||||
"source": "approval-engine", "tenant": self.tenant,
|
||||
"since": start.isoformat(), "until": end.isoformat(),
|
||||
"bounds": "since-inclusive-until-exclusive",
|
||||
"window_basis": "created_at", "counts": counts,
|
||||
"means": "Source occurrence counts and local acknowledgments; receiver "
|
||||
"acceptance-time comparison requires drain-lag and boundary "
|
||||
"accounting. Agreement does not prove completeness.",
|
||||
}
|
||||
|
||||
def outbox_stats(self) -> dict[str, Any]:
|
||||
conn = self._conn()
|
||||
now = self.now()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue