GameServer.Lock (GameServer v1.0.637)

Copy Markdown

Serialized execution using database-level advisory locks.

Wraps a function in a Repo.transaction with an advisory lock so that only one process at a time can execute the critical section for a given (namespace, resource_id) pair.

This is useful for game RPCs where multiple players may trigger the same operation concurrently (e.g. guessing a word, claiming a reward) and the logic involves read-modify-write on KV entries or lobby metadata.

How it works

On PostgreSQL, the lock is acquired via pg_advisory_xact_lock and is automatically released when the transaction commits or rolls back. Other callers with the same key block until the lock is available.

On SQLite (dev/test), advisory locks are a no-op because SQLite already serializes all writes at the database level.

Multi-node safety

Because the lock lives in the database, it works correctly across multiple application nodes — all nodes share the same Postgres instance, so the lock is globally consistent.

Namespace conventions

The namespace argument can be:

  • A predefined atom: :lobby (1), :group (2), :party (3)
  • An arbitrary string: hashed to a stable integer, e.g. "word_guessed"

The resource_id is typically the lobby, group, or user id that scopes the lock.

Examples

# Serialize all "word_guessed" RPCs per lobby
GameServer.Lock.serialize("word_guessed", lobby_id, fn ->
  {:ok, entry} = GameServer.KV.get("game_state", lobby_id: lobby_id)
  new_val = Map.update(entry.value, "guessed", [word], &[word | &1])
  GameServer.KV.put("game_state", new_val, %{}, lobby_id: lobby_id)
end)

# Using a predefined atom namespace
GameServer.Lock.serialize(:lobby, lobby_id, fn ->
  # exclusive per-lobby operation
end)

Return value

Returns {:ok, result} where result is the return value of the function, or {:error, reason} if the transaction rolls back.

Summary

Functions

Execute fun inside a transaction with an advisory lock on (namespace, resource_id).

Functions

serialize(namespace, resource_id, fun)

@spec serialize(atom() | String.t(), integer(), (-> result)) ::
  {:ok, result} | {:error, term()}
when result: term()

Execute fun inside a transaction with an advisory lock on (namespace, resource_id).

Only one process at a time can hold the lock for a given key pair. Other callers block until the lock is released (on transaction commit/rollback).

Returns {:ok, result} on success or {:error, reason} on rollback.

Parameters

  • namespace — atom (:lobby, :group, :party) or any string
  • resource_id — integer identifying the specific resource (e.g. lobby id)
  • fun — zero-arity function to execute while holding the lock