How Fabrica-Util's Closure Primitive Manages Thread-Safe Callbacks in Game Servers

Fabrica-Util's Closure primitive is a synchronization wrapper that guarantees callbacks execute exactly once across concurrent goroutines while preventing data races.

In high-concurrency game server architectures like those built with the go-pantheon ecosystem, managing callbacks safely is critical to avoiding data races and duplicate side effects. The Closure type defined in xsync/closure.go provides a zero-allocation, mutex-protected mechanism for executing arbitrary functions with guaranteed single-fire semantics.

What Is the Closure Primitive?

The Closure primitive is a tiny synchronization wrapper that encapsulates a callback function—either func() error or func(context.Context) error for context-aware variants—and ensures it runs safely even when triggered simultaneously from multiple goroutines. Unlike a naïve function pointer, the Closure struct maintains an internal sync.Mutex, storage for the returned error, and boolean flags to track execution state.

This design isolates concurrency concerns to a single, well-tested component. Rather than re-implementing locking logic in every subsystem, game server components delegate callback safety to Closure, which resides at github.com/go-pantheon/fabrica-util/xsync/closure.go.

Why Game Servers Need Thread-Safe Callbacks

Game servers handling thousands of concurrent connections face specific challenges where unprotected callbacks cause catastrophic failures:

  • Event handling: Player login notifications, match-making updates, and world-tick events must fire exactly once to prevent duplicate database writes or conflicting state updates.
  • Lifecycle hooks: Connection teardown, resource cleanup, and graceful shutdown require idempotent execution to avoid double-closing sockets or leaking goroutines.
  • Asynchronous responses: Background operations like AI calculations must respect cancellation signals when the server initiates shutdown.

Without synchronization, these callbacks risk data races, panic-induced crashes, and duplicated work when network retransmissions or multiple subsystems attempt to trigger the same event simultaneously.

How Closure Guarantees Safe Execution

The implementation in xsync/closure.go solves these problems through several coordinated mechanisms:

Mutex-protected execution: The Do() method acquires an internal sync.Mutex before invoking the wrapped callback. This ensures that state-mutating operations—such as updating a player's session metadata—never run concurrently, eliminating race conditions.

Idempotent single-fire semantics: After the first successful invocation, Closure stores the returned error and a completion flag. Subsequent calls to Do() return the cached result without re-invoking the underlying function. This prevents duplicated side effects when events are published multiple times due to network retransmissions or redundant triggers.

Context awareness: The ClosureCtx variant accepts func(context.Context) error signatures and provides DoCtx(ctx), allowing callbacks to abort early on timeout or cancellation. This enables graceful termination of long-running logic when the server receives a shutdown signal.

Error propagation: The wrapper stores the first error returned by the callback and returns it on every subsequent Do() call. Callers can inspect this result to determine whether to retry operations, log failures, or disconnect clients.

Zero-allocation overhead: The struct consists only of a function field, mutex, error slot, and boolean flags—minimal memory footprint critical for massively multiplayer servers managing millions of connections.

Practical Implementation Examples

Basic Usage Pattern

Create a Closure using the constructor and invoke it safely from multiple goroutines:

package main

import (
    "fmt"
    "time"

    "github.com/go-pantheon/fabrica-util/xsync"
)

func updatePlayerScore() error {
    fmt.Println("updating score...")
    // Database or cache update logic here
    return nil
}

func main() {
    cl := xsync.NewClosure(updatePlayerScore)

    // Fire from five concurrent goroutines—only the first executes
    for i := 0; i < 5; i++ {
        go func(id int) {
            if err := cl.Do(); err != nil {
                fmt.Printf("goroutine %d: %v\n", id, err)
            }
        }(i)
    }

    time.Sleep(100 * time.Millisecond)
}

Context-Aware Execution

For operations requiring cancellation support, use NewClosureCtx with timeout constraints:

ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()

ctxClosure := xsync.NewClosureCtx(func(ctx context.Context) error {
    select {
    case <-time.After(30 * time.Millisecond):
        fmt.Println("work completed")
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
})

if err := ctxClosure.DoCtx(ctx); err != nil {
    fmt.Println("operation aborted:", err)
}

Real-World Game Server Pattern

In a gateway component (similar to the Janus architecture), ensure connections close exactly once:

type ConnHandler struct {
    closeOnce xsync.Closure
}

func NewConnHandler(conn net.Conn) *ConnHandler {
    h := &ConnHandler{}
    h.closeOnce = xsync.NewClosure(func() error {
        return conn.Close()
    })
    return h
}

// Safe to call from read loop, write loop, or shutdown signal
func (h *ConnHandler) Close() error {
    return h.closeOnce.Do()
}

Integration with Higher-Level Utilities

The Closure primitive serves as the foundational building block for fabrica-util's advanced synchronization utilities:

  • Delayer (xsync/delayer.go): Time-based task scheduling relies on Closure to ensure delayed callbacks execute safely when their timer fires.
  • Future (xsync/future.go): Asynchronous result handling uses Closure to wrap completion callbacks, preventing double-completion when multiple goroutines resolve the same future.
  • Routines (xsync/routines.go): Goroutine lifecycle management utilities employ the same mutex patterns pioneered in Closure for coordinated shutdown.

By centralizing callback safety in xsync/closure.go, these higher-level components avoid duplicating concurrency logic while maintaining consistent behavior across the Roma, Janus, Lares, and Senate server architectures.

Summary

  • Fabrica-Util's Closure provides mutex-protected, single-fire callback execution in xsync/closure.go.
  • Do() guarantees the wrapped function runs exactly once regardless of how many goroutines invoke it simultaneously.
  • Context-aware variants (ClosureCtx) support cancellation and timeout propagation for long-running operations.
  • Zero-allocation design minimizes per-connection overhead for high-scale game servers.
  • Error caching allows callers to inspect initial failures while preventing side-effect duplication.
  • Higher-level utilities like Delayer and Future build upon Closure for complex scheduling and async patterns.

Frequently Asked Questions

How does Closure prevent callbacks from running multiple times?

Closure maintains an internal boolean flag that flips to true after the first successful Do() invocation. The method acquires a mutex, checks this flag, and returns the cached result if execution already occurred. This ensures the underlying func() error or func(context.Context) error executes exactly once even when triggered concurrently from thousands of goroutines.

What is the difference between NewClosure and NewClosureCtx?

NewClosure wraps a standard func() error and provides the Do() method, suitable for fire-and-forget operations. NewClosureCtx wraps func(context.Context) error and provides DoCtx(ctx), enabling the callback to respect cancellation signals, timeouts, and deadlines—critical for graceful server shutdowns and preventing resource leaks in long-running computations.

Can Closure handle panics in the wrapped function?

The current implementation in xsync/closure.go does not automatically recover panics. The wrapped callback should implement its own panic recovery if needed. However, the mutex is released via deferred unlock, so even if the wrapped function panics, the lock will not remain held, preventing deadlocks in the calling goroutines.

How does Closure fit into the broader fabrica-util synchronization toolkit?

Closure is the primitive upon which Delayer, Future, and Routines are built. For example, Delayer uses Closure to ensure delayed tasks execute safely when their scheduled time arrives, while Future uses it to guarantee completion callbacks fire exactly once. This layering allows game server developers to use high-level abstractions while trusting that the underlying synchronization is handled consistently and efficiently.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →