Fabrica-Util XSync Synchronization Primitives: Stopper, Future, and Delayer Explained
The xsync package in go-pantheon/fabrica-util provides four core synchronization primitives—Stopper for graceful shutdown coordination, generic Future[T] for promise-style async results, Delayer for resettable timers, and panic-safe goroutine helpers—to manage complex goroutine lifecycles without heavyweight frameworks.
The xsync package (located at xsync/ in the go-pantheon/fabrica-util repository) offers lightweight, composable synchronization primitives designed for production Go applications. These tools handle common concurrency patterns—coordinated shutdown, deferred value retrieval, and timed scheduling—while eliminating repetitive boilerplate for panic recovery and structured logging. Understanding when to use each primitive helps you build resilient services that gracefully manage goroutine orchestration.
Stopper: Coordinated Graceful Shutdown
The Stopper type (defined in xsync/stopper.go) implements a finite state machine (transiting through stateIdle → stateTriggered → stopping → stateStopped) that orchestrates clean shutdown across multiple goroutines. It provides a centralized mechanism to broadcast stop signals while enforcing timeout boundaries.
Key capabilities include:
StopTriggered()– Returns a receive-only channel that closes once shutdown begins, allowing workers to select on stop signals.TurnOff(fn)– Executes a user-supplied shutdown function with a configurable deadline, returningErrTurnOffTimeoutif execution exceeds the limit.- Lifecycle-aware goroutine launchers –
Go,GoWaitStop, andGoAndStopstart workers that automatically respect the stopper's lifecycle and capture panics.
When to use Stopper: Deploy this primitive for any long-running service (HTTP servers, background worker pools, or message consumers) that must react to SIGTERM/SIGINT signals. It ensures in-flight requests complete or abort cleanly within defined boundaries rather than terminating abruptly.
// Create a stopper that aborts shutdown after 5 seconds
stopper := xsync.NewStopper(5 * time.Second)
// Start a worker that respects the stop signal
stopper.Go("worker.loop", func() error {
for {
select {
case <-stopper.StopTriggered():
// Perform cleanup before exiting
return nil
default:
// Regular work execution
time.Sleep(100 * time.Millisecond)
}
}
})
// Trigger shutdown from signal handler or admin endpoint
if err := stopper.Stop(context.Background()); err != nil {
slog.Error("shutdown failed", "error", err)
}
<-stopper.WaitStopped() // Block until all workers finish
Future[T]: Type-Safe Asynchronous Results
The Future[T] type (implemented in xsync/future.go) provides a generic promise-style container for values produced by concurrent operations. Unlike channels that require continuous polling, Future offers one-shot completion semantics with multiple retrieval options.
Core API surface:
Complete(value, err)– Idempotently sets the result exactly once; subsequent calls have no effect.Get()– Blocks indefinitely until completion.GetWithContext(ctx)andGetWithTimeout(d)– Provide cancellation and deadline propagation.Cancel()– Marks the future as cancelled, causing retrievers to receiveErrFutureCancelled.
When to use Future: Employ this primitive when a caller needs a value computed by another goroutine (such as database lookups, remote RPC calls, or background calculations) without blocking indefinitely. It supports multiple concurrent waiters, making it ideal for fan-out/fan-in patterns where several consumers await the same result.
// Create a typed future for integer results
f := xsync.NewFuture[int]()
// Produce the value asynchronously
xsync.Go("calc", func() error {
time.Sleep(200 * time.Millisecond)
f.Complete(42, nil)
return nil
})
// Consumer waits with timeout context
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
value, err := f.GetWithContext(ctx)
if err != nil {
slog.Error("future failed", "error", err)
} else {
slog.Info("result received", "value", value)
}
Delayer: Resettable Expiry Timers
The Delayer type (located in xsync/delayer.go) implements the Delayable and WorkerDelayable interfaces to provide a resettable timer that can be extended or cancelled after creation. Unlike standard time.Timer instances that fire once and cannot be reused reliably, Delayer supports dynamic deadline adjustments.
Interface methods include:
SetExpiryTime(t)– Schedules or reschedules the timer to fire at the specifiedtime.Time.Reset()– Clears the current timer and marks the delayer as stopped.Close()– Permanently stops the timer and releases resources.Wait()– Returns a channel that receives a signal upon expiry.IsExpired()andTimeRemaining()– Query the current timer state.
When to use Delayer: Choose this primitive for scenarios requiring mutable deadlines, such as exponential backoff retry logic, debouncing rapid user input events, or scheduling one-off tasks that may need rescheduling before execution.
delayer := xsync.NewDelayer()
// Debounce: reset timer to 300ms on each event
handleEvent := func() {
delayer.SetExpiryTime(time.Now().Add(300 * time.Millisecond))
}
// Wait for debounce to fire
xsync.Go("debounce.wait", func() error {
<-delayer.Wait()
slog.Info("debounce fired - processing aggregated events")
return nil
})
// Simulate rapid incoming events
for i := 0; i < 5; i++ {
handleEvent()
time.Sleep(100 * time.Millisecond)
}
Utility Helpers: Panic-Safe Goroutine Management
The xsync package includes runtime utilities (defined in xsync/routines.go) that wrap goroutine execution with standardized panic recovery, structured logging, and timeout enforcement.
Available helpers:
Go(msg, fn, filters...)– Launchesfnin a new goroutine, recovers from panics, logs errors with the provided message context, and optionally filters expected errors from logs.Run(fn)– Executesfnsynchronously with panic recovery, returning any recovered error.Timeout(ctx, msg, fn, d, filters...)– RunsfnviaGobut aborts if execution exceeds durationdor the context deadline.RoutineID()– Parses the runtime stack to extract the current goroutine ID for debug correlation.
When to use these helpers: Integrate these wrappers whenever you launch background work to eliminate repetitive recover/defer boilerplate. They integrate seamlessly with Stopper (via stopper.Go) and provide consistent observability across your application's goroutine fleet.
// Execute with hard timeout
err := xsync.Timeout(
context.Background(),
"critical.task",
func() error {
// Simulate long-running operation
time.Sleep(5 * time.Second)
return nil
},
2*time.Second,
)
if err != nil {
// Captures timeout or panic information
slog.Error("task failed", "error", err)
}
Summary
Stopper(xsync/stopper.go) orchestrates graceful shutdown via state-machine-driven lifecycle management with timeout enforcement.Future[T](xsync/future.go) delivers type-safe, promise-style asynchronous results supporting context cancellation and multiple waiters.Delayer(xsync/delayer.go) provides resettable, queryable timers ideal for backoff strategies and debouncing.- Routines (
xsync/routines.go) offer panic-protected goroutine launching with structured logging and deadline enforcement throughGo,Run, andTimeout.
Frequently Asked Questions
How does Stopper enforce shutdown timeouts?
The Stopper enforces timeouts through the TurnOff method and internal deadline tracking established via NewStopper(duration). If a shutdown function exceeds the specified duration, the method returns ErrTurnOffTimeout, ensuring that stuck workers cannot block application termination indefinitely.
Can multiple goroutines wait on a single Future value?
Yes, Future[T] supports multiple concurrent consumers. Any number of goroutines can call Get, GetWithContext, or GetWithTimeout on the same Future instance. Once Complete is called, all waiting goroutines receive the result value or error simultaneously.
What distinguishes Delayer from a standard time.Timer?
While time.Timer fires once at a fixed deadline and cannot be safely reset after expiry without potential races, Delayer provides the SetExpiryTime method to dynamically adjust deadlines and the Reset method to cancel pending timers cleanly. This reset capability makes Delayer appropriate for debouncing where the trigger time shifts based on incoming events.
How does the Go helper recover from panics?
The Go function (and by extension Timeout) uses defer with recover to catch panics within the launched goroutine. It logs the panic details using the provided message string and optional error filters, then converts the panic into a returned error without crashing the application. This pattern ensures that background workers fail safely and observably.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →