Files
suaveplan/openspec/archive/2026-05-24-game-server-performance/specs/game-server/spec.md
2026-05-27 23:29:53 +01:00

100 lines
3.5 KiB
Markdown

# game-server — performance spec delta (Wave B-5b)
## ADDED Requirements
### Requirement: ObjectPool typed reuse
`ObjectPool<T>` SHALL recycle typed objects between a configurable
`factory`, `reset`, and optional `dispose` to dampen GC pressure, and
SHALL enforce a configurable `maxSize` cap by raising typed errors
when callers misuse the pool.
#### Scenario: pre-allocation populates the pool
- **GIVEN** an `ObjectPool` configured with `initialSize: 2` and
`preAllocate: true`
- **WHEN** the pool is constructed
- **THEN** `getStats().available` reports `2`
#### Scenario: acquire returns a recycled object
- **GIVEN** a pool with two pre-allocated objects
- **WHEN** the caller invokes `acquire()`
- **THEN** the returned object came from the pool
- **AND** `getStats().totalReused` reports `1`
#### Scenario: cap rejection
- **GIVEN** a pool with `maxSize: 1` and one object outstanding
- **WHEN** the caller invokes `acquire()` again
- **THEN** the call throws
`GameServerError(GAME_SERVER.POOL_EXHAUSTED)`
#### Scenario: foreign-object release rejection
- **WHEN** the caller releases an object not handed out by this pool
- **THEN** `release` throws
`GameServerError(GAME_SERVER.RELEASE_FOREIGN_OBJECT)`
#### Scenario: dispose lockout
- **GIVEN** a disposed `ObjectPool`
- **WHEN** the caller invokes `acquire()`, `release(obj)`, or `clear()`
- **THEN** each call throws
`GameServerError(GAME_SERVER.POOL_DISPOSED)`
### Requirement: SessionPool plain-data session recycling
`SessionPool` SHALL pool plain-data `PooledSession` records and
expose a `acquireSession(data)` API that stamps the borrowed record
with the caller-supplied identity and metadata before returning it.
#### Scenario: acquireSession stamps the record
- **WHEN** the caller invokes
`acquireSession({ id: "match-42", type: "game", metadata: {...} })`
- **THEN** the returned `PooledSession` carries `id === "match-42"`
- **AND** its `children` array is empty
- **AND** its `metadata.createdAt` mirrors the caller's input
#### Scenario: releaseSession recycles the record
- **GIVEN** a pool with `initialSize: 1, maxSize: 5`
- **WHEN** the caller acquires, mutates, and releases a session
- **AND** the caller immediately re-acquires
- **THEN** the same underlying object is returned with a freshly
empty `children` array
### Requirement: NetworkTierAdapter capability-driven tier selection
`NetworkTierAdapter` SHALL accept a non-empty, validated tier list at
construction time, track per-node capability assessments, and
recommend an optimal tier per node based on its composite score.
#### Scenario: empty tier list rejection
- **WHEN** the adapter is constructed with `tiers: []`
- **THEN** the constructor throws
`GameServerError(GAME_SERVER.INVALID_TIER_CONFIG)`
#### Scenario: score-based optimal tier
- **GIVEN** an adapter with `host`, `sub-host`, `player` tiers
- **WHEN** `getOptimalTier({score: 92, ...})` is invoked
- **THEN** the returned tier carries `nodeType === "host"`
- **AND** `getOptimalTier({score: 60, ...})` returns `sub-host`
- **AND** `getOptimalTier({score: 20, ...})` returns `player`
#### Scenario: load-balancing recommendation sort order
- **GIVEN** three registered nodes with scores 90, 60, 20
- **WHEN** `getLoadBalancingRecommendations()` is invoked
- **THEN** the result is sorted by capability score descending
- **AND** the first entry's `recommendedLoad` is `floor(90 / 10) === 9`
#### Scenario: unknown-tier rejection
- **WHEN** `getTierForNodeType("nope")` is invoked
- **THEN** the call throws
`GameServerError(GAME_SERVER.UNKNOWN_TIER)`