Arquitetura
O SlotBattle entrega dois binários e mais nada. Saber o que cada um faz explica a maior parte da superfície operacional e a maior parte das restrições.
| Binário | Roda como | Responsabilidade |
|---|---|---|
| Control plane | Container de vida longa (ECS Fargate) | API HTTP, feed WebSocket, estado do lobby, consumidor de fila, despacho, agregação |
| Gravador | Uma invocação por assento de batalha (Lambda ou task ECS) | Um browser headless, uma captura de vídeo, um upload, um callback |
O control plane nunca toca em Chrome nem em ffmpeg. Essa divisão é o motivo de a imagem do control plane ter ~24 MB e ser distroless enquanto a do gravador tem ~325 MB, e de escalar a plateia e escalar a gravação serem problemas independentes.
As peças
Dependências, e para que cada uma é crítica
Postgres é a fonte da verdade. Batalhas e assentos vivem lá, e também as duas tabelas que fazem o lançamento sobreviver a uma queda: intents duráveis de lançamento e tentativas por assento. Um control plane que morre no meio de um lançamento retoma a partir delas em vez de perder a batalha.
Redis faz três trabalhos sem relação entre si:
- Barreiras entre processos. Os assentos coordenam o início por barreiras de conjunto, de modo que oito browsers que subiram em velocidades diferentes ainda giram juntos.
- Pub/sub de eventos. Como um evento publicado por um processo alcança clientes WebSocket conectados em outro.
- A fila de batalhas (
slotbattle:stream). Uma segunda forma de abrir uma batalha, ao lado do HTTP.
S3 atrás de uma CDN guarda a saída HLS. Os segmentos são enviados em ordem conforme são produzidos. Espectadores puxam vídeo da CDN, nunca do SlotBattle.
Como um assento é gravado
Dentro de uma invocação do gravador, por assento:
A captura é um screencast CDP, não uma leitura de tela X11. Ela codifica só os frames que o jogo de fato renderiza, e produz saída com frame rate constante, para reprodução suave.
A gravação é janelada por evento: a captura arma quando os giros grátis começam e para quando a rodada acaba, caindo para uma duração fixa se esses eventos nunca chegarem. A saída é o bônus, não dez minutos de tela de carregamento.
Os cliques são dirigidos por receitas por jogo com esperas por evento em vez de sleeps fixos. O gravador espera o jogo informar que está pronto.
Sob Xvfb, o Chrome precisa de ANGLE/EGL via /dev/dri. Sem isso, ele cai para Canvas2D a algo
como 2 fps: a gravação termina e fica impossível de assistir, sem nada no log que marque isso.
O SwiftShader é o fallback suportado sem GPU, e custa CPU no lugar.
Três formas de despachar um assento
O control plane escolhe a primeira configurada, nesta ordem:
| Modo | Quando é usado | Propósito |
|---|---|---|
| Lambda | Uma função de gravador está configurada | Produção. Invoke assíncrono real, um por assento |
| RIE | Senão, se uma URL de Runtime Interface Emulator estiver definida | Desenvolvimento local e smoke tests |
| ECS | Senão, se cluster e task definition estiverem definidos | Fallback de qualidade com CPU alta, mesma imagem, ≥8 vCPU |
O boot falha se nenhum dos três estiver configurado. Uma instância que não consegue despachar um assento não consegue rodar uma batalha, então a falha aparece na inicialização e não na primeira batalha.
Duas portas
A API REST escuta em 8070. O feed WebSocket escuta em 8071, a partir de um servidor HTTP
próprio, e não pelo roteador do framework.
O framework instala um middleware global de timeout que dispara um 503 em qualquer conexão
que um handler já sequestrou para o upgrade de WebSocket, e a conexão de uma batalha fica
aberta durante toda a duração dela, o que são minutos. Não há override por rota alcançável do
código da aplicação, então o feed roda o próprio servidor com os próprios timeouts: um read
timeout no handshake para manter a proteção contra slowloris, e nenhum write timeout.
A consequência operacional: o seu load balancer precisa de dois target groups, um por
porta, com uma regra de listener roteando /ws para o segundo. Um único target group quebra a
visualização ao vivo enquanto deixa a API REST saudável.
Onde as fronteiras são impostas
A direção de import é
dtos ← models ← services ← adapters/repositories ← http/console/runners ← providers.
A CI verifica isso como um gate bloqueante, e não como relatório, então um import que viola
quebra o build.
Próximo
- Tenancy: como uma instância serve vários casinos
- Eventos: o que passa pelo Redis e o que chega a um browser
- O que você recebe: as superfícies disso tudo que você realmente toca