Phase6/7 runtime + Gitea smoke gate setup #1

Merged
daarion-admin merged 214 commits from codex/sync-node1-runtime into main 2026-03-05 10:38:18 -08:00
2 changed files with 77 additions and 2 deletions
Showing only changes of commit 3b16739671 - Show all commits

View File

@@ -2,10 +2,13 @@ from __future__ import annotations
import os import os
import time import time
import json
from collections import OrderedDict from collections import OrderedDict
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, Dict, Optional, Protocol from typing import Any, Dict, Optional, Protocol
from .logging import log_event
@dataclass @dataclass
class ReplayEntry: class ReplayEntry:
@@ -71,9 +74,62 @@ class InMemoryIdempotencyStore:
self._values.clear() self._values.clear()
class RedisIdempotencyStore:
def __init__(self, redis_client: Any, ttl_seconds: int = 900, prefix: str = "sofiia:idem:") -> None:
self._redis = redis_client
self._ttl_seconds = max(60, int(ttl_seconds))
self._prefix = str(prefix or "sofiia:idem:")
def _k(self, key: str) -> str:
return f"{self._prefix}{key}"
def get(self, key: str) -> Optional[ReplayEntry]:
raw = self._redis.get(self._k(key))
if raw is None:
return None
if isinstance(raw, bytes):
raw = raw.decode("utf-8", errors="ignore")
try:
payload = json.loads(str(raw))
except Exception:
return None
if not isinstance(payload, dict):
return None
return ReplayEntry(
message_id=str(payload.get("message_id") or ""),
response_body=dict(payload.get("response_body") or {}),
created_at=float(payload.get("created_at") or 0.0),
node_id=str(payload.get("node_id") or ""),
)
def set(self, key: str, entry: ReplayEntry) -> None:
payload = {
"message_id": entry.message_id,
"response_body": entry.response_body,
"created_at": float(entry.created_at),
"node_id": entry.node_id,
}
self._redis.set(self._k(key), json.dumps(payload, ensure_ascii=True), ex=self._ttl_seconds)
# Debug/test helpers
def delete(self, key: str) -> None:
self._redis.delete(self._k(key))
def reset(self) -> None:
keys = self._redis.keys(f"{self._prefix}*")
if keys:
self._redis.delete(*keys)
_STORE: Optional[IdempotencyStore] = None _STORE: Optional[IdempotencyStore] = None
def _make_redis_client(redis_url: str) -> Any:
import redis # type: ignore
return redis.Redis.from_url(redis_url, decode_responses=False)
def get_idempotency_store() -> IdempotencyStore: def get_idempotency_store() -> IdempotencyStore:
global _STORE global _STORE
if _STORE is None: if _STORE is None:
@@ -83,6 +139,24 @@ def get_idempotency_store() -> IdempotencyStore:
os.getenv("CHAT_IDEMPOTENCY_TTL_SEC", "900"), os.getenv("CHAT_IDEMPOTENCY_TTL_SEC", "900"),
) )
) )
backend = os.getenv("SOFIIA_IDEMPOTENCY_BACKEND", "inmemory").strip().lower() or "inmemory"
if backend == "redis":
redis_url = os.getenv("SOFIIA_REDIS_URL", "redis://localhost:6379/0").strip()
prefix = os.getenv("SOFIIA_REDIS_PREFIX", "sofiia:idem:").strip() or "sofiia:idem:"
try:
client = _make_redis_client(redis_url)
_STORE = RedisIdempotencyStore(client, ttl_seconds=ttl, prefix=prefix)
except Exception as exc:
max_size = int(os.getenv("SOFIIA_IDEMPOTENCY_MAX", "5000"))
_STORE = InMemoryIdempotencyStore(ttl_seconds=ttl, max_size=max_size)
log_event(
"idempotency.backend.fallback",
backend="redis",
status="degraded",
error_code="redis_unavailable",
error=str(exc)[:180],
)
else:
max_size = int(os.getenv("SOFIIA_IDEMPOTENCY_MAX", "5000")) max_size = int(os.getenv("SOFIIA_IDEMPOTENCY_MAX", "5000"))
_STORE = InMemoryIdempotencyStore(ttl_seconds=ttl, max_size=max_size) _STORE = InMemoryIdempotencyStore(ttl_seconds=ttl, max_size=max_size)
return _STORE return _STORE

View File

@@ -5,6 +5,7 @@ python-multipart>=0.0.6
pyyaml>=6.0 pyyaml>=6.0
python-dotenv>=1.0.0 python-dotenv>=1.0.0
prometheus-client>=0.20.0 prometheus-client>=0.20.0
redis>=5.0.0
# Projects / Documents / Sessions persistence (Phase 1: SQLite) # Projects / Documents / Sessions persistence (Phase 1: SQLite)
aiosqlite>=0.20.0 aiosqlite>=0.20.0
# Document text extraction (optional — used when available) # Document text extraction (optional — used when available)