gameabi

package
v2.14.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 17, 2026 License: MIT Imports: 28 Imported by: 0

Documentation

Overview

Package gameabi is the host side of the shellcade wasm game ABI: the Extism (wazero) host adapter that makes a .wasm artifact satisfy sdk.Game/sdk.Handler, and the host functions exposing the Room effect/read surface to the guest.

The ABI itself — version, names, packed payload encodings — is owned by the PUBLIC gamekit module (github.com/shellcade/kit/v2/wire); this package maps wire types onto the engine's sdk/canvas types.

Index

Constants

View Source
const DefaultCallbackDeadline = 100 * time.Millisecond

DefaultCallbackDeadline is the per-callback wall-clock kill switch when Options.CallbackDeadline is unset (admin-tunable per game via host.deadline_ms — see HostConfigSpecs).

View Source
const DefaultCheckpointInterval = 30 * time.Second

DefaultCheckpointInterval is the spec's ~30s default cadence.

View Source
const Heartbeat = 50 * time.Millisecond

Heartbeat is the default host-owned wake cadence (admin-tunable per game in production; a flag in the devkit).

View Source
const ResidentConfigKey = "host.resident"

ResidentConfigKey is the reserved admin config key granting the resident lifecycle to a slug (room-lifecycle-modes): a bool, set via the admin Game settings UI, read by the matchmaker when resolving a game's lifecycle.

View Source
const Version = wire.Version

Version is the ABI major version this host implements.

View Source
const WireRevision = wire.Revision

WireRevision is the kit wire revision this host was compiled against (wire.Revision): the monotonic counter of wire-visible minor additions within the ABI major. Re-exported here because wire imports are confined to this package (the anti-corruption layer) — the catalog compares an artifact's declared meta revision (sdk.GameMeta.WireRevision) against it and warns when an artifact is ahead of the host (deploy-order skew).

Variables

View Source
var ErrArtifactMismatch = errors.New("artifact mismatch")

ErrArtifactMismatch is returned (wrapped) by Restore when the snapshot was taken under a DIFFERENT wasm artifact than the game it is being restored into — the inevitable outcome of a catalog promotion or rollback moving the slug's live-version pointer while rooms were parked. The blob itself is intact; it is the live game that moved. Callers use errors.Is to tell this apart from genuine corruption: the lobby surfaces "your saved game was retired by a game update" instead of a corrupt/expired notice, and the resident bring-up logs the discarded drain snapshot explicitly.

View Source
var ErrCheckpointCorrupt = errors.New("gameabi: checkpoint failed integrity verification")

ErrCheckpointCorrupt reports a checkpoint that failed integrity verification (the MAC did not verify, or the blob was truncated/tampered in storage). It wraps blobstore.ErrSealVerify. The re-hydration path maps this to PER-ROOM quarantine (room-hosting spec "Re-Hydration") — never to peer death. By contract NO payload byte escapes ReadLatest when this is returned, so the restore path can rely on verify-before-write.

View Source
var ErrNoCheckpoint = errors.New("gameabi: no checkpoint for room")

ErrNoCheckpoint reports that a room has no checkpoint yet (the latest pointer is absent). Distinct from ErrCheckpointCorrupt: missing is "nothing to restore", corrupt is "quarantine this room".

Functions

func ABIMajor

func ABIMajor(g sdk.Game) (major uint32, ok bool)

ABIMajor reports the ABI major a loaded wasm game declared at the handshake probe (record-abi-major); ok is false if g is not a wasm game. Today the value always equals Version — LoadGameBytes refuses any other major — but the catalog persists the PROBED value per verified version so the column stays truthful the day the host accepts a set of majors.

func BindServices

func BindServices(h sdk.Handler, svc sdk.Services) bool

BindServices attaches live host services to a restored handler before it is driven. Services (leaderboard, per-user KV, config) are host resources that a snapshot deliberately does not carry; a resumed room must be rebound to the running instance's services so kv/config/leaderboard host calls behave as they did before hibernation. No-op (and reports false) if h is not a wasm room.

func CallbackDeadlineOf

func CallbackDeadlineOf(g sdk.Game) (time.Duration, bool)

CallbackDeadlineOf reports the per-callback wall-clock deadline a loaded wasm game runs under. ok is false if g is not a wasm game. Inspection helper (the load-test game runs a generous deadline so heavy ticks degrade, not trap).

func CallbackSplit

func CallbackSplit(h sdk.Handler) (total, host time.Duration)

CallbackSplit reports the cumulative wall time spent inside guest callbacks for h, and the portion of it spent in the send/identical HOST functions (delta apply + frame decode + fan-out) the guest invoked mid-callback. Guest-pure compute = total - host. Diagnostic surface for load benchmarks; actor-goroutine accuracy (read between callbacks).

func CheckpointHandler

func CheckpointHandler(ctx context.Context, cs *CheckpointStore, roomID string, epoch int64, h sdk.Handler) error

CheckpointHandler captures a NON-DESTRUCTIVE snapshot of a live wasm room and writes it through cs at the given epoch — the periodic-durability path (room-hosting spec "Periodic Room Checkpoints", design D5). It reuses the hibernation codec's deterministic snapshot byte-for-byte (SnapshotHandler); unlike sdk.Room.Hibernate it does NOT end or dispose the room, so the same handler keeps running and is checkpointed again at the next epoch. The capture MUST be taken at a quiescent point (no guest callback on the stack), exactly like SnapshotHandler — the actor schedules it on the room goroutine.

NOTE this convenience runs capture AND store write on the calling goroutine; the production peer instead splits them (SnapshotHandler on the actor, Write from the scheduler/drain goroutine — peer.fireCheckpoint) so a slow store never stalls the room actor. Prefer the split anywhere a live room serves players; this composite remains for the conformance harness and tests.

func CloseHandler

func CloseHandler(h sdk.Handler) bool

CloseHandler releases a handler's live plugin instance WITHOUT driving the room — the disposal path for a restored handler that was never adopted by a runtime. RestoreHandler returns a handler holding a live instance with grown, written linear memory (up to the game's 32MiB cap); the instance is otherwise closed only via OnClose through a running room, so dropping an unadopted handler pins that memory in the compiled plugin's shared wazero runtime until process restart. Every Restore call site guards its error and lost-race returns with the adopted-flag pattern:

adopted := false
defer func() {
	if !adopted {
		gameabi.CloseHandler(h)
	}
}()
...
ctl := sdk.NewRoomRuntime(roomID, h, ...) // the runtime owns h from here
adopted = true

Safe on a never-driven handler (no guest call is on the stack, so the instance closes immediately) and idempotent. No-op (reports false) if h is not a wasm room.

func GuestMemorySize

func GuestMemorySize(h sdk.Handler) uint32

GuestMemorySize reports the wasm room's current GUEST linear-memory size in bytes (the program's own TinyGo heap, not the extism runtime's). Returns 0 if h is not a wasm room or has no live instance. Sample it after a callback to track peak memory under a limit.

func HandlerConfig

func HandlerConfig(h sdk.Handler) (sdk.RoomConfig, bool)

HandlerConfig returns the wasm room's RoomConfig — for a restored handler that is the ORIGINAL room's config carried by the snapshot (mode, capacity, min players, seed), so a resume can rebuild the runtime with the room's real identity instead of synthesizing a fresh one.

func HandlerDeadline

func HandlerDeadline(h sdk.Handler) (d time.Duration, ok bool)

HandlerDeadline returns the per-room callback deadline the handler enforces (after any host.* config override) — the harness names this as the limit when a callback breaches it.

func HandlerEnded

func HandlerEnded(h sdk.Handler) bool

HandlerEnded reports whether the wasm room has settled (the guest called end, or a fault settled it).

func HandlerRoster

func HandlerRoster(h sdk.Handler) []sdk.Player

HandlerRoster returns the wasm room's last-seen roster — the same membership the snapshot codec records — so the hibernation header can carry it WITHOUT decompressing the blob. At an abandonment quiesce point the live room is empty, but the handler still holds the roster of the player(s) who were in it (the codec's roster of record), which is exactly who may resume the room. Returns nil if h is not a wasm room.

func HostConfigSpecs

func HostConfigSpecs() []sdk.ConfigKeySpec

HostConfigSpecs declares the reserved host-interpreted config keys as platform-side specs (add-config-specs), so the admin Game settings area renders them through the same typed machinery as game-declared keys — for every wasm game, replacing hand-written help text. Defaults and bounds are sourced from the constants above; like any config write, a change applies to NEW rooms only (read once at room construction).

func LastCallback

func LastCallback(h sdk.Handler) (exit uint32, err error, faulted bool)

LastCallback reports the most recent callback's wasm exit code, any trap or deadline error, and whether that callback faulted the room (a non-zero exit or a kill-switch error settled it). A timed-out callback surfaces as faulted with a non-nil err — the harness names it against the per-callback deadline.

func LoadGame

func LoadGame(path string, opts Options) (sdk.Game, error)

LoadGame compiles a wasm artifact from a file, validates the ABI handshake and meta on a throwaway instance, and returns a sdk.Game whose rooms run the guest.

func LoadGameBytes

func LoadGameBytes(wasm []byte, opts Options) (sdk.Game, error)

LoadGameBytes is LoadGame over an in-memory artifact (the catalog loads a verified blob from the object store, never a path). The two share all compile/handshake/meta logic; LoadGame is the file-backed convenience.

func MemoryCapBytes

func MemoryCapBytes(g sdk.Game) (uint64, bool)

MemoryCapBytes returns the wasm game's linear-memory cap in bytes (the load-time manifest MaxPages) — the harness names this as the memory limit.

func OverrideSlug

func OverrideSlug(g sdk.Game, slug string) bool

OverrideSlug renames a loaded wasm game (dev sideloading: avoid colliding with a compiled-in slug). Returns false if g is not a wasm game.

func QuarantineExempt

func QuarantineExempt(g sdk.Game) (exempt, ok bool)

QuarantineExempt reports whether a loaded wasm game has NO fault hook wired, so the fault watchdog can never quarantine it — as the built-in load-test game is (it must survive the overload it exists to measure). ok is false if g is not a wasm game.

func RestoreHandler

func RestoreHandler(g sdk.Game, blob []byte) (sdk.Handler, error)

RestoreHandler rehydrates a blob into a fresh handler bound to game g. g must be the same artifact the blob was taken from (the embedded sha256 + ABI version are verified).

The restored handler resumes the guest's linear memory, clock, roster, input context, RoomConfig, and entropy position — everything the snapshot owns. It does NOT resume the host SERVICES (leaderboard, per-user KV, config): those are live host resources, not part of the portable blob, so the caller must rebind them with BindServices before driving the first callback, exactly as the engine wires services into a fresh NewRoom. A restored room with no services no-ops kv/config/leaderboard host calls and will diverge from a live room that has them.

func SetHidden

func SetHidden(g sdk.Game, hidden bool) bool

SetHidden marks a loaded wasm game live-but-unlisted (sdk.GameMeta.Hidden): it stays reachable by exact slug (quick-match, direct entry, admin) but the lobby's player-facing menu omits it. Used for the built-in load-test game (add-loadtest-harness). ok is false if g is not a wasm game.

func SetLifecycle

func SetLifecycle(g sdk.Game, lc sdk.Lifecycle) bool

SetLifecycle overrides the room end-of-life mode a loaded wasm game declares. The built-in load-test game forces Ephemeral so its rooms run while players are connected and dispose after the abandon grace — never hibernated (no parked-room snapshots) and never resident (no always-on idle tick). ok is false if g is not a wasm game.

func SetMaxPlayers

func SetMaxPlayers(g sdk.Game, n int) bool

SetMaxPlayers overrides the room capacity a loaded wasm game declares. The built-in load-test game uses this to cap lobby size (a guest may declare a huge MaxPlayers as a stress testbed; the host bounds it). ok is false if g is not a wasm game.

func SnapshotHandler

func SnapshotHandler(h sdk.Handler) ([]byte, error)

SnapshotHandler freezes a wasm room handler into a portable blob. h must be a handler returned by a wasm game's NewRoom, taken at a quiescent point (no guest call on the stack).

func ValidateBareName

func ValidateBareName(slug string) error

ValidateBareName reports whether slug is a valid bare game name — the lower-case kebab-case rule (`^[a-z0-9-]{1,32}$`) every declared meta.slug must satisfy at load time. Exported so author-facing tooling (`shellcade-kit new`) can reject an invalid name at scaffold time with the same error text the loader would produce at the first `check`, instead of after a game has been built around the slug.

Types

type CadenceConfig

type CadenceConfig struct {
	Base       time.Duration // default cadence; <=0 falls back to DefaultCheckpointInterval
	Jitter     time.Duration // +/- spread around Base; <0 treated as 0, clamped to <= Base
	Clock      Clock         // injectable; nil = SystemClock
	Idle       func() bool   // true => suppress this interval's checkpoint; nil = never idle
	Checkpoint func(ctx context.Context, epoch int64) error
	Rand       *rand.Rand // injectable jitter source; nil = a time-seeded source

	// StartEpoch is the FIRST epoch the scheduler fires (default 0). It exists for
	// the post-reclaim path: a room restored from a checkpoint at epoch N must seed
	// its new scheduler at N+1 so the next periodic write SUPERSEDES the restore
	// blob and advances the latest pointer — the pointer-epoch is strictly
	// monotonic across drain/reclaim cycles, forever (a fresh placement uses 0).
	StartEpoch int64
}

CadenceConfig configures one room's checkpoint scheduler. Base and Jitter are the per-game override hook: the caller picks them per game (default ~30s base when zero). Idle suppresses checkpoints for a lobby-idle room. Checkpoint captures+writes the snapshot for the given epoch (typically a closure over CheckpointHandler, the room's CheckpointStore, and its UUID).

type CheckpointScheduler

type CheckpointScheduler struct {
	// contains filtered or unexported fields
}

CheckpointScheduler drives ONE room's periodic, non-destructive checkpoint cadence (room-hosting spec "Periodic Room Checkpoints", design D5): a jittered ticker (default ~30s ± jitter) that fires a checkpoint callback with a monotonic epoch starting at 0, suppressed while the room is lobby-idle.

The scheduler is deliberately decoupled from room internals: it takes an Idle probe func and a Checkpoint callback rather than a Room/Handler, so it lives entirely in this package's durability seam and the caller wires it to whatever idle signal and capture path it owns (e.g. CheckpointHandler on the actor). The epoch is owned here (monotonic per room from 0); the caller passes it straight to CheckpointStore.Write.

func NewCheckpointScheduler

func NewCheckpointScheduler(cfg CadenceConfig) *CheckpointScheduler

NewCheckpointScheduler builds a scheduler from cfg. It does not start ticking until Run is called.

func (*CheckpointScheduler) Close

func (s *CheckpointScheduler) Close()

Close stops the scheduler and BLOCKS until any in-flight fire has completed (so the epoch it advanced is visible to a subsequent NextEpoch). Run returns promptly once no fire is running. Idempotent.

func (*CheckpointScheduler) NextEpoch

func (s *CheckpointScheduler) NextEpoch() int64

NextEpoch reports the epoch the scheduler would fire next — the value the drain reads (after Close, so no periodic tick can advance it concurrently) to pick a drain epoch strictly above every committed periodic checkpoint. Safe to call from another goroutine.

func (*CheckpointScheduler) Run

func (s *CheckpointScheduler) Run(ctx context.Context)

Run drives the cadence until ctx is cancelled or Close is called. On each jittered tick it skips the checkpoint when Idle reports true (no epoch advance), otherwise it fires the Checkpoint callback with the next monotonic epoch (0, 1, 2, …) and advances only on a successful capture+write. A Checkpoint error is left to the callback to log; the epoch does not advance so the same epoch is retried next interval (a failed write must not burn an epoch the latest pointer never reached).

type CheckpointStore

type CheckpointStore struct {
	// contains filtered or unexported fields
}

CheckpointStore is the NON-DESTRUCTIVE, versioned room-durability path (design D5, room-hosting spec "Periodic Room Checkpoints") — distinct from the disposing HibernationStore in this package. Where HibernationStore writes a single flat snapshots/<roomID> object and deletes it on restore, the checkpoint store writes monotonic, MAC'd snapshots/<roomID>/<epoch> objects behind a monotonically-advanced latest pointer, so a room can be checkpointed repeatedly while it keeps running and a slow lower-epoch write can never regress the pointer off a later (higher-epoch) drain write.

The payload is opaque to the store: it is the same deterministic snapshot blob the hibernation codec already produces (SnapshotHandler) — the checkpoint path does not invent a new format, it only adds versioned keying + a server-side MAC over whatever bytes it is handed.

PRECONDITION — single writer per room: Write MUST be serialized per roomID (each room is driven by its own actor/scheduler, which is the only thing that checkpoints that room). The store takes no lock and makes no assertion of this — coordination deliberately lives in the actor, not here. The monotonic latest-pointer advance and the never-overwrite-an-epoch check are correct only under this precondition; concurrent writers to one room would race both. Across DIFFERENT rooms Write is safe to call concurrently (the backing Store is).

func NewCheckpointStore

func NewCheckpointStore(store blobstore.Store, sealer blobstore.Sealer) *CheckpointStore

NewCheckpointStore wraps a blobstore.Store with a Sealer that MACs every blob with a server-side key held outside the wasm sandbox. nil store/sealer is a programmer error guarded per-method (matching HibernationStore), never a panic.

func (*CheckpointStore) ReadLatest

func (s *CheckpointStore) ReadLatest(ctx context.Context, roomID string) (payload []byte, epoch int64, err error)

ReadLatest resolves the latest pointer, GETs the checkpoint it names, and VERIFIES the MAC before returning any payload byte (verify-before-write: the restore path writes these bytes into guest memory). A verification failure returns ErrCheckpointCorrupt (wrapping blobstore.ErrSealVerify) and a nil payload — no partial payload escapes. An absent pointer returns ErrNoCheckpoint.

func (*CheckpointStore) Write

func (s *CheckpointStore) Write(ctx context.Context, roomID string, epoch int64, payload []byte) error

Write seals payload, PUTs it at the never-overwritten epoch key snapshots/<roomID>/<epoch>, then advances the latest pointer to that epoch — but only if epoch is higher than the epoch the pointer currently names. That advance is a read-check-then-PUT: readPointerEpoch reads the current pointer, Write compares, and PUTs the new pointer only when this epoch is strictly higher. The single PUT of the pointer object is atomic (S3/Tigris PUT of one object is atomic; the in-memory double matches), so a concurrent READER sees the old pointer or the new one, never a torn value — but that PUT atomicity buys only torn-read safety, NOT the monotonic advance. The monotonic guarantee rests on the single-writer-per-room precondition (see the type doc): with no concurrent writer for this room, the read-compare-PUT sequence cannot race, so a slow lower-epoch Write that completes AFTER a higher-epoch Write reads the higher pointer, fails the compare, and leaves the pointer untouched. This is the "late periodic PUT cannot clobber the drain snapshot" guarantee: the slow write still lands its own (distinct) epoch object, but never regresses the pointer off the higher drain epoch.

An epoch key that already exists is never overwritten in place — but it is not an error either: under the single-writer-per-room precondition the only way the object can exist is OUR OWN earlier attempt at this epoch that was interrupted between the epoch PUT and the pointer advance (e.g. a per-fire timeout). The blob PUT is atomic, so the stored object is a complete, sealed snapshot from that attempt; Write resumes by skipping the payload PUT and finishing the pointer advance, so a retry at the same epoch CONVERGES instead of failing "already written" forever (which would brick the room's periodic cadence AND its drain, both of which retry the unadvanced epoch). The probe is a read-then-PUT; like the pointer advance it is race-free only under the single-writer precondition (it is not an independent guard against a concurrent writer).

type Clock

type Clock interface {
	Now() time.Time
	After(d time.Duration) <-chan time.Time
}

Clock is the scheduler's view of time, injectable for tests. The production implementation (SystemClock) delegates to the stdlib; tests use a controllable fake that fires After channels on demand.

type Header struct {
	Slug   string         // game slug the snapshot belongs to (resume listing + game lookup)
	RoomID string         // the parked room's id (== the storage key suffix)
	At     time.Time      // when the room was hibernated (resume listing "age")
	Roster []RosterMember // who was in the room (resume listing "players" + membership filter)
}

Header is the small, UNCOMPRESSED descriptor prepended to a stored snapshot so the resume metadata (game, age, roster) is readable WITHOUT decompressing the zstd body or touching the wasm runtime. It is also the row shape of the Postgres parked-room directory (add-parked-room-directory) the production resume listing derives from. It deliberately duplicates a few fields the codec also carries (slug, roster) because the codec body is opaque to the store and only the host that owns the matching artifact can decode it — the lobby must list a member's parked rooms without loading every game.

On-wire layout is fixed and self-describing (magic + format), length-prefixed, little-endian — decodable standalone, never via zstd:

u32  magic ("SCH1")
u32  format version
str  game slug
str  room id
i64  hibernated-at unix nanos
u16  roster length, then that many { str accountID; str handle }
... snapshot body (the opaque zstd codec blob) follows the header.

func (Header) Has

func (h Header) Has(accountID string) bool

Has reports whether accountID is in the parked roster.

type HibernationStore

type HibernationStore struct {
	// contains filtered or unexported fields
}

HibernationStore parks frozen rooms in a blobstore.Store under snapshots/. A stored blob is the uncompressed Header followed by the opaque snapshot body; Get returns the body for restore, and a successful restore Deletes the blob (TTL is the bucket's backstop). List exists ONLY for the directory-less legacy path (see its doc): the production resume listing derives from the Postgres parked-room directory (add-parked-room-directory), never from blob enumeration. All ops are context-bound and concurrency-safe (the backing Store is).

INTEGRITY: when constructed with a Sealer, Put MACs the whole header+body blob and Get/List verify it BEFORE decoding the header — a restored snapshot writes the roster and raw guest linear memory into the host, so an unauthenticated blob is a direct write-primitive for anyone who can write the bucket, and the header roster alone gates whose Resume menu a parked room appears in. This is the same HMAC Sealer (same server-side key convention) the versioned checkpoint scheme uses. A nil Sealer keeps the legacy unsealed layout for keyless dev/test stores.

func NewHibernationStore

func NewHibernationStore(store blobstore.Store, sealer blobstore.Sealer) *HibernationStore

NewHibernationStore wraps a blobstore.Store. A nil store yields a no-op-safe zero value? No — callers must pass a real store; nil is a programmer error and every method guards it by returning an error rather than panicking. sealer authenticates parked blobs (see the type doc); nil means unsealed — only acceptable when no server-side MAC key exists (keyless dev mode, in-process test rigs).

MIGRATION (clean cutover): a sealed store REJECTS blobs parked unsealed by an older binary (their trailing bytes are not a MAC), discarding them through the corrupt-blob path. Grandfathering unsealed blobs was deliberately NOT implemented — it would let a bucket writer bypass the MAC by stripping it, and the bucket's 14-day snapshot TTL bounds the loss to rooms parked at deploy time, the same loss policy as prior snapshot format bumps (v2→v3, v3→v4 hard-reject).

func (*HibernationStore) Delete

func (s *HibernationStore) Delete(ctx context.Context, roomID string) error

Delete removes a parked room (called on a successful restore, and to discard a failed/corrupt snapshot). Deleting a missing room is not an error.

func (*HibernationStore) Get

func (s *HibernationStore) Get(ctx context.Context, roomID string) (Header, []byte, bool, error)

Get returns the parked room's header and snapshot body (the opaque codec blob to feed RestoreHandler). ok=false when no snapshot exists for roomID. With a Sealer wired, the seal is verified BEFORE the header is decoded; a failure returns an error wrapping blobstore.ErrSealVerify and the caller MUST refuse the restore (no guest memory is written) and discard the blob like any other corrupt snapshot.

func (*HibernationStore) List

func (s *HibernationStore) List(ctx context.Context) ([]Header, error)

List returns the header of every parked room, newest first.

COST: this is NOT cheap. blobstore.Store.Get returns the WHOLE object, so List downloads every blob under snapshots/ — including every versioned room checkpoint sharing the prefix (snapshots/<roomID>/<epoch>, headerless, fully fetched only to fail the magic check below) — just to decode the small headers. It is retained ONLY for hibernators constructed without a Postgres parked-room directory (in-process test rigs over blobstore.Memory); the production resume listing is a per-account directory query (add-parked-room-directory) and never calls this.

Corrupt or foreign blobs under snapshots/ are skipped (logged by the caller if it cares), never fatal — a single bad object must not hide the rest. With a Sealer wired, a blob that fails seal verification is skipped the same way BEFORE its header is decoded: the header roster gates Resume-list visibility (Header.Has), so an unverified header must never reach a listing.

func (*HibernationStore) Put

func (s *HibernationStore) Put(ctx context.Context, h Header, body []byte) error

Put parks a snapshot: it prepends the header to the body, seals the whole blob when a Sealer is wired, and writes it at snapshots/<header.RoomID>. The header's RoomID is authoritative for the key.

type Metrics

type Metrics interface {
	GameFrameBytesOut(slug string, n int)
	GameInputBytesIn(slug string, n int)
	GameFault(slug string)
	// GameCallback records the host-measured wall-clock duration of one guest
	// callback (the CPU-attribution surface: a spinning game piles into the top
	// bucket right before its deadline kill).
	GameCallback(slug, callback string, seconds float64)
	// GameCallbackDeadline records one callback the kill switch fired on — a
	// spin-to-deadline, distinct from other faults.
	GameCallbackDeadline(slug, callback string)
	// GameHostIODeadline records one callback the kill switch fired on while
	// the guest was blocked in the host's OWN store/config call — a host-I/O
	// incident (slow shared Postgres), deliberately excluded from the fault
	// path so DB slowness never feeds quarantine.
	GameHostIODeadline(slug, callback string)
	// GameKVError records one failed kv/config host call (op: kv_get | kv_set |
	// kv_delete | config_get) — silent dropped writes made visible.
	GameKVError(slug, op string)
	// GameLinearMemoryDelta adjusts the per-game linear-memory gauge by delta
	// bytes. The host samples each room's ACTUAL guest memory size (wazero
	// Memory.Size — never a module-reported figure) on the room heartbeat and
	// reports the change since the room's previous sample, retiring the room's
	// whole contribution when its instance closes, so the gauge is the sum
	// across the game's live rooms with no per-room series.
	GameLinearMemoryDelta(slug string, delta int64)
}

Metrics is the host-side per-game instrumentation surface (implemented by *metrics.Metrics; defined here so gameabi does not import the metrics package). Byte counts are logical (pre-terminal-encoding) frame/input sizes — the per-game attribution numbers, not wire bytes.

type Options

type Options struct {
	Heartbeat        time.Duration // wake cadence (default Heartbeat)
	MemoryPages      uint32        // linear-memory cap in 64KiB pages (default 512 = 32MiB)
	CallbackDeadline time.Duration // per-callback wall-clock kill switch (default 100ms)

	// OnFault, when set, is told the game's slug each time a guest faults
	// (failed instantiation, trap, callback deadline, memory cap). Wire it to
	// Quarantine.RecordFault to remove repeat offenders from the live roster.
	// Called from room actor goroutines; must be safe for concurrent use.
	OnFault func(slug string)

	// Metrics, when set, receives host-measured per-game counters (add-metrics).
	// Every value is measured by the HOST from bytes it moved across the module
	// boundary — never a module-reported figure, so a guest cannot inflate or
	// fabricate a count. Called from room actor goroutines; implementations must
	// be safe for concurrent use. nil ⇒ no recording.
	Metrics Metrics
}

Options bound a loaded game's runtime behavior. Zero values take defaults.

type Quarantine

type Quarantine struct {

	// OnQuarantine, when set, is told the slug of a game the watchdog has just
	// pulled from the live roster, so the catalog can flip its metadata state to
	// quarantined. Called under the watchdog lock from a room-actor goroutine;
	// keep it quick and non-blocking. Optional (nil for non-catalog callers, e.g.
	// dev sideloads).
	OnQuarantine func(slug string)
	// contains filtered or unexported fields
}

Quarantine is the fault-count watchdog: wire its RecordFault into Options.OnFault and a game that faults Threshold times within Window is removed from the live roster (new rooms and lobby listing stop; running rooms are spared — they hold their own Game reference). Removal is admin-reversible via Restore. Every transition is audit-logged.

func NewQuarantine

func NewQuarantine(reg *sdk.Registry, threshold int, window time.Duration, log *slog.Logger) *Quarantine

NewQuarantine builds a watchdog over reg. threshold <= 0 defaults to 3 faults; window <= 0 defaults to 10 minutes.

func (*Quarantine) Quarantined

func (q *Quarantine) Quarantined() []string

Quarantined returns the slugs currently held out of the roster.

func (*Quarantine) RecordFault

func (q *Quarantine) RecordFault(slug string)

RecordFault counts one guest fault for slug and quarantines the game when the in-window count reaches the threshold. Safe from any goroutine (room actors report concurrently).

func (*Quarantine) Restore

func (q *Quarantine) Restore(slug string) error

Restore returns a quarantined game to the live roster with a clean fault count (the admin-reversible half of the contract).

type RosterMember

type RosterMember struct {
	AccountID string
	Handle    string
}

RosterMember is one parked player, enough to filter the resume list to the requesting member and to show who else was in the room.

func RosterFrom

func RosterFrom(roster []sdk.Player) []RosterMember

RosterFrom projects a roster of sdk.Players to header roster members.

type StealMetrics added in v2.14.0

type StealMetrics interface {
	// GameCallbackStealDeadline records one wall-clock callback-deadline kill
	// during which host-stolen CPU advanced — i.e. the VM was being stolen from
	// across the killed callback's window. Recorded ALONGSIDE (never instead of)
	// GameCallbackDeadline. A correlation signal for the future exonerate case,
	// not a kill/quarantine decision.
	GameCallbackStealDeadline(slug, callback string)
}

StealMetrics is a NON-BREAKING optional extension of the Metrics surface. It is deliberately NOT folded into the Metrics interface: doing so would break every existing Metrics implementer (notably the platform's) on the next kit bump. The kill site type-asserts the configured Metrics to StealMetrics and records ONLY if the implementer opts in, so older implementers compile and run unchanged while the seam stays dormant until the platform implements it.

type SystemClock

type SystemClock struct{}

SystemClock is the real-time Clock backed by the stdlib.

func (SystemClock) After

func (SystemClock) After(d time.Duration) <-chan time.Time

func (SystemClock) Now

func (SystemClock) Now() time.Time

Directories

Path Synopsis
Package conformance runs a scripted scenario against a wasm game through the REAL gameabi adapter (limits ON) and reports per-callback latency, exit codes, frames, and peak linear memory, plus budget verdicts that name the breached limit, the measured value, and the step that breached it.
Package conformance runs a scripted scenario against a wasm game through the REAL gameabi adapter (limits ON) and reports per-callback latency, exit codes, frames, and peak linear memory, plus budget verdicts that name the breached limit, the measured value, and the step that breached it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL