latch

package
v0.10.5 Latest Latest
Warning

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

Go to latest
Published: Aug 3, 2026 License: Apache-2.0 Imports: 6 Imported by: 0

Documentation

Overview

Package latch parks callers on a key until something reports that key done.

A Board is the set of keys callers are waiting on. A caller parks with Latch and gets back the status that ended its wait. The owner drives Sweep on its own ticker: each pass hands every latched key to the StatusResolver callback and wakes the callers whose keys come back done.

board, err := latch.New(func(ctx context.Context, keys []string) (map[string]string, error) {
	// Look up keys however you like; report ONLY the ones whose wait is over.
	return done, err
})

go func() {
	for range ticker.C {
		observe(board.Sweep(ctx))
	}
}()

status, err := board.Latch(ctx, key) // blocks until the key is reported done

Two things wake a parked caller: a sweep, and Release for an owner that learns a key is done by some other means. Close ends every wait at shutdown.

The board holds no database handle, spawns no goroutine, picks no cadence, and has no opinion on what a status string means - StatusResolver decides which keys are done, and the ticker decides how often. Nothing needs arming before a key can settle: a key already done when Latch is called is reported by the next sweep, so registration order costs latency at worst.

Latch, Release, Latched and Close are safe to call from anywhere at any time. Sweep is not: drive it from one goroutine.

Index

Constants

This section is empty.

Variables

View Source
var ErrClosed = errors.New("latch board is closed")

ErrClosed is what Latch returns once the board is closed. It carries no status code; a caller mapping errors onto a transport translates it there.

Functions

This section is empty.

Types

type Board

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

Board is the set of keys callers are parked on.

Latch, Release, Latched and Close are safe to call from anywhere at any time. Sweep is NOT safe for concurrent use - drive it from one goroutine, which also means StatusResolver is never re-entered.

func New

func New(resolve StatusResolver) (*Board, error)

New returns an empty board over the callback that reports which keys are done.

func (*Board) Close

func (b *Board) Close()

Close wakes every parked caller with ErrClosed and rejects later Latch calls. It is idempotent, and it does not stop a Sweep already in flight - the owner drives that and stops driving it.

A caller already released keeps its status: shutdown never overwrites an answer that was delivered.

func (*Board) Latch

func (b *Board) Latch(ctx context.Context, key string) (string, error)

Latch parks the caller on one key and returns the status that ended its wait.

It returns when the key is reported done - by a sweep or by a direct Release - and otherwise on ctx, or with ErrClosed once the board is closed. A key already done when Latch is called is reported by the next sweep, so a caller wanting to skip that wait checks the key itself first.

func (*Board) Latched

func (b *Board) Latched() int

Latched is how many distinct keys currently have someone parked on them.

func (*Board) Release

func (b *Board) Release(key string, status string) int

Release wakes every caller parked on one key and reports how many there were. It is the direct path, for a caller that learns a key is done by some means other than a sweep; a sweep releases through it too. Releasing a key nobody holds is a no-op.

func (*Board) SetOnPark

func (b *Board) SetOnPark(fn func(key string))

SetOnPark registers a callback fired by Board.Latch once a caller is ON the board and about to block, or clears it when nil. It is the observation point a caller outside this package cannot reach for itself: registering and blocking are one call here - deliberately, since a gap between them is exactly the race Close is careful not to leave - so "the caller is parked" has no other moment.

It exists for the one thing a duration cannot state. Registration order is not a correctness question (the board is polled, so a key that settles before its caller arrives is reported by the next sweep), but it IS the premise of any test about a caller that was ALREADY blocked when the key settled - and left to a sleep, that premise silently degrades into its opposite on a slow machine: the caller registers late, its own first read answers it, and the test passes having exercised nothing.

The callback runs on the parking goroutine while it holds no lock of this package's, so it may do as it likes; a board with none set pays one atomic load per Latch. It is NOT called for a Latch turned away by a closed board, which never parks.

func (*Board) Sweep

func (b *Board) Sweep(ctx context.Context) Result

Sweep runs one detector pass: it asks StatusResolver about every latched key and releases the ones that come back done. With nothing latched it does not call StatusResolver at all, so an idle board is free.

It never returns an error, only a Result carrying one - a failed pass needs no decision from the caller, since the next sweep asks again.

func (*Board) Waiting

func (b *Board) Waiting(key string) int

Waiting is how many callers are parked on one key.

type Result

type Result struct {
	// Latched is how many distinct keys the sweep asked about, Reported how many of those came back done,
	// and Released how many parked callers were woken (one key can hold several).
	Latched  int
	Reported int
	Released int

	// Duration spans the whole pass, StatusResolver included.
	Duration time.Duration

	// Err is what StatusResolver returned. Any releases it reported alongside the error have already been
	// applied; this is for the log line.
	Err error
}

Result is one sweep's outcome, for logging and metrics. Nothing in it is a control signal: read it and carry on.

type StatusResolver

type StatusResolver func(ctx context.Context, keys []string) (map[string]string, error)

StatusResolver reports which of the given keys are done waiting, and with what status.

A key it OMITS stays latched, so reporting nothing is how to say "still waiting". It may report a key that is not in keys; that release simply finds no waiter.

It is called with at least one key and never concurrently with itself, and is free to chunk, pad, group or route the keys however it needs. On error it may still return whatever it did resolve: those releases are applied, and the keys it did not report are asked about again on the next sweep.

Jump to

Keyboard shortcuts

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