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
+10 -1
View File
@@ -53,7 +53,8 @@ The only stateful piece of the app.
| Value | Meaning |
|-------|---------|
| `status` | `connecting` \| `open` \| `closed` (will retry) \| `replaced` (opened in another tab, no retry) |
| `status` | `connecting` \| `open` \| `closed` (will retry) \| `replaced` (opened in another tab, no retry) \| `busy` (server full, retries slowly) |
| `serverStatus` | last `server_status` message: `{ load, accepting_games }` |
| `playerId` | from `welcome` |
| `lobby`, `game` | exactly what the last `state` message contained |
| `completedTrick` | the last `trick_completed` event, kept for 2.2 s so the trick stays visible |
@@ -73,6 +74,10 @@ The only stateful piece of the app.
second tab is a different player.
- On close it reconnects with exponential backoff (1, 2, 4, 8, 10, 10 … s),
except after close code 4000.
- If the server refuses the connection because it is full (an `error` with
code `busy` before `welcome`, then close code 1013), the status becomes
`busy` and it retries slowly (15 s, 30 s, 60 s, …) so waiting clients don't
add load.
- `send()` shows a toast if the socket is not open.
## Screen selection (`App.tsx`)
@@ -96,6 +101,8 @@ shows the connection status when it is not `open`.
- Code input + "Join" → `actions.joinLobby(code, name)`. The code is prefilled
from `?lobby=` so invite links work.
- Buttons are disabled while disconnected or without a name.
- While `serverStatus.accepting_games` is false, "Create lobby" is disabled and
a notice explains that the server is busy (joining still works).
### LobbyScreen
@@ -107,6 +114,8 @@ shows the connection status when it is not `open`.
seats are filled). Others see the options read-only and "Waiting for the
host…".
- Shows a warning if AI players are present but the AI service is offline.
- While `serverStatus.accepting_games` is false, "Start game" shows
"Server busy" (disabled) with a notice; the lobby stays open.
- Explains the fixed bidding rule (bids may not add up to the cards in hand).
### GameScreen (`screens/GameScreen/`)