semaphore

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: MIT Imports: 5 Imported by: 0

README

semaphore

CI CodeQL Coverage Mutation Documentation Go Reference Release Go License

semaphore is a process-local, FIFO weighted counting semaphore for Go 1.26.6 and newer. It adds bounded waiting, owned exactly-once permits, deterministic shutdown, immutable snapshots, and bounded observation events to the basic counting-semaphore pattern.

Use it when those lifecycle and policy guarantees are required. For a simple fixed concurrency cap, a buffered channel remains smaller. For admission without ownership, shutdown, or queue observability, consider golang.org/x/sync/semaphore.

Quick start

sem, err := semaphore.New(semaphore.Config{
    Capacity: 4,
    // Zero disables waiting; positive values bound the FIFO queue.
    MaxWaiters: 64,
})
if err != nil {
    return err
}

value, err := semaphore.Execute(ctx, sem, 2,
    func(ctx context.Context) (string, error) {
        return perform(ctx)
    },
)

Execute releases on success, error, and panic, then preserves the returned value and error or the original panic. Manual ownership is explicit:

permit, err := sem.Acquire(ctx, 2)
if err != nil {
    return err
}
defer permit.Release()

The permit remains releasable after ctx is canceled. A second or concurrent duplicate release returns *DuplicateReleaseError and cannot add capacity.

Contract

  • Capacity and weights are positive int64 values. A weight above capacity fails immediately.
  • Acquire admits immediately only when capacity is available and the queue is empty. Otherwise it enters the bounded FIFO queue or returns ErrQueueFull.
  • TryAcquire never waits and never bypasses queued callers. Capacity unavailability is (nil, false, nil); invalid, oversized, and closed states return typed errors.
  • Strict FIFO prevents starvation under the model that admitted permits are eventually released and waiting contexts remain live. A large head waiter intentionally blocks smaller followers. Split large requests or use separate semaphores only when that different policy is explicit and safe.
  • Admission linearizes while the semaphore mutex is held when acquired weight, admission count, and permit identity are assigned. Release linearizes under that mutex when the exactly-once decision and capacity return occur. If cancellation races with a grant, the mutex winner determines the result; a completed grant returns an owned permit.
  • Close is idempotent, rejects new and queued acquisition with ErrClosed, and leaves existing permits valid. Wait(ctx) waits only for acquired weight to return; call Close first to stop admission and reject the queue.
  • No goroutine, timer, finalizer, registry, environment lookup, or distributed coordination is owned by the implementation.

Snapshots contain capacity, acquired and available weight, queued waiters, admissions, rejections, cancellations, and shutdown state. Observers receive immutable low-cardinality events outside the accounting lock. Observer panics are recovered; slow observers delay only the caller delivering that event, and observers must support concurrent calls. Events contain no caller keys, errors, callbacks, context values, or arbitrary labels.

Errors

Use errors.Is for categories and errors.As for bounded details:

  • ErrInvalidConfig / *ConfigError
  • ErrInvalidWeight or ErrOversize / *WeightError
  • ErrQueueFull / *QueueFullError
  • ErrCanceled or ErrDeadline / *CanceledError, also matching the corresponding context error
  • ErrClosed / *ClosedError
  • ErrDuplicateRelease / *DuplicateReleaseError

Validation precedes lifecycle checks, and a context already done when Acquire begins precedes the closed-state check. These paths all return immediately. As with standard Go context APIs, callers must not pass a nil context.

Scope and composition

This package owns only counting and weighted permits. It does not own rate limits, bulkhead identity, adaptive limits, worker pools, queues, retries, breakers, timeouts, fallbacks, locks, leases, or Kubernetes coordination. A bulkhead may compose this narrow permit contract with resource identity and fixed isolation policy; applications should otherwise depend on the smallest consumer-owned interface they need.

The semaphore is process-local and therefore pod-local. With capacity N on R replicas, aggregate in-flight weight can reach N * R. It is not global exclusion. See Kubernetes operations before using local capacity in a replicated workload.

Documentation

Ecosystem

Use the Golib documentation portal to choose companion packages, supported stacks, recipes, and operations guidance.

Documentation

Overview

Package semaphore provides a process-local FIFO weighted semaphore with explicit permit ownership and deterministic shutdown.

Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/faustbrian/go-semaphore"
)

func main() {
	sem, err := semaphore.New(semaphore.Config{Capacity: 4, MaxWaiters: 32})
	if err != nil {
		panic(err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	value, err := semaphore.Execute(ctx, sem, 2, func(context.Context) (string, error) {
		return "finished", nil
	})
	if err != nil {
		panic(err)
	}
	fmt.Println(value, sem.Snapshot().Available)
}
Output:
finished 4

Index

Examples

Constants

View Source
const MaxWaiters = 1_000_000

MaxWaiters is the largest supported bounded FIFO queue.

Variables

View Source
var (
	// ErrInvalidConfig classifies construction failures.
	ErrInvalidConfig = errors.New("semaphore: invalid configuration")
	// ErrDuplicateRelease classifies repeated release of one permit.
	ErrDuplicateRelease = errors.New("semaphore: duplicate permit release")
	// ErrInvalidWeight classifies non-positive acquisition weights.
	ErrInvalidWeight = errors.New("semaphore: invalid weight")
	// ErrOversize classifies weights larger than total capacity.
	ErrOversize = errors.New("semaphore: weight exceeds capacity")
	// ErrQueueFull classifies bounded-waiter saturation.
	ErrQueueFull = errors.New("semaphore: waiter queue full")
	// ErrCanceled classifies acquisition cancellation.
	ErrCanceled = errors.New("semaphore: context canceled")
	// ErrDeadline classifies acquisition deadline expiry.
	ErrDeadline = errors.New("semaphore: context deadline exceeded")
	// ErrClosed classifies admission after deterministic shutdown.
	ErrClosed = errors.New("semaphore: closed")
)

Functions

func Execute

func Execute[T any](ctx context.Context, semaphore *Semaphore, weight int64, operation func(context.Context) (T, error)) (result T, err error)

Execute acquires weight, invokes operation, and releases on success, error, or panic. It preserves the returned value and error or the original panic.

Types

type CanceledError

type CanceledError struct {
	Deadline bool
}

CanceledError distinguishes cancellation from deadline expiry while preserving compatibility with the corresponding context error.

func (*CanceledError) Error

func (err *CanceledError) Error() string

Error returns a stable cancellation diagnostic.

func (*CanceledError) Is

func (err *CanceledError) Is(target error) bool

Is supports package and context cancellation classification.

type ClosedError

type ClosedError struct{}

ClosedError reports that the semaphore no longer accepts work.

func (*ClosedError) Error

func (err *ClosedError) Error() string

Error returns a stable shutdown diagnostic.

func (*ClosedError) Unwrap

func (err *ClosedError) Unwrap() error

Unwrap exposes ErrClosed for errors.Is.

type Config

type Config struct {
	Capacity   int64
	MaxWaiters int
	Observer   Observer
}

Config defines immutable semaphore capacity and queue bounds.

type ConfigError

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

ConfigError describes one invalid, bounded configuration field without retaining arbitrary caller-controlled text.

func (*ConfigError) Error

func (err *ConfigError) Error() string

Error returns a bounded configuration diagnostic.

func (*ConfigError) Field

func (err *ConfigError) Field() ConfigField

Field returns the invalid bounded configuration field.

func (*ConfigError) Problem

func (err *ConfigError) Problem() ConfigProblem

Problem returns the bounded validation problem.

func (*ConfigError) Unwrap

func (err *ConfigError) Unwrap() error

Unwrap exposes ErrInvalidConfig for errors.Is.

type ConfigField

type ConfigField string

ConfigField identifies a bounded configuration field.

const (
	// FieldCapacity identifies Config.Capacity.
	FieldCapacity ConfigField = "capacity"
	// FieldMaxWaiters identifies Config.MaxWaiters.
	FieldMaxWaiters ConfigField = "max waiters"
)

type ConfigProblem

type ConfigProblem string

ConfigProblem identifies a bounded configuration violation.

const (
	// ProblemMustBePositive identifies a required positive value.
	ProblemMustBePositive ConfigProblem = "must be positive"
	// ProblemMustNotBeNegative identifies a required non-negative value.
	ProblemMustNotBeNegative ConfigProblem = "must not be negative"
	// ProblemExceedsBound identifies a value above the supported bound.
	ProblemExceedsBound ConfigProblem = "exceeds the supported bound"
)

type DuplicateReleaseError

type DuplicateReleaseError struct {
	ID PermitID
}

DuplicateReleaseError identifies the permit released more than once.

func (*DuplicateReleaseError) Error

func (err *DuplicateReleaseError) Error() string

Error returns a stable duplicate-release diagnostic.

func (*DuplicateReleaseError) Unwrap

func (err *DuplicateReleaseError) Unwrap() error

Unwrap exposes ErrDuplicateRelease for errors.Is.

type Event

type Event struct {
	Kind     EventKind
	Reason   Reason
	PermitID PermitID
	Weight   int64
	Snapshot Snapshot
}

Event is an immutable, bounded, secret-safe transition snapshot.

type EventKind

type EventKind string

EventKind identifies one bounded semaphore state transition.

const (
	// EventAdmitted reports successful acquisition.
	EventAdmitted EventKind = "admitted"
	// EventQueued reports entry into the bounded FIFO queue.
	EventQueued EventKind = "queued"
	// EventCanceled reports removal from the queue by caller cancellation.
	EventCanceled EventKind = "canceled"
	// EventRejected reports work that did not enter or acquire from the queue.
	EventRejected EventKind = "rejected"
	// EventReleased reports successful exactly-once permit release.
	EventReleased EventKind = "released"
	// EventClosed reports the first shutdown transition.
	EventClosed EventKind = "closed"
)

type Observer

type Observer interface {
	Observe(Event)
}

Observer receives state transitions after accounting locks are released. Implementations must be safe for concurrent calls. Panics are recovered; slow callbacks delay only the caller delivering that event.

type ObserverFunc

type ObserverFunc func(Event)

ObserverFunc adapts a function to Observer.

func (ObserverFunc) Observe

func (observer ObserverFunc) Observe(event Event)

Observe calls observer(event).

type Permit

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

Permit owns acquired weight until Release succeeds exactly once.

func (*Permit) ID

func (permit *Permit) ID() PermitID

ID returns stable process-local identity metadata.

func (*Permit) Release

func (permit *Permit) Release() error

Release returns the permit's weight exactly once. It remains valid after the acquisition context is canceled and is safe for concurrent callers.

func (*Permit) Weight

func (permit *Permit) Weight() int64

Weight returns the acquired weight.

type PermitID

type PermitID uint64

PermitID is stable process-local identity metadata for one admission.

func (PermitID) IsZero

func (id PermitID) IsZero() bool

IsZero reports whether the identifier is unset.

func (PermitID) String

func (id PermitID) String() string

String returns a bounded non-secret diagnostic representation.

type QueueFullError

type QueueFullError struct {
	MaxWaiters int
}

QueueFullError reports deterministic bounded-waiter saturation.

func (*QueueFullError) Error

func (err *QueueFullError) Error() string

Error returns a bounded saturation diagnostic.

func (*QueueFullError) Unwrap

func (err *QueueFullError) Unwrap() error

Unwrap exposes ErrQueueFull for errors.Is.

type Reason

type Reason string

Reason is a bounded, low-cardinality transition reason.

const (
	// ReasonImmediate identifies acquisition without waiting.
	ReasonImmediate Reason = "immediate"
	// ReasonFIFO identifies admission from the FIFO queue.
	ReasonFIFO Reason = "fifo"
	// ReasonUnavailable identifies immediate capacity rejection.
	ReasonUnavailable Reason = "unavailable"
	// ReasonInvalidWeight identifies a non-positive weight.
	ReasonInvalidWeight Reason = "invalid_weight"
	// ReasonOversize identifies a weight above total capacity.
	ReasonOversize Reason = "oversize"
	// ReasonQueueFull identifies bounded queue saturation.
	ReasonQueueFull Reason = "queue_full"
	// ReasonContextCanceled identifies caller cancellation.
	ReasonContextCanceled Reason = "context_canceled"
	// ReasonDeadline identifies caller deadline expiry.
	ReasonDeadline Reason = "deadline"
	// ReasonClosed identifies work rejected by shutdown.
	ReasonClosed Reason = "closed"
	// ReasonReleased identifies successful permit release.
	ReasonReleased Reason = "released"
	// ReasonShutdown identifies the first close operation.
	ReasonShutdown Reason = "shutdown"
)

type Semaphore

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

Semaphore is a process-local weighted counting semaphore.

func New

func New(config Config) (*Semaphore, error)

New constructs a semaphore after validating all configuration.

func (*Semaphore) Acquire

func (semaphore *Semaphore) Acquire(ctx context.Context, weight int64) (*Permit, error)

Acquire acquires positive weight immediately when capacity is available or waits in strict FIFO order. The context must be non-nil.

Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/faustbrian/go-semaphore"
)

func main() {
	sem, err := semaphore.New(semaphore.Config{Capacity: 3, MaxWaiters: 8})
	if err != nil {
		panic(err)
	}
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	permit, err := sem.Acquire(ctx, 2)
	if err != nil {
		panic(err)
	}
	defer func() {
		if err := permit.Release(); err != nil {
			panic(err)
		}
	}()

	fmt.Println(permit.Weight(), sem.Snapshot().Available)
}
Output:
2 1

func (*Semaphore) Close

func (semaphore *Semaphore) Close() error

Close idempotently stops new admission and rejects every queued waiter. Existing permits remain valid and releasable.

func (*Semaphore) Run

func (semaphore *Semaphore) Run(ctx context.Context, weight int64, operation func(context.Context) error) error

Run is the error-only convenience form of Execute.

func (*Semaphore) Snapshot

func (semaphore *Semaphore) Snapshot() Snapshot

Snapshot returns a consistent immutable copy of current state.

func (*Semaphore) TryAcquire

func (semaphore *Semaphore) TryAcquire(weight int64) (*Permit, bool, error)

TryAcquire attempts immediate admission without bypassing queued callers.

func (*Semaphore) Wait

func (semaphore *Semaphore) Wait(ctx context.Context) error

Wait blocks until all acquired weight is returned or ctx is done. It does not close the semaphore or wait for queued callers unless they are admitted.

type Snapshot

type Snapshot struct {
	Capacity      int64
	Acquired      int64
	Available     int64
	Waiters       int
	Admissions    uint64
	Rejections    uint64
	Cancellations uint64
	Closed        bool
}

Snapshot is an immutable copy of observable semaphore state.

type WeightError

type WeightError struct {
	Weight   int64
	Capacity int64
	// contains filtered or unexported fields
}

WeightError describes an invalid or oversized acquisition request.

func (*WeightError) Error

func (err *WeightError) Error() string

Error returns a bounded weight diagnostic.

func (*WeightError) Unwrap

func (err *WeightError) Unwrap() error

Unwrap classifies the invalid weight for errors.Is.

Jump to

Keyboard shortcuts

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