first commit

This commit is contained in:
2026-08-21 08:37:51 -03:00
commit 881a99b0a8
214 changed files with 61407 additions and 0 deletions

View File

@@ -0,0 +1,416 @@
# Arquitetura TIA Regional com Pool xAI Pré-Aquecido
## 1. Introdução
Este documento descreve a evolução do TIA para operar TTS xAI em alta disponibilidade e alta volumetria usando réplicas Kubernetes distribuídas por região. A solução parte da implementação atual do TIA/LiveKit e mantém seu contrato de TTS, suas métricas de underflow/TTFB e sua lógica de proteção contra repetição após áudio parcial.
A mudança principal é mover a manutenção do conjunto de conexões WebSocket xAI para um **pool persistente por Pod TIA**, implementado como sidecar. Cada Pod regional pode manter até `XAI_POOL_SIZE` conexões xAI pré-aquecidas e prontas para uso. O TIA continua enxergando um endpoint compatível com xAI, porém local (`127.0.0.1`).
## 2. Dificuldade do modelo anterior
Na versão anterior, cada instância `OraclexAITTS` mantinha uma única conexão reutilizável. Como o LiveKit executa chamadas em processos de job, essas conexões são naturalmente distribuídas por chamadas/processos e não formam um pool global do Pod.
Isso cria alguns riscos em alta volumetria:
1. **Burst de handshakes WebSocket.** Muitas chamadas podem abrir conexões ao xAI praticamente ao mesmo tempo.
2. **Capacidade não compartilhada.** Uma chamada pode manter uma conexão ociosa enquanto outra precisa abrir uma nova.
3. **Ausência de backpressure no balanceador.** O LB enxerga o TIA como saudável mesmo quando a capacidade de TTS daquele Pod está totalmente ocupada.
4. **Acoplamento entre sessão e conexão xAI.** A quantidade de chamadas pode virar, indiretamente, a quantidade de conexões abertas, mesmo quando apenas uma parte das chamadas está sintetizando naquele instante.
5. **Falha regional afeta novas chamadas.** Sem readiness orientada à saúde/capacidade do xAI, novas sessões podem continuar chegando a uma réplica cuja região está degradada.
O histórico de testes do TTS já mostrou que problemas de estabelecimento de WebSocket e jitter podem se manifestar de forma regional e em função de carga. A arquitetura proposta transforma esses sinais em capacidade operacional do Pod.
## 3. Objetivos
A solução tem os seguintes objetivos:
- manter conexões OCI xAI abertas e pré-aquecidas;
- reutilizar uma conexão entre diferentes sínteses;
- reservar conexão upstream apenas durante uma utterance;
- liberar a conexão imediatamente após `audio.done`;
- impedir bursts de abertura de WebSockets no caminho crítico da chamada;
- permitir TIA ativo/ativo em múltiplas regiões;
- retirar automaticamente uma réplica saturada do balanceamento de novas conexões;
- manter chamadas existentes durante draining/rollout;
- renovar sockets antes do TTL de forma escalonada, evitando reconexão simultânea;
- preservar o protocolo atual do `OraclexAITTS` e minimizar mudanças no código de voz;
- permitir escalabilidade horizontal em Kubernetes.
## 4. Arquitetura proposta
```text
Clientes / Telefonia
|
v
Load Balancer / Service
(novas conexões WebSocket)
|
+--------------------+--------------------+
| |
v v
Deployment ORD Deployment IAD
| |
+--------+--------+ +--------+--------+
| Pod TIA ORD | | Pod TIA IAD |
| | | |
| bridge | | bridge |
| agent/livekit | | agent/livekit |
| | | | | |
| v | | v |
| xAI pool sidecar| | xAI pool sidecar|
| 50 WS warm | | 50 WS warm |
+-------+---------+ +-------+---------+
| |
v v
OCI xAI ORD OCI xAI IAD
```
O mesmo `Service` Kubernetes seleciona Pods ORD e IAD. Cada deployment acrescenta o label `tia-region`, útil para métricas e operação, mas ambos compartilham `app=${APP_NAME}-regional`.
## 5. Componentes
### 5.1 Bridge TIA
Mantém o WebSocket de entrada e o comportamento existente do TIA. O código principal não precisa conhecer o endpoint xAI regional.
### 5.2 LiveKit Agent
Continua instanciando `OraclexAITTS`, porém passa a usar:
```text
XAI_WEBSOCKET_URL=ws://127.0.0.1:18100/xai/v1/tts
```
O adapter continua enviando:
```text
text.clear
text.delta
text.done
```
e recebendo:
```text
audio.clear
audio.delta
audio.done
```
Portanto, a lógica atual de streaming, TTFB, gaps, underflow e prevenção de replay permanece válida.
### 5.3 Sidecar `xai-pool`
Implementado em:
```text
src/app/livekit/adapters/xai_pool_proxy.py
```
Responsabilidades:
- abrir `XAI_POOL_SIZE` WebSockets no startup;
- usar abertura em ondas controladas (`XAI_POOL_PREWARM_CONCURRENCY`);
- manter os sockets vivos;
- recuperar automaticamente slots desconectados;
- renovar sockets antes do TTL;
- aplicar jitter no refresh para não reconectar todos simultaneamente;
- emprestar uma conexão a uma utterance;
- devolver a conexão ao pool depois de `audio.done`;
- expor health, readiness, status, drain e métricas.
### 5.4 OCI xAI regional
Cada deployment recebe seu endpoint próprio por `XAI_POOL_UPSTREAM_URL`.
Exemplo:
```text
ORD -> wss://...us-chicago-1.../xai/v1/tts
IAD -> wss://...us-ashburn-1.../xai/v1/tts
```
As credenciais reais ficam somente no sidecar de pool. O container `agent` usa uma credencial local dummy porque se conecta apenas a `localhost`.
### 5.5 Kubernetes Service / Load Balancer
O Service seleciona todas as réplicas regionais. O WebSocket funciona normalmente através do LB: o balanceador escolhe um Pod durante o HTTP Upgrade e aquela conexão permanece no mesmo backend durante sua vida.
A estratégia de capacidade não tenta migrar uma conexão existente. Ela afeta **novas conexões**.
## 6. Pool compartilhado por Pod
Uma chamada não reserva um socket xAI por toda sua duração.
```text
Call A falando ---------- sem TTS
Call B ouvindo ---------- sem nova síntese
Call C precisa falar ---- acquire WS #17
text.clear
text.delta
text.done
audio.delta...
audio.done
release WS #17
```
Portanto, 200 chamadas podem coexistir em um Pod com pool de 50, desde que não existam mais de 50 sínteses concorrentes naquele instante.
Essa é a principal diferença entre dimensionar por **chamadas simultâneas** e por **utterances TTS simultâneas**.
## 7. Readiness orientada à capacidade
O sidecar expõe:
```text
GET /healthz
GET /readyz
GET /pool/status
GET /metrics
POST /drain
```
`/healthz` responde se o processo está vivo.
`/readyz` responde se o Pod deve aceitar **novas chamadas**.
Exemplo padrão:
```text
XAI_POOL_SIZE=50
XAI_POOL_UNAVAILABLE_FREE=2
XAI_POOL_RECOVER_FREE=5
```
Com o Pod inicialmente pronto:
```text
free > 2 -> ready
free <= 2 -> not ready (HTTP 503)
```
Depois de sair da rotação, só retorna quando:
```text
free >= 5 -> ready novamente
```
Isso cria histerese e evita flapping de readiness em torno do limite.
Para a política estrita sugerida de somente sair quando todas estiverem ocupadas:
```text
XAI_POOL_UNAVAILABLE_FREE=0
XAI_POOL_RECOVER_FREE=5
```
Em produção recomenda-se uma pequena reserva (por exemplo 2 a 5 conexões), pois ela absorve rajadas e reduz a chance de uma sessão recém-chegada não encontrar capacidade.
## 8. Como o LB redireciona tráfego
Em Kubernetes, um Pod é Ready apenas quando todos os containers que possuem readiness probe estão Ready.
O sidecar `xai-pool` possui uma probe em `/readyz`.
Quando a capacidade acaba:
```text
xai-pool /readyz -> 503
|
v
Pod becomes NotReady
|
v
Pod removed from Service Endpoints
|
v
LB stops sending NEW connections
```
As conexões WebSocket já estabelecidas não são redirecionadas e continuam no Pod enquanto o processo continuar disponível.
## 9. Renovação escalonada das conexões
Manter 50 sockets abertos indefinidamente sem renovação é arriscado porque serviços upstream normalmente aplicam TTL e renovação de autorização.
A configuração padrão usa:
```text
XAI_POOL_CONNECTION_TTL_S=540
XAI_POOL_REFRESH_JITTER_S=45
```
Cada conexão recebe um refresh deadline diferente:
```text
WS01 -> ~501s
WS02 -> ~527s
WS03 -> ~509s
...
```
Somente conexões livres são renovadas. Isso evita um evento no qual 50 conexões expiram e fazem handshake simultaneamente.
## 10. Alta disponibilidade regional
Operação normal:
```text
LB
|-- ORD Pod 1 -> ready
|-- ORD Pod 2 -> ready
|-- IAD Pod 1 -> ready
`-- IAD Pod 2 -> ready
```
Se ORD perder saúde/capacidade xAI, os slots começam a falhar e a quantidade de conexões saudáveis/livres cai. Quando a readiness cruza o threshold, os Pods ORD saem dos endpoints e novas chamadas passam a ser atendidas pelos Pods IAD disponíveis.
Não há necessidade de alterar o cliente ou o LiveKit para escolher a região.
## 11. Escala horizontal
A capacidade teórica de pool é:
```text
capacidade regional de sockets = replicas_region * XAI_POOL_SIZE
```
Exemplo:
```text
ORD: 2 pods x 25 sockets = 50
IAD: 2 pods x 25 sockets = 50
TOTAL = 100 sockets prewarmed
```
ou, se a OCI conceder capacidade independente suficiente:
```text
ORD: 2 pods x 50 = 100
IAD: 2 pods x 50 = 100
TOTAL = 200
```
### Restrição crítica
`replicas * XAI_POOL_SIZE` **não pode ultrapassar o limite real concedido pela OCI para o endpoint/tenancy/região**.
Se a OCI disser que o limite 50 é global por endpoint, então duas réplicas de 50 no mesmo endpoint seriam incorretas. Nesse caso use, por exemplo:
```text
2 replicas x 25 = 50 total
```
ou obtenha endpoints/capacidades independentes.
## 12. Escala visual
```text
Carga baixa
=========
LB
|-- ORD-1 [pool 50: 10 leased / 40 free]
`-- IAD-1 [pool 50: 8 leased / 42 free]
Carga aumenta
=============
LB
|-- ORD-1 [47 leased / 3 free] READY
`-- IAD-1 [30 leased /20 free] READY
ORD satura
===========
LB
|-- ORD-1 [48 leased /2 free] NOT READY -> sem novas chamadas
`-- IAD-1 [31 leased /19 free] READY -> recebe novas chamadas
ORD recupera
=============
ORD-1 chega a 45 leased /5 free
/readyz volta a 200
LB volta a considerá-lo para novas conexões
```
## 13. Draining e rollout
O deployment usa:
```yaml
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
```
O sidecar executa no `preStop`:
```text
POST /drain
```
Isso torna `/readyz` imediatamente 503, removendo o Pod da entrada de novas conexões antes de sua finalização.
`terminationGracePeriodSeconds` deve ser compatível com a duração/grace desejada para as chamadas existentes.
## 14. Segurança
As credenciais OCI/xAI reais ficam no Secret indicado por `XAI_SECRET_NAME` e são montadas apenas no sidecar.
O agent conecta a localhost e não precisa conhecer a chave real do xAI.
Para produção recomenda-se evoluir para `OKE_WORKLOAD_IDENTITY` sempre que suportado pela política do ambiente, eliminando API keys estáticas.
## 15. Observabilidade
`GET /pool/status` retorna:
```json
{
"status": "ready",
"region": "ord",
"configured": 50,
"healthy": 50,
"leased": 17,
"free": 33,
"total_acquires": 845,
"total_acquire_timeouts": 0,
"total_proxy_failures": 0
}
```
`GET /metrics` expõe métricas Prometheus simples:
- `tia_xai_pool_connections{state="healthy"}`
- `tia_xai_pool_connections{state="leased"}`
- `tia_xai_pool_connections{state="free"}`
- `tia_xai_pool_acquires_total`
- `tia_xai_pool_acquire_timeouts_total`
- `tia_xai_pool_proxy_failures_total`
Estas métricas devem ser correlacionadas com as métricas já existentes no TIA, especialmente TTFB, gap e underflow.
## 16. O que esta versão resolve
| Problema | Solução |
|---|---|
| handshakes xAI no caminho crítico | pool pré-aquecido |
| burst de WebSockets | prewarm em ondas + refresh com jitter |
| conexão presa a uma chamada | lease somente durante utterance |
| LB envia tráfego a Pod sem capacidade TTS | `/readyz` baseado no pool |
| flapping de health | histerese unavailable/recover |
| indisponibilidade regional | Deployments ORD/IAD no mesmo Service |
| rollout derruba novas sessões | drain + RollingUpdate |
| chave xAI em todos os processos | credencial real somente no sidecar |
| observabilidade de capacidade | `/pool/status` + `/metrics` |
## 17. O que esta versão não resolve sozinha
A solução não cria capacidade de inferência no OCI xAI. Se todos os endpoints regionais terminarem no mesmo pool de inferência saturado, o TIA terá failover e melhor utilização de sockets, mas não multiplicará a capacidade real do modelo.
É necessário confirmar com a OCI:
1. limite de WebSockets por endpoint/região/tenancy;
2. se endpoints dedicados possuem capacidade independente;
3. se API servers/LB e inference workers são dedicados ou compartilhados;
4. quais limites podem ser reservados/negociados para a TIM.

View File

@@ -0,0 +1,32 @@
# Changelog — TIA Regional / xAI Pool
## Implementação adicionada
- sidecar `xai_pool_proxy` compatível com o protocolo WebSocket xAI usado pelo TIA;
- pool pré-aquecido configurável por Pod (`XAI_POOL_SIZE`, default de exemplo 50);
- acquire por utterance após `text.clear`;
- release automático após `audio.done`;
- recuperação de slots desconectados;
- refresh escalonado por TTL + jitter;
- prewarm em ondas para evitar burst de handshake;
- `/healthz`, `/readyz`, `/pool/status`, `/metrics` e `/drain`;
- readiness com histerese;
- Deployments regionais ORD/IAD usando a mesma imagem;
- credencial xAI real isolada no sidecar;
- Service único para balancear Pods regionais;
- RollingUpdate, PDB e HPA;
- scripts de renderização, validação e deployment;
- manuais de arquitetura, implantação e testes.
## Compatibilidade
O modo anterior permanece disponível. O deployment regional é opcional e não remove `XAI_WEBSOCKET_URL` direto para OCI xAI.
## Validações executadas na geração
- `py_compile` / `compileall` do novo sidecar: PASS;
- parsing YAML dos templates Kubernetes: PASS;
- renderização ORD/IAD via `envsubst`: PASS;
- parsing YAML dos manifests renderizados: PASS.
Não foi executado teste real contra OCI xAI, pois depende das credenciais/endpoints do ambiente TIM/OCI. O manual descreve smoke, saturação, failover e stress test a executar em FQA.

View File

@@ -0,0 +1,385 @@
# Deployment — TIA Regional com Pool xAI
## 1. Pré-requisitos
- cluster Kubernetes/OKE funcional;
- `kubectl` configurado;
- `envsubst` (`gettext`) instalado na máquina de deployment;
- imagem TIA construída a partir desta versão;
- ConfigMap `${APP_NAME}-config` já existente;
- Secrets `${APP_NAME}-api-secrets`, `${APP_NAME}-google-sa-secret` e `shared-tls-secret` já existentes conforme deployment atual;
- endpoint e credencial xAI de cada região;
- quota xAI validada para o número total de WebSockets configurado.
## 2. Arquivos adicionados
```text
src/app/livekit/adapters/xai_pool_proxy.py
k8s/regional/
deployment-region.yaml
service.yaml
pdb.yaml
hpa-region.yaml
xai-secret.example.yaml
regions.env.example
scripts/
render-regional-k8s.sh
validate-regional-k8s.sh
deploy-regional-k8s.sh
```
## 3. Construir a imagem
Use o pipeline existente ou Dockerfile atual do TIA. O novo sidecar utiliza a mesma imagem e apenas muda o módulo executado:
```text
python -m app.livekit.adapters.xai_pool_proxy
```
Exemplo local:
```bash
docker build -f k8s/tia/Dockerfile \
-t iad.ocir.io/<namespace>/tia:regional-xai-pool-v1 .
```
Faça push para o registry usado pelo cluster.
## 4. Criar arquivo de ambiente de deployment
```bash
cp k8s/regional/regions.env.example k8s/regional/regions.env
```
Edite no mínimo:
```bash
APP_NAME=tim-ai-atend-agnt-integ-tia
K8S_NAMESPACE=...
IMAGE_REPOSITORY=...
IMAGE_TAG=...
ORD_XAI_UPSTREAM_URL=wss://...us-chicago-1.../xai/v1/tts
ORD_XAI_SECRET_NAME=xai-ord-credentials
IAD_XAI_UPSTREAM_URL=wss://...us-ashburn-1.../xai/v1/tts
IAD_XAI_SECRET_NAME=xai-iad-credentials
```
## 5. Definir tamanho do pool
Para um Pod com 50 sockets:
```bash
XAI_POOL_SIZE=50
```
Readiness recomendada:
```bash
XAI_POOL_UNAVAILABLE_FREE=2
XAI_POOL_RECOVER_FREE=5
```
Se desejar exatamente o comportamento "só sair quando os 50 estiverem ocupados":
```bash
XAI_POOL_UNAVAILABLE_FREE=0
XAI_POOL_RECOVER_FREE=5
```
### Importante
Se houver `N` réplicas apontando para o mesmo endpoint:
```text
sockets máximos = N * XAI_POOL_SIZE
```
Nunca configure isso acima da quota xAI real.
## 6. Criar Secrets regionais
Não versione chaves reais.
Exemplo por linha de comando:
```bash
kubectl -n "$K8S_NAMESPACE" create secret generic xai-ord-credentials \
--from-literal=XAI_API_KEY='<ORD_KEY>' \
--from-literal=OCI_COMPARTMENT_ID='<COMPARTMENT_OCID>'
kubectl -n "$K8S_NAMESPACE" create secret generic xai-iad-credentials \
--from-literal=XAI_API_KEY='<IAD_KEY>' \
--from-literal=OCI_COMPARTMENT_ID='<COMPARTMENT_OCID>'
```
Para `API_KEY`, `OCI_COMPARTMENT_ID` pode ficar vazio se o fluxo upstream não o exigir.
Para produção, prefira OCI Vault/External Secrets/Workload Identity em vez de gravar chaves no repositório.
## 7. Renderizar manifests
```bash
./scripts/render-regional-k8s.sh
```
Saída:
```text
k8s/regional/rendered/
deployment-ord.yaml
deployment-iad.yaml
hpa-ord.yaml
hpa-iad.yaml
service.yaml
pdb.yaml
```
É possível escolher outro arquivo e diretório:
```bash
./scripts/render-regional-k8s.sh ./minha-config.env /tmp/tia-regional
```
## 8. Validar sem implantar
```bash
./scripts/validate-regional-k8s.sh
```
O script usa:
```bash
kubectl apply --dry-run=client
```
Revise também:
```bash
kubectl diff -f k8s/regional/rendered/
```
## 9. Implantar
```bash
./scripts/deploy-regional-k8s.sh
```
Ou manualmente:
```bash
kubectl apply -f k8s/regional/rendered/service.yaml
kubectl apply -f k8s/regional/rendered/pdb.yaml
kubectl apply -f k8s/regional/rendered/deployment-ord.yaml
kubectl apply -f k8s/regional/rendered/deployment-iad.yaml
kubectl apply -f k8s/regional/rendered/hpa-ord.yaml
kubectl apply -f k8s/regional/rendered/hpa-iad.yaml
```
## 10. Verificar rollout
```bash
kubectl -n "$K8S_NAMESPACE" get pods -l app=${APP_NAME}-regional -o wide
```
Todos os containers devem ficar Ready:
```text
bridge 1/1
agent 1/1
xai-pool 1/1
```
Confira os deployments:
```bash
kubectl -n "$K8S_NAMESPACE" rollout status deployment/${APP_NAME}-ord
kubectl -n "$K8S_NAMESPACE" rollout status deployment/${APP_NAME}-iad
```
## 11. Validar o pool
Escolha um Pod:
```bash
POD=$(kubectl -n "$K8S_NAMESPACE" get pod \
-l app=${APP_NAME}-regional,tia-region=ord \
-o jsonpath='{.items[0].metadata.name}')
```
Port-forward:
```bash
kubectl -n "$K8S_NAMESPACE" port-forward "$POD" 18100:18100
```
Em outro terminal:
```bash
curl -s http://127.0.0.1:18100/healthz | jq
curl -s http://127.0.0.1:18100/readyz | jq
curl -s http://127.0.0.1:18100/pool/status | jq
curl -s http://127.0.0.1:18100/metrics
```
Resultado esperado após prewarm:
```json
{
"region": "ord",
"configured": 50,
"healthy": 50,
"leased": 0,
"free": 50,
"status": "ready"
}
```
## 12. Logs
```bash
kubectl -n "$K8S_NAMESPACE" logs "$POD" -c xai-pool -f
```
Eventos relevantes:
```text
XAI_POOL_SLOT_OPENED
XAI_POOL_PREWARM_FAILED
XAI_POOL_SLOT_RECOVERY_FAILED
XAI_POOL_STARTED
```
## 13. Testar saturação/readiness
Objetivo: provar que o Pod sai da rotação para novas sessões quando o pool atinge o limite.
1. Gere sínteses concorrentes suficientes para ocupar o pool.
2. Observe:
```bash
watch -n 1 'curl -s http://127.0.0.1:18100/pool/status | jq'
```
3. Quando `free <= XAI_POOL_UNAVAILABLE_FREE`, espere:
```bash
curl -i http://127.0.0.1:18100/readyz
```
Resultado:
```text
HTTP/1.1 503 Service Unavailable
```
4. Verifique o Pod:
```bash
kubectl get pod "$POD"
```
Ele deve ficar `NotReady` enquanto o sidecar estiver sem capacidade.
5. Quando conexões forem liberadas e `free >= XAI_POOL_RECOVER_FREE`, o `/readyz` volta a 200 e o Pod retorna aos endpoints.
## 14. Verificar endpoints do Service
```bash
kubectl -n "$K8S_NAMESPACE" get endpoints ${APP_NAME}-regional -o wide
```
ou, em clusters novos:
```bash
kubectl -n "$K8S_NAMESPACE" get endpointslice \
-l kubernetes.io/service-name=${APP_NAME}-regional -o yaml
```
Um Pod NotReady não deve ser usado para novas conexões do Service.
## 15. Testar failover regional
### Teste controlado de ORD
Coloque ORD em drain:
```bash
ORD_POD=$(kubectl -n "$K8S_NAMESPACE" get pod \
-l app=${APP_NAME}-regional,tia-region=ord \
-o jsonpath='{.items[0].metadata.name}')
kubectl -n "$K8S_NAMESPACE" exec "$ORD_POD" -c xai-pool -- \
python -c "import urllib.request; urllib.request.urlopen(urllib.request.Request('http://127.0.0.1:18100/drain', method='POST')).read()"
```
Confirme `/readyz=503` e gere **nova chamada**. Ela deve ser entregue a um Pod IAD ainda Ready.
Esse teste não deve ser interpretado como migração de uma chamada existente; somente novas conexões são rebalanceadas.
## 16. Testar recuperação
O drain manual é intencional e não é revertido. Para voltar o Pod, reinicie-o:
```bash
kubectl -n "$K8S_NAMESPACE" delete pod "$ORD_POD"
```
O novo Pod deve:
1. iniciar sidecar;
2. pré-aquecer sockets;
3. atingir `recover_free_threshold`;
4. ficar Ready;
5. entrar novamente nos endpoints.
## 17. Load Balancer e WebSocket
WebSocket é suportado porque a conexão começa como HTTP Upgrade. O LB escolhe o backend no handshake e mantém a conexão naquele Pod.
Não use sticky session como requisito de correção. A conexão WebSocket em si já é persistente ao backend selecionado. Em caso de perda do Pod, o cliente precisa reconectar.
Uma política equivalente a least-connections pode ser útil quando o LB permite configuração, mas o mecanismo primário de proteção desta arquitetura é a readiness baseada em capacidade real do TTS.
## 18. HPA
Os manifests incluem HPA por CPU como proteção inicial.
Atenção: aumentar réplicas também aumenta o número de sockets xAI pré-aquecidos. Portanto, HPA só deve ter `maxReplicas` maior que 1 se a quota xAI permitir:
```text
HPA_MAX_REPLICAS * XAI_POOL_SIZE <= quota regional permitida
```
Para evolução futura, prefira uma métrica customizada que considere chamadas/TTS ativos e também um controlador que respeite orçamento global de sockets.
## 19. Rollback
Para voltar ao modelo anterior:
1. redirecione o Service/LB para o deployment antigo;
2. ou restaure o manifesto `k8s/tia/deployment.yaml` original;
3. o código antigo `OraclexAITTS` continua presente e compatível com endpoint xAI direto.
A alteração não remove suporte ao modo anterior.
## 20. Checklist de produção
- [ ] confirmar quota real de WebSockets por endpoint/região/tenancy;
- [ ] confirmar se ORD e IAD têm pools de capacidade independentes;
- [ ] validar `XAI_POOL_SIZE * replicas` por região;
- [ ] validar Secret/Vault;
- [ ] medir tempo de prewarm dos 50 sockets;
- [ ] observar taxa de erro de handshake;
- [ ] validar refresh escalonado em janela > 10 minutos;
- [ ] testar saturation -> `/readyz=503`;
- [ ] testar recovery -> `/readyz=200`;
- [ ] testar drain durante chamada ativa;
- [ ] testar rollout sem perda de novas chamadas;
- [ ] testar perda total de ORD e entrada em IAD;
- [ ] correlacionar TTFB/gap/underflow com `pool_free`;
- [ ] validar LB/Service com WebSocket real;
- [ ] executar stress test com perfil semelhante ao tráfego de produção.

View File

@@ -0,0 +1,151 @@
# Plano de Testes — TIA Regional / Pool xAI
## Objetivo
Validar separadamente capacidade do pool, comportamento do Kubernetes, failover regional, impacto de latência e resiliência do xAI.
## Camada 1 — Smoke
1. subir 1 Pod ORD com pool pequeno (`XAI_POOL_SIZE=3`);
2. confirmar 3 conexões healthy;
3. realizar uma chamada e uma síntese;
4. confirmar que `leased` vai 0 -> 1 -> 0;
5. confirmar que o socket upstream continua healthy após `audio.done`.
## Camada 2 — Prewarm
Subir com `XAI_POOL_SIZE=50` e medir:
- tempo total até Ready;
- taxa de sucesso de handshake;
- número máximo de handshakes simultâneos;
- impacto de `XAI_POOL_PREWARM_CONCURRENCY` 2, 5 e 10.
Critério inicial: nenhuma rajada deve reproduzir os erros de conexão observados no modelo burst.
## Camada 3 — Saturação
Gerar mais utterances concorrentes que slots.
Esperado:
- leases nunca superam `XAI_POOL_SIZE`;
- acquire espera até `XAI_POOL_ACQUIRE_TIMEOUT_S`;
- readiness fica 503 quando free cruza threshold;
- novas chamadas deixam de entrar naquele Pod;
- chamadas existentes continuam.
## Camada 4 — Histerese
Com size=10:
```text
UNAVAILABLE_FREE=2
RECOVER_FREE=5
```
Esperado:
- free=2 -> NotReady;
- free=3/4 -> continua NotReady;
- free=5 -> Ready.
## Camada 5 — Refresh
Use TTL curto em FQA:
```text
XAI_POOL_CONNECTION_TTL_S=60
XAI_POOL_REFRESH_JITTER_S=20
```
Observe por 5 minutos.
Esperado:
- sockets são renovados individualmente;
- não existe burst de 50 reconnects;
- slots ocupados não são renovados no meio da síntese;
- pool retorna ao tamanho configurado.
## Camada 6 — Falha upstream
Bloqueie ORD ou aponte temporariamente para endpoint inválido.
Esperado:
- slots ORD tornam-se unhealthy;
- maintenance tenta recuperação;
- readiness ORD cai;
- Service deixa de enviar novas chamadas a ORD;
- IAD continua Ready.
## Camada 7 — Latência
Compare três cenários:
A. TIA -> xAI direto sem pool
B. TIA -> localhost pool -> xAI com socket já aquecido
C. TIA -> localhost pool -> xAI durante recuperação/abertura de socket
Meça:
- `provider_ttfb_ms`;
- `end_to_end_ttfb_ms`;
- `max_audio_delta_gap_ms`;
- `xai_underrun_estimado_ms`;
- connect time do upstream;
- `pool_free` e `pool_leased`.
Hipótese: o hop localhost adiciona latência desprezível frente ao TTFB do provider, enquanto remove handshake xAI do caminho crítico na situação normal.
## Camada 8 — Carga semelhante a produção
Evite somente burst C=200. Use sockets persistentes e concorrência de síntese representativa da operação real, seguindo a metodologia que produziu resultados reprodutíveis nos testes anteriores.
Rodar pelo menos:
```text
20% da carga alvo
50%
80%
100%
120% por janela curta
```
## Camada 9 — Rollout
Com chamadas ativas:
```bash
kubectl rollout restart deployment/<tia-ord>
```
Validar:
- Pod antigo entra em drain;
- não recebe novas conexões;
- Pod novo preaquece antes de ficar Ready;
- `maxUnavailable=0` preserva capacidade durante rollout.
## Camada 10 — Failover regional
1. ORD e IAD Ready;
2. iniciar tráfego contínuo;
3. tornar ORD NotReady;
4. verificar novas chamadas em IAD;
5. recuperar ORD;
6. verificar reentrada progressiva.
## Evidências a guardar
- logs do sidecar;
- `/pool/status` em intervalos de 1s;
- métricas Prometheus;
- logs TIA de TTFB/underflow;
- quantidade de endpoints Ready por região;
- distribuição de chamadas por Pod;
- erros de WebSocket upstream;
- timestamps de `audio.done`.