100 lines
3.5 KiB
Markdown
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)`
|