Pular para o conteúdo principal

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árioRoda comoResponsabilidade
Control planeContainer de vida longa (ECS Fargate)API HTTP, feed WebSocket, estado do lobby, consumidor de fila, despacho, agregação
GravadorUma 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:

  1. 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.
  2. Pub/sub de eventos. Como um evento publicado por um processo alcança clientes WebSocket conectados em outro.
  3. 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.

O gravador precisa de GPU

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:

ModoQuando é usadoPropósito
LambdaUma função de gravador está configuradaProdução. Invoke assíncrono real, um por assento
RIESenão, se uma URL de Runtime Interface Emulator estiver definidaDesenvolvimento local e smoke tests
ECSSenão, se cluster e task definition estiverem definidosFallback 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