Initial Commit
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
# SessionPool
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`SessionPool` is a session-specific wrapper around `ObjectPool<T>` that
|
||||
recycles `PooledSession` records — plain-data session containers
|
||||
(`id`, `type`, `metadata`, `children[]`) — between game-loop frames.
|
||||
The wrapper presents a "fresh session per call" API
|
||||
(`acquireSession(data) → PooledSession`) while internally amortising
|
||||
the underlying allocation across borrows, which keeps GC pressure low
|
||||
in workloads that churn through thousands of short-lived game sessions
|
||||
per minute (matchmaking server, tournament orchestrator, tutorial sim).
|
||||
|
||||
The pool is universal. It owns no logic beyond what `ObjectPool`
|
||||
already provides; the wrapper exists so the factory + reset semantics
|
||||
stay close to the session shape and so callers can inject a `Clock` for
|
||||
deterministic placeholder timestamps without poking at the underlying
|
||||
pool's `factory`.
|
||||
|
||||
## Features
|
||||
|
||||
- Typed wrapper over `ObjectPool<PooledSession>`.
|
||||
- Configurable `initialSize` (default 20), `maxSize` (default 100), and
|
||||
`preAllocate` toggle.
|
||||
- Injectable `Clock` for deterministic placeholder timestamps.
|
||||
- `acquireSession(data)`, `releaseSession(session)`, and auto-release
|
||||
`withSession(data, callback)`.
|
||||
- `getStats()` forwards `ObjectPool`'s `PoolStats` for dashboard reuse.
|
||||
- Forwarded lifecycle: `clear()`, `dispose()`, `disposed` getter.
|
||||
- Default session type configurable via `defaultSessionType`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```typescript
|
||||
import { SessionPool } from "@suaveplan/game-server/performance/session-pool";
|
||||
|
||||
const pool = new SessionPool();
|
||||
const session = pool.acquireSession({
|
||||
id: "match-42",
|
||||
type: "game",
|
||||
metadata: { createdAt: Date.now(), updatedAt: Date.now() },
|
||||
});
|
||||
session.children.push("player-alice", "player-bob");
|
||||
|
||||
pool.releaseSession(session);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```typescript
|
||||
import { SessionPool } from "@suaveplan/game-server/performance/session-pool";
|
||||
import { metrics } from "@opentelemetry/api";
|
||||
|
||||
const pool = new SessionPool(
|
||||
{
|
||||
initialSize: 100,
|
||||
maxSize: 2_000,
|
||||
defaultSessionType: "queue",
|
||||
},
|
||||
{
|
||||
clock: () => Date.now(),
|
||||
meter: metrics.getMeter("game-server.sessions"),
|
||||
},
|
||||
);
|
||||
|
||||
// Hot loop: every match-found event borrows a session, populates it,
|
||||
// hands it to a downstream coordinator, and releases it on cleanup.
|
||||
await pool.withSession(
|
||||
{
|
||||
id: `match-${crypto.randomUUID()}`,
|
||||
type: "game",
|
||||
metadata: {
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
queueKey: "ranked",
|
||||
},
|
||||
},
|
||||
async (session) => {
|
||||
for (const playerId of matchedPlayers) {
|
||||
session.children.push(playerId);
|
||||
}
|
||||
await coordinator.start(session);
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new SessionPool(config?: SessionPoolConfig, deps?: SessionPoolDeps)`.
|
||||
- `acquireSession(data: PooledSessionData): PooledSession` — stamps the
|
||||
borrowed record with caller fields, clears `children`, returns.
|
||||
- `releaseSession(session: PooledSession): void` — forwards to
|
||||
`ObjectPool.release`; throws `RELEASE_FOREIGN_OBJECT` on a foreign
|
||||
object, `POOL_DISPOSED` after dispose.
|
||||
- `withSession(data, callback): Promise<R>` — borrows + finally-releases.
|
||||
- `getStats(): PoolStats`.
|
||||
- `clear()` / `dispose()` / `disposed: boolean`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`acquireSession` always overwrites the record's identity and metadata
|
||||
fields before returning it, so callers never observe stale placeholder
|
||||
data left over from a previous borrow. The `children` array is
|
||||
emptied via `length = 0` rather than reassigned to a new array, which
|
||||
keeps the underlying buffer (and its hidden-class shape) alive — a
|
||||
small but measurable win in V8 hot loops. The factory's placeholder id
|
||||
counter increments monotonically across both factory + reset paths so
|
||||
production logs that capture a borrowed session before the caller
|
||||
overwrote its id can be debugged unambiguously. The wrapper does not
|
||||
re-export `ObjectPool`'s underlying counters — instead the OTel meter
|
||||
is forwarded into the underlying pool so the same `pool.acquire_total`
|
||||
/ `pool.release_total` instruments carry the session pool's
|
||||
contribution. Dashboards can disambiguate by attaching pool-name
|
||||
attributes on a thin wrapper if needed.
|
||||
Reference in New Issue
Block a user