mailbox

package
v0.2.7 Latest Latest
Warning

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

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

Documentation

Overview

Package mailbox serializes ownership of a mutable value onto one goroutine.

A Mailbox is the shape both of deploy's harness runtimes converged on independently: state that several callers must read and write, guarded not by a lock but by a single goroutine that runs every access in turn. Callers hand it a Command — a function of a pointer to the state — and the goroutine runs commands one at a time, so no field needs a lock and no two commands ever observe each other half-applied.

The contract

Submission is the serialization point, and the channel is unbuffered deliberately: a caller whose command was accepted knows its work runs next rather than sitting behind an invisible backlog. Submission is also the only place a caller learns the mailbox is gone — Mailbox.Submit reports false rather than blocking forever or panicking, and the caller decides what that means in its own vocabulary.

A command returning true retires the goroutine. Mailbox.Run then stops the mailbox, so a retirement always makes later submissions fail; a command may also call Mailbox.Stop itself, before it returns, when it has state to publish that a caller waiting on Mailbox.Stopped must see. Stop is idempotent precisely so those two paths can both happen.

What it is not

It is not a work queue, a supervisor, or a scheduler. It has no buffering, no retry, no timeout of its own, and no opinion about what the state is or what a command may do with it — including blocking, which stalls every other caller and is the one thing a command must not do without meaning to. The lifecycle above it (starting the goroutine, deciding what retires it, what a failed submission means) stays with the owner, because that is the part the two callers do not agree on.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Command

type Command[State any] func(state *State) bool

Command is one unit of work run on the owning goroutine, holding exclusive access to the state for as long as it runs. Returning true retires the goroutine after this command completes; returning false leaves the mailbox accepting work.

type Mailbox

type Mailbox[State any] struct {
	// contains filtered or unexported fields
}

Mailbox is the submission side of one serialized state owner. The zero value is not usable; construct one with New.

func New

func New[State any]() *Mailbox[State]

New returns a mailbox whose goroutine has not started. The owner starts it by calling Mailbox.Run in a goroutine of its own, which is what lets the owner build the initial state — and anything the state's lifetime is tied to, such as a cancellation function to defer — in that goroutine.

func (*Mailbox[State]) Run

func (mailbox *Mailbox[State]) Run(state *State)

Run owns state until a command retires the mailbox, then stops it. It blocks, and it must be called exactly once, from the one goroutine that is to own the state.

func (*Mailbox[State]) Stop

func (mailbox *Mailbox[State]) Stop()

Stop makes every later submission fail and closes the channel Mailbox.Stopped reports. It is idempotent, and it does not retire the goroutine on its own — only a command returning true does that. A command calls it directly when a caller waiting on Stopped must see state the command has just published.

func (*Mailbox[State]) Stopped

func (mailbox *Mailbox[State]) Stopped() <-chan struct{}

Stopped is closed once the mailbox no longer accepts work. A caller awaiting shutdown selects on it; whatever the owner publishes before calling Mailbox.Stop is visible to everything this unblocks.

func (*Mailbox[State]) Submit

func (mailbox *Mailbox[State]) Submit(command Command[State]) bool

Submit hands one command to the owning goroutine and blocks until it is accepted, reporting false when the mailbox had already stopped. It does not wait for the command to run: a caller that needs the result replies to itself through a channel the command closes over.

A successful rendezvous with the unbuffered command channel is Submit's linearization point. Stop observed before submission begins always wins. If Stop races after submission has begun, either result is valid: an accepted command rendezvoused first, while a refused command observed Stop first.

func (*Mailbox[State]) SubmitContext

func (mailbox *Mailbox[State]) SubmitContext(
	ctx context.Context,
	canceled <-chan struct{},
	command Command[State],
) bool

SubmitContext is Submit with two more ways to give up: ctx being done, and canceled being closed. A nil canceled channel never fires, which is how a caller that has only a context spells "no second cancellation".

It reports false for all three abandonments alike, because the caller already holds the ctx and the channel and can tell them apart better than this package can name them. An abandonment that is already observable when SubmitContext begins always wins over an available command receiver. Once it has begun waiting, the first selected event is the linearization point.

Jump to

Keyboard shortcuts

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