v0.11.0 harden persistence and prepare game replays
This commit is contained in:
+69
-27
@@ -2,14 +2,18 @@
|
||||
|
||||
## Components
|
||||
|
||||
### Transport Layer (`internal/http`)
|
||||
### Transport Layer (`internal/server/http`)
|
||||
Fiber web server handling HTTP requests/responses. Implements routing, rate limiting, content-type validation, JWT authentication middleware, request parsing. Translates HTTP to internal Command objects.
|
||||
|
||||
### Processing Layer (`internal/processor`)
|
||||
### Processing Layer (`internal/server/processor`)
|
||||
Central command handler containing business logic. Single `Execute(Command)` entry point decouples transport from logic. Uses synchronous UCI engine for validation, asynchronous EngineQueue for computer moves. Commands include optional user context for authenticated operations.
|
||||
|
||||
### Service Layer (`internal/service`)
|
||||
In-memory state storage with authentication support. Thread-safe game map protected by RWMutex. Manages game lifecycle, snapshots, player configuration, user accounts, and JWT token generation. Coordinates with storage layer for persistence of both games and users.
|
||||
### Service Layer (`internal/server/service`)
|
||||
In-memory live-game storage with authentication support. A mutex protects each
|
||||
game transition and callers receive immutable game views rather than mutable
|
||||
game pointers. The service manages game lifecycle, snapshots, player
|
||||
configuration, user accounts, JWT tokens, persistence, and terminal-game
|
||||
eviction. Eviction removes only the memory copy; durable replay data remains.
|
||||
|
||||
#### Long-Polling Registry (`internal/service/waiter.go`)
|
||||
Manages clients waiting for game state changes via HTTP long-polling. Tracks move counts per client, sends notifications on state changes, enforces 30-second timeout. Non-blocking notification pattern handles slow clients gracefully. Coordinates with service layer for game updates and deletion events.
|
||||
@@ -18,18 +22,31 @@ Manages clients waiting for game state changes via HTTP long-polling. Tracks mov
|
||||
- **Password Hashing**: Argon2id for secure password storage
|
||||
- **JWT Management**: HS256 tokens with 7-day expiration
|
||||
- **User Operations**: Registration, login, profile management
|
||||
- **Session Tracking**: Last login timestamps
|
||||
- **Session Tracking**: One persisted session per user, with JWT subject/session
|
||||
binding and last-login timestamps
|
||||
|
||||
### Storage Layer (`internal/storage`)
|
||||
SQLite persistence with async writes for games, synchronous writes for authentication operations. Buffered channel (1000 ops) processes game writes sequentially in background. User operations use direct database access for consistency. Graceful degradation on write failures. WAL mode for development environments.
|
||||
### Storage Layer (`internal/server/storage`)
|
||||
SQLite persistence with ordered asynchronous writes for gameplay and
|
||||
synchronous writes for authentication. A bounded channel (1,000 operations)
|
||||
feeds one transactional writer. Move persistence groups the move, first-move
|
||||
slot claim, and terminal result in one transaction. Replay reads insert a
|
||||
barrier behind accepted writes and then read the game and moves in one SQLite
|
||||
snapshot.
|
||||
|
||||
WAL, foreign-key enforcement, a five-second busy timeout, and NORMAL
|
||||
synchronous mode are configured in the connection string so every pooled
|
||||
connection receives the same settings. The pool is deliberately small (eight
|
||||
open, four idle) because SQLite has one writer. A write failure or full queue
|
||||
marks storage degraded; live play remains available in memory and `/health`
|
||||
reports that durable history may be incomplete.
|
||||
|
||||
### Supporting Modules
|
||||
- **Engine** (`internal/engine`): UCI protocol wrapper for Stockfish process communication
|
||||
- **Game** (`internal/game`): Game state with snapshot history and player associations
|
||||
- **Board** (`internal/board`): FEN parsing and ASCII generation
|
||||
- **Core** (`internal/core`): Shared types, API models, error constants
|
||||
- **CLI** (`cmd/chessd/cli`): Database and user management commands
|
||||
- **Client** (`cmd/chess-client`, `internal/client`): Interactive debugging client with command registry, session management, and colored terminal output
|
||||
- **CLI** (`cmd/chess-server/cli`): Database and user management commands
|
||||
- **Client** (`cmd/chess-client-cli`, `internal/client`): Interactive debugging client with command registry, session management, and colored terminal output
|
||||
|
||||
## Request Flow
|
||||
|
||||
@@ -55,9 +72,10 @@ SQLite persistence with async writes for games, synchronous writes for authentic
|
||||
3. Creates MakeMoveCommand, calls `processor.Execute()`
|
||||
4. Processor validates move via locked validation engine
|
||||
5. If legal, gets new FEN from engine
|
||||
6. Calls `service.ApplyMove()` to update state
|
||||
7. Persists move with player identification
|
||||
8. Returns GameResponse
|
||||
6. Calls `service.ApplyMoveWithState()` with the FEN, turn, and state that were validated
|
||||
7. Service rejects a stale concurrent commit or atomically updates the move, optional slot claim, and terminal result
|
||||
8. The same logical mutation is queued as one SQLite transaction
|
||||
9. Returns GameResponse
|
||||
|
||||
### Computer Move
|
||||
1. HTTP handler receives `POST /games/{id}/moves` with `{"move": "cccc"}`
|
||||
@@ -78,32 +96,44 @@ SQLite persistence with async writes for games, synchronous writes for authentic
|
||||
7. Client disconnection cancels wait via context
|
||||
8. Game deletion notifies and removes all waiters
|
||||
|
||||
### Durable Replay Read
|
||||
1. Client requests `GET /api/v1/games/{id}/history`
|
||||
2. Storage queues a barrier after all previously accepted gameplay writes
|
||||
3. The writer reaches the barrier only after those transactions finish
|
||||
4. Storage reads the game row and ordered moves in one read transaction
|
||||
5. The API returns the initial FEN plus every UCI move and resulting FEN
|
||||
6. This path works after terminal-memory eviction or a server restart
|
||||
|
||||
## Persistence Flow
|
||||
|
||||
### User Write Operations (Synchronous)
|
||||
1. Service layer calls storage method directly (CreateUser, UpdateUserPassword, etc.)
|
||||
2. Operations use database transactions for consistency
|
||||
3. Unique constraint checks within transaction
|
||||
4. Immediate commit or rollback
|
||||
5. Returns success or specific error (duplicate username, etc.)
|
||||
1. Service serializes public registrations within the process
|
||||
2. Storage checks uniqueness and capacity in a transaction
|
||||
3. At capacity, the oldest temporary user is evicted in that transaction
|
||||
4. The new user and initial session commit together; any failure rolls back the entire operation
|
||||
5. Login replaces the user's single session with one SQLite UPSERT
|
||||
6. Other account mutations commit before success is returned
|
||||
|
||||
### Game Write Operations (Asynchronous)
|
||||
1. Service layer calls storage method (RecordNewGame, RecordMove, DeleteUndoneMoves)
|
||||
1. Service layer calls a storage method (`RecordNewGame`, `RecordMove`, `RecordPlayers`, or `RewindGame`)
|
||||
2. Operation queued to buffered channel (non-blocking)
|
||||
3. Writer goroutine processes queue sequentially
|
||||
4. Transactions ensure atomicity
|
||||
5. Failures trigger degradation to memory-only mode
|
||||
4. Each logical mutation commits in one transaction
|
||||
5. A full queue or write failure is logged and triggers degraded memory-only mode
|
||||
6. Shutdown rejects new writes and drains every write already accepted
|
||||
|
||||
### Query Operations
|
||||
1. CLI invokes Store.QueryGames or Store.GetUserByUsername with filters
|
||||
2. Direct database read (no queue)
|
||||
3. Case-insensitive matching for usernames/emails
|
||||
4. Results formatted as tabular output
|
||||
1. Replay-sensitive game reads wait on the async-write barrier
|
||||
2. User-game filtering matches creation-time player IDs and later claim IDs
|
||||
3. Move lookup uses the `UNIQUE(game_id, move_number)` index prefix and returns move order directly
|
||||
4. Username and email lookups are case-insensitive
|
||||
5. CLI database queries format records as tabular output
|
||||
|
||||
## Concurrency
|
||||
|
||||
- **HTTP Server**: Fiber handles concurrent connections
|
||||
- **Game State**: Single RWMutex protects game map (concurrent reads, serial writes)
|
||||
- **Move Validation**: Optimistic FEN/state/turn checks reject a result if the game changed while Stockfish was validating
|
||||
- **Engine Workers**: Fixed pool (2 workers) with dedicated Stockfish processes
|
||||
- **Validation Engine**: Single mutex-protected instance for synchronous validation
|
||||
- **Storage Writer**: Single goroutine processes game write queue sequentially
|
||||
@@ -140,6 +170,7 @@ type Snapshot struct {
|
||||
"sub": "user-id",
|
||||
"username": "alice",
|
||||
"email": "alice@example.com",
|
||||
"session_id": "session-id",
|
||||
"exp": 1234567890
|
||||
}
|
||||
```
|
||||
@@ -174,7 +205,11 @@ games (
|
||||
black_type INTEGER,
|
||||
black_level INTEGER,
|
||||
black_search_time INTEGER,
|
||||
start_time_utc DATETIME
|
||||
start_time_utc DATETIME,
|
||||
result TEXT, -- white_wins, black_wins, draw, or stalemate
|
||||
end_time_utc DATETIME,
|
||||
white_claimed_by TEXT, -- user that claimed the slot after creation
|
||||
black_claimed_by TEXT
|
||||
)
|
||||
|
||||
-- Move history
|
||||
@@ -186,10 +221,17 @@ moves (
|
||||
fen_after_move TEXT,
|
||||
player_color TEXT,
|
||||
move_time_utc DATETIME,
|
||||
FOREIGN KEY (game_id) REFERENCES games(game_id)
|
||||
FOREIGN KEY (game_id) REFERENCES games(game_id) ON DELETE CASCADE,
|
||||
UNIQUE (game_id, move_number)
|
||||
)
|
||||
```
|
||||
|
||||
Schema version 2 is applied idempotently to legacy databases with guarded
|
||||
`ALTER TABLE ADD COLUMN` migrations. Redundant indexes formerly duplicating
|
||||
UNIQUE constraints or useful index prefixes are removed. Purpose-built partial
|
||||
indexes cover non-empty email uniqueness, temporary-user cleanup, session
|
||||
expiry, and post-creation game claims.
|
||||
|
||||
## Security Architecture
|
||||
|
||||
### Authentication Flow
|
||||
@@ -209,5 +251,5 @@ moves (
|
||||
- Passwords never stored in plaintext
|
||||
- JWT secret rotates on restart (or fixed in dev mode)
|
||||
- User IDs use UUIDs with collision detection
|
||||
- Transactions ensure data consistency
|
||||
- Transactions keep registration, sessions, moves, claims, results, and rewinds internally consistent
|
||||
- Case-insensitive queries prevent duplicate accounts
|
||||
|
||||
Reference in New Issue
Block a user