Add resource limits, abuse protection and overload handling

Running games always continue; new work is refused when there is no
capacity; abuse is rejected immediately.

- Docker: CPU/memory/pids caps per container (overridable in .env);
  uvicorn message size, queue and concurrency limits
- Server abuse limits: connections total/per IP (close 1013), message
  rate per connection (token bucket, close 1008 on floods), lobby
  creation rate per IP
- Load monitor: event loop lag, CPU of the own cgroup quota and
  throttling, with hysteresis (normal/busy/overloaded, severe lag
  overloads immediately)
- Admission control: create_lobby and start_game refused while
  overloaded or MAX_ACTIVE_GAMES are running; server_status message
  broadcast on changes; /health reports load but stays ok
- AI bridge: timeout sent with requests, in-flight cap, circuit breaker
- AI service: bounded workers and queue ("overloaded"), drops expired
  requests, degrades to the heuristic agent when busy or too slow
- Website: busy notices, Create/Start disabled when not accepting
  games, slow retry when the server is full
- Tests for limiters, load levels, admission control and AI service
  limits; docs updated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 12:51:46 +02:00
co-authored by Claude Opus 5.5
parent 905574e8f1
commit 87dfcd55b7
33 changed files with 1591 additions and 128 deletions
+18 -3
View File
@@ -10,10 +10,12 @@ const STATUS_MESSAGES = {
connecting: 'Connecting to the server…',
closed: 'Connection lost. Reconnecting…',
replaced: 'This game was opened in another window. Reload the page to continue here.',
busy: 'The server is full right now. Retrying in a moment…',
} as const
function App() {
const { status, playerId, lobby, game, completedTrick, toasts, dismissToast, actions } = useWizard()
const { status, playerId, lobby, game, completedTrick, serverStatus, toasts, dismissToast, actions } =
useWizard()
const lobbyCode = lobby?.code
// Keep the lobby code in the address bar so the URL can be shared as an invite link.
@@ -27,9 +29,22 @@ function App() {
let screen
if (!lobby || !playerId) {
screen = <HomeScreen connected={status === 'open'} actions={actions} />
screen = (
<HomeScreen
connected={status === 'open'}
acceptingGames={serverStatus.accepting_games}
actions={actions}
/>
)
} else if (lobby.status === 'waiting' || !game) {
screen = <LobbyScreen lobby={lobby} playerId={playerId} actions={actions} />
screen = (
<LobbyScreen
lobby={lobby}
playerId={playerId}
acceptingGames={serverStatus.accepting_games}
actions={actions}
/>
)
} else {
screen = (
<GameScreen