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>
70 lines
2.0 KiB
TypeScript
70 lines
2.0 KiB
TypeScript
import { useEffect } from 'react'
|
|
import styles from './App.module.css'
|
|
import { useWizard } from './api/useWizard'
|
|
import { Toasts } from './components/Toasts/Toasts'
|
|
import { GameScreen } from './screens/GameScreen/GameScreen'
|
|
import { HomeScreen } from './screens/HomeScreen/HomeScreen'
|
|
import { LobbyScreen } from './screens/LobbyScreen/LobbyScreen'
|
|
|
|
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, 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.
|
|
useEffect(() => {
|
|
if (lobbyCode) {
|
|
window.history.replaceState(null, '', `?lobby=${lobbyCode}`)
|
|
} else if (status === 'open' && window.location.search) {
|
|
window.history.replaceState(null, '', window.location.pathname)
|
|
}
|
|
}, [lobbyCode, status])
|
|
|
|
let screen
|
|
if (!lobby || !playerId) {
|
|
screen = (
|
|
<HomeScreen
|
|
connected={status === 'open'}
|
|
acceptingGames={serverStatus.accepting_games}
|
|
actions={actions}
|
|
/>
|
|
)
|
|
} else if (lobby.status === 'waiting' || !game) {
|
|
screen = (
|
|
<LobbyScreen
|
|
lobby={lobby}
|
|
playerId={playerId}
|
|
acceptingGames={serverStatus.accepting_games}
|
|
actions={actions}
|
|
/>
|
|
)
|
|
} else {
|
|
screen = (
|
|
<GameScreen
|
|
lobby={lobby}
|
|
game={game}
|
|
playerId={playerId}
|
|
completedTrick={completedTrick}
|
|
actions={actions}
|
|
/>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className={styles.app}>
|
|
{status !== 'open' && <div className={styles.banner}>{STATUS_MESSAGES[status]}</div>}
|
|
{screen}
|
|
<Toasts toasts={toasts} onDismiss={dismissToast} />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default App
|