Status: Accepted Datum: 2026-05-16 Oblast: Platform architektura / AI runtime / Horizontální škálování Supersedes: — Navazuje na: ADR-023 §5 (namespace-per-tenant-env jako substrát), ADR-015 (K8s namespace provisioning), ADR-019 (Kaniko Job pattern — rozšiřuje na gradle/test), ADR-014 (K8s client foundation — reuse pro Job dispatch)
Context
Problém: škálovací zeď v AgentSidecarExecutor
Driver tohoto ADR je horizontální škálování exekuce pod zátěží, nikoliv dekompozice domény. Doména zůstává v platform BE (Spring Boot, Kotlin, control plane).
Současný stav: třída AgentSidecarExecutor (~1670 řádků, Kotlin @Service singleton v BE)
spawnuje jeden dlouhoběžící Node sidecar přes ProcessBuilder a multiplexuje
všechny konverzace všech tenantů přes jedinou NDJSON stdin/stdout pipe:
- jeden synchronized
stdinWriter - jeden
stdoutreader daemon thread ConcurrentHashMapživých konverzací- životnost sidecaru == životnost BE podu
Sidecar volá zpět BE na http://localhost:$serverPort; HMAC secret se injectuje přes
ProcessBuilder.environment() (talkide-be#104).
Image build je již odsazen na Kaniko K8s Jobs (KanikoBuildService / KanikoJobBuilder,
ADR-019). Ale gradle build/test v dev-loopu volá agenta (Mara) jako tool přímo ve working tree
na NFS v kontextu jednoho BE podu. Při N simultánních uživatelích výsledek:
- OOM a CPU hladovění BE podu
- single-process strop — jeden JVM + jeden Node proces obsluhují všechny tenanty
- gradle JVM warmup × N konverzací souběžně = degradace pro všechny
- lifecycle sidecaru svázaný s BE redeployem → přerušení 3-week session resume
Toto je škálovací zeď, kterou ADR řeší.
Odmítnuté alternativy
| Alternativa | Důvod odmítnutí |
|---|---|
| A) Decouple sidecar přes síť, zachovat oba jazyky | Přepisuje nejbolestivější kus (pipe multiplexer) na distribuovaný síťový protokol → více failure módů, ne méně. Distributed state k udržení → složitost roste, ne klesá. Odmítnuto. |
| B) Kotlin mikroservisa, sidecar pořád child proces | Přesune pipe boundary do tenant ns, získá izolaci + billing, ale složitost nemaže — jen ji stěhuje. Odmítnuto jako fallback-only varianta, nevolí se primárně. |
Decision
1. Node/TypeScript thin worker — jedna instance per tenant-environment namespace
Sidecar přestává být spawnovaný child proces — stává se in-process knihovnou.
Anthropic Agent SDK je již Node/TS; worker jej volá in-process bez meziprocesní pipe. Tím se maže (nikoliv migruje) celá mašinérie:
AgentSidecarExecutorProcessBuilder / NDJSON-pipe / stdout-reader / cancel-přes-pipeBuildAwaiter(talkide-be#105)
Nedávné hardening v té cestě bylo taktické, nikoliv trvalé — záměrně se neprojektuje do budoucna.
Worker běží jako dedikovaný pod v tenant-environment namespace (např. mirek-dev, mirek-prod)
— stejný namespace, který je substrát izolace dle ADR-023 §5 a ADR-015.
2. Thin-seam kontrakt: control-plane vs. worker
Tohle je jádro ADR — jasná dělící čára mezi tím, co zůstává v platform BE a co jde do workeru.
Control-plane (Spring Boot Kotlin, namespace talkide-prod) drží:
| Odpovědnost | Detail |
|---|---|
| Identity / JWT issuance | Auth token pipeline, session validace |
| Tenant / project / billing perzistence | Veškerá data v control-plane DB |
| Quota / budget autorita | Kdo smí kolik spotřebovat — worker se ptá, nestanoví |
| Worker orchestrace | Create / destroy worker podu v tenant ns |
| Gateway policy | Proxuje Anthropic volání — drží raw Anthropic API klíč |
Worker (Node/TS, tenant-environment namespace) dělá:
| Odpovědnost | Detail |
|---|---|
| Mara / Anthropic SDK runtime | Běh agenta in-process, správa transcript na NFS |
| SSE stream do FE | Streaming tokenů a event notifikací přímo do browseru |
| Dispatch build/test K8s Jobů | Vytvoří ephemeral Job v tenant ns pro gradle build/test |
| Usage / activity reporting | Zpětný kanál do control-plane přes thin REST/event API (#104 HMAC seam) |
3. Gateway-proxy: worker nikdy nedrží raw Anthropic klíč
Worker volá platform gateway endpoint (control-plane), která klíč drží a proxuje volání dál.
Důvody:
- Minimalizuje blast-radius kompromitace podu v tenant ns
- Umožňuje rate-limit + billing accounting na jednom místě (gateway)
- Snižuje co je potřeba propagovat do worker podu (jen interní HMAC token, ne Anthropic klíč)
4. Hybrid topologie workloadů
Klíčové rozhodnutí — různé workloady mají různou topologii, protože mají různý charakter:
| Workload | Topologie | Proč |
|---|---|---|
| Agent / konverzace (Mara) | Dlouhoběžící Node worker pod per tenant-env | Stateful (session, transcript na NFS, 3-week resume); I/O-bound (čeká na Anthropic API, ne na CPU); přežívá BE redeploy |
| Gradle build + test | Ephemeral K8s Job v tenant ns, worker je dispatchne | Stateless, bounded runtime, paralelní — cluster scheduler je přirozený concurrency manager; izoluje OOM per job |
| Image build | Kaniko Job (již existuje — ADR-019) | Pattern se rozšiřuje konzistentně; žádná duplicita |
Ephemeral Job pattern pro gradle/test je přímé rozšíření Kaniko Job patternu (ADR-019) na dev-loop.
5. Škálovací páka: ResourceQuota per namespace dle plánu
ResourceQuota a LimitRange per tenant-environment namespace, klíčované plánem uživatele
= plan-based vertikální sizing bez nutnosti sdíleného výpočetního prostoru.
Namespace-per-tenant-env (ADR-023 §5 / ADR-015) je substrát: namespace = jednotka kvóty
- domov worker podu + domov build/test Jobů.
Consequences
Pozitiva
- Maže nejbolestivější pipe boundary — odstraňuje složitost, nepřepisuje ji na jiné místo.
- Konkurenční buildy/testy = nezávislé K8s Joby — cluster scheduler je concurrency manager; žádný single-process strop; OOM jednoho buildu nezabije ostatní konverzace.
- Per-tenant izolace + billing attribution — worker pod žije v tenant ns; resource consumption je přirozeně atribuovatelný bez heuristik.
- Worker lifecycle odpojený od BE redeployů — 3-week session resume je čistší; BE rolling update nepřerušuje živé Mara konverzace.
- Jeden runtime ve worker podu — žádný JVM ~800 MB baseline navíc na tenantu; Node worker je řádově lehčí.
- Gateway-proxy centralizuje klíč — billing metering, rate limiting a secret management na jednom místě.
Rizika — designovat dopředu, nikoliv objevit v produkci
-
Cold-start ephemeral Job per build. Schedule latency + image pull + gradle/JVM daemon warmup → viditelný delay před prvním výstupem buildu. Stejný root cause jako žitá bolest „BE test suite runtime nás zabíjí”.
Mitigace (patří do implementace, ne do budoucích ADR):
- Gradle build cache na NFS (working tree je na NFS already — cache volume se přidá do Job specu)
- Pre-pulled builder image na každém K8s nodu (DaemonSet image prepull nebo
imagePullPolicy: IfNotPresent+ warm cache) - Reused cache volume v Job specu (PVC sdílený přes Jobs v tenant ns)
- Případně warm pool pre-started Jobů (post-alpha optimalizace)
-
K8s RBAC pro worker pod. Worker v tenant ns potřebuje práva na dispatch Jobs (CREATE/GET/WATCH/DELETE) scopovaná na daný namespace. Privileged-ish komponenta — nutno omezit explicitním
Role+RoleBinding(NEClusterRole). Gateway-proxy minimalizuje co worker drží. -
Rewrite-risk. I „thin” worker je re-implementace SSE streaming, cancel, usage-reporting z Kotlinu do TypeScriptu. #104 HMAC seam pomáhá definovat rozhraní, ale parity testy jsou nutné před vypnutím staré cesty. Stará cesta (
AgentSidecarExecutor) zůstává aktivní až do ověřeného cut-over.
Alternatives Considered
Viz sekce Context — alternativy A (síťový sidecar decouple) a B (Kotlin mikroservisa s child procesem) byly formálně zváženy a odmítnuty. Schválený tvar C (Node worker in-process per tenant-env) je jediný, který zároveň maže pipe složitost, přirozeně škáluje přes K8s scheduler a zachovává čistý boundary pro billing a klíč management.
Implementation Notes
Závislosti a pořadí
Tento ADR závisí na namespace-per-tenant-env modelu jako svém substrátu. Implementační pořadí:
- ADR-023 DB refactor (
infra#18→talkide-be#109/talkide-be#110) - Namespace-per-tenant-env model (ADR-015 plně aktivní v produkci)
- Tento ADR — worker extraction
- Test kolečko Stopy B (end-to-end preview + publish flow)
Tento ADR vědomě rozšiřuje definition-of-done Stopy B — user toto rozhodnutí explicitně přijal.
Related issues a ADR
| Reference | Vztah |
|---|---|
| ADR-023 §5 | Namespace-per-tenant-env substrát (nutná podmínka) |
| ADR-015 | Namespace provisioning (worker pod se provisonuje do existujícího ns) |
| ADR-019 | Kaniko Job pattern — rozšiřuje se na gradle/test Joby |
| ADR-014 | K8s klient (fabric8) — reuse pro Job dispatch z worker podu |
| talkide-be#104 | HMAC seam — stává se základem reporting kanálu worker → control-plane |
| talkide-be#105 | BuildAwaiter — maže se pod tímto ADR |
Vysokoúrovňové refactor dopady
Dopady jsou popsány jako změny v architektuře, nikoliv jako issue tickety (PM vytvoří issues až sekvence dorazí na implementaci).
| Oblast | Dopad |
|---|---|
| Nová Node worker service | Nová TS codebase + image + CI pipeline + K8s Deployment manifest v tenant ns |
| Smazání pipe mašinérie | AgentSidecarExecutor, stdout reader, NDJSON pipe, BuildAwaiter — smazat po cut-over |
| Thin orchestrační API v control-plane | Create/destroy worker endpoint, dispatch handshake, worker health check |
| Gateway-proxy | Nový endpoint v platform BE proxující Anthropic API volání; drží a neexpozuje Anthropic klíč |
| Gradle build/test jako K8s Joby | Nový GradleJobBuilder analogický KanikoJobBuilder (ADR-019); worker jej volá |
| ResourceQuota per ns dle plánu | ResourceQuota + LimitRange manifesty per tenant-env ns, parametrizované plánem |
| NFS cache strategie | Gradle build cache PVC per tenant ns; Job spec montuje cache volume; strategie invalidace |
| RBAC pro worker pod | Role + RoleBinding v tenant ns scopující Job dispatch práva |
Thanks for the feedback.