Help us shape the future of Go! Take the 2026 Go Developer Survey

workflow

package
v1.9.3 Latest Latest
Warning

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

Go to latest
Published: Oct 26, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package workflow defines typed workflow definitions, runs, step checkpointing, and the workflow store interface.

Workflows are durable, multi-step functions. They survive process restarts by checkpointing completed steps. A step that already completed is skipped on resume; only the failed or pending step re-executes.

Defining a Workflow

var ProcessOrder = workflow.NewWorkflow("process_order",
    func(wf *workflow.Workflow, input OrderInput) error {
        if err := wf.Step("validate", func(ctx context.Context) error {
            return validateOrder(ctx, input)
        }); err != nil {
            return err
        }

        if err := wf.Step("charge", func(ctx context.Context) error {
            return chargeCard(ctx, input.PaymentToken, input.Amount)
        }); err != nil {
            return err
        }

        return wf.Step("fulfill", func(ctx context.Context) error {
            return fulfillOrder(ctx, input.OrderID)
        })
    },
)

Parallel Steps

Use Workflow.Parallel to run independent steps concurrently. The first argument names the group, so the checkpoint survives a restart:

err := wf.Parallel("notify",
    func(ctx context.Context) error { return sendConfirmationEmail(ctx, input.Email) },
    func(ctx context.Context) error { return updateInventory(ctx, input.Items) },
)

Waiting for Events

A workflow can pause until an external event arrives:

evt, err := wf.WaitForEvent("payment.confirmed", 24*time.Hour)

State Machine

A Run moves through these states:

running -> completed
running -> failed

Key Types

  • Definition: typed workflow descriptor with Name and Handler
  • Run: a single workflow execution record
  • RunState: running, completed, or failed
  • Registry: maps workflow names to type-erased runner functions

Package workflow defines workflow definitions, runs, steps, checkpoints, and the workflow store interface.

Index

Constants

This section is empty.

Variables

View Source
var ErrRunnerShutdown = errors.New("workflow: runner is shut down")

ErrRunnerShutdown is what ReplayFrom returns once Shutdown has begun. The refusal comes before the run is claimed, so the run is left exactly as it was.

Functions

func CompareCheckpoints

func CompareCheckpoints(a, b *Checkpoint) int

CompareCheckpoints orders checkpoints by creation time, then ID. Timeline, replay preview and storage pruning use the same tie boundary.

func FanOut

func FanOut[T, R any](w *Workflow, name string, inputs []T) (_ []R, err error)

FanOut starts N child workflows in parallel and collects all results. Each child is independently checkpointed via RunChild. If any child fails, the remaining are cancelled and the error is returned.

func RegisterDefinition

func RegisterDefinition[T any](r *Registry, def *Definition[T])

RegisterDefinition registers a typed workflow definition. The generic handler is wrapped in a closure that JSON-unmarshals the input into T before calling the typed handler.

If Version is 0 (default), it is treated as version 1. Multiple versions of the same workflow name can coexist.

This is a package-level generic function because Go does not allow generic methods on non-generic receiver types.

func RunChild

func RunChild[T, R any](w *Workflow, name string, input T) (_ R, err error)

RunChild starts a child workflow and blocks until it completes, returning the decoded result. The child run is linked to the parent via ParentRunID and the result is checkpointed for crash recovery.

T is the input type, R is the result type (decoded from the child's JSON output).

func SpawnChild

func SpawnChild[T any](w *Workflow, name string, input T) (_ id.RunID, err error)

SpawnChild starts a child workflow asynchronously (fire-and-forget) and returns the child's run ID. The child run is linked to the parent via ParentRunID and executes in a background goroutine. Use the Store's GetRun to check on its status later.

func StepWithResult

func StepWithResult[T any](w *Workflow, name string, fn func(ctx context.Context) (T, error)) (_ T, err error)

StepWithResult executes a named step that returns a typed value. The result is serialized via encoding/gob and saved as a checkpoint. On resume, the cached result is deserialized and returned without re-executing the step function.

This is a package-level generic function because Go does not allow generic methods on non-generic receiver types.

func StepWithResultAndCompensation

func StepWithResultAndCompensation[T any](
	w *Workflow,
	name string,
	execute func(ctx context.Context) (T, error),
	compensate func(ctx context.Context) error,
) (T, error)

StepWithResultAndCompensation executes a named step that returns a typed value, with an associated compensation function. If the step succeeds, the result is checkpointed and the compensation is registered on the stack.

Types

type Checkpoint

type Checkpoint struct {
	ID        id.CheckpointID `json:"id"`
	RunID     id.RunID        `json:"run_id"`
	StepName  string          `json:"step_name"`
	Data      []byte          `json:"data"`
	CreatedAt time.Time       `json:"created_at"`
}

Checkpoint stores the serialized state of a completed workflow step, enabling crash recovery by replaying from the last checkpoint.

type ChildStarter

type ChildStarter interface {
	// StartChildRaw starts a child workflow synchronously, blocking until it completes.
	// The child run's ParentRunID is set to the given parent.
	StartChildRaw(ctx context.Context, parentRunID id.RunID, name string, input []byte) (*Run, error)

	// SpawnChildRaw starts a child workflow asynchronously, returning immediately
	// after the run is persisted. The child executes in a background goroutine.
	SpawnChildRaw(ctx context.Context, parentRunID id.RunID, name string, input []byte) (*Run, error)
}

ChildStarter is the interface used by the Workflow to start child workflow runs. It is satisfied by the workflow.Runner, injected by the engine layer to break import cycles.

type Compensation

type Compensation struct {
	StepName   string
	Compensate func(ctx context.Context) error
}

Compensation represents a deferred undo action for a completed step.

type CountRunsOpts

type CountRunsOpts struct {
	State RunState
	Name  string
}

CountRunsOpts filters CountRuns. Name is an exact workflow name; empty State or Name counts every value.

func (CountRunsOpts) Match

func (o CountRunsOpts) Match(r *Run) bool

Match reports whether r is counted under o.

type Definition

type Definition[T any] struct {
	// Name is the unique identifier for this workflow type.
	Name string

	// Version is the definition version (default 0 = version 1).
	// Multiple versions of the same workflow can be registered simultaneously.
	// New runs use the latest version; existing runs continue on their version.
	Version int

	// Handler is the function that executes the workflow logic.
	// It receives a *Workflow which provides Step, Parallel,
	// WaitForEvent, and Sleep methods.
	Handler func(wf *Workflow, input T) error
}

Definition is a typed workflow definition with a handler function. T is the input type (must be JSON-serializable for Run.Input storage).

func NewWorkflow

func NewWorkflow[T any](name string, handler func(wf *Workflow, input T) error) *Definition[T]

NewWorkflow creates a typed workflow definition (version 1).

func NewWorkflowV

func NewWorkflowV[T any](name string, version int, handler func(wf *Workflow, input T) error) *Definition[T]

NewWorkflowV creates a typed workflow definition with an explicit version.

type ListOpts

type ListOpts struct {
	// Limit is the maximum number of runs to return. Zero means no limit.
	Limit int
	// Offset is the number of runs to skip.
	Offset int
	// State filters by run state. Empty means all states.
	State RunState
}

ListOpts controls pagination for workflow run list queries.

type ListRunsPageOpts

type ListRunsPageOpts struct {
	State      RunState
	NamePrefix string
	ScopeAppID string
	ScopeOrgID string
	Cursor     string
	Limit      int
}

ListRunsPageOpts filters and pages ListRunsPage. The rules are the same as job.ListJobsOpts: exact matches except NamePrefix, empty means no filter, newest first by ID.

func (ListRunsPageOpts) Match

func (o ListRunsPageOpts) Match(r *Run) bool

Match reports whether r passes every filter in o.

type PageLister

type PageLister interface {
	ListRunsPage(ctx context.Context, opts ListRunsPageOpts) (RunPage, error)
	CountRuns(ctx context.Context, opts CountRunsOpts) (int64, error)
}

PageLister is the paged run read and the run count the dashboard uses. ListRuns is kept, unchanged, for the engine's own callers.

type Registry

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

Registry maps workflow names to versioned runner functions. Multiple versions of the same workflow can be registered; the latest version is used for new runs. It is safe for concurrent use.

func NewRegistry

func NewRegistry() *Registry

NewRegistry creates an empty workflow registry.

func (*Registry) Get

func (r *Registry) Get(name string) (RunnerFunc, bool)

Get returns the latest-version runner for the given workflow name. Returns false if no runner is registered.

func (*Registry) GetVersion

func (r *Registry) GetVersion(name string, version int) (RunnerFunc, bool)

GetVersion returns the runner for a specific version of a workflow. If version <= 0, behaves like Get (returns latest).

func (*Registry) LatestVersion

func (r *Registry) LatestVersion(name string) int

LatestVersion returns the highest registered version number for a workflow. Returns 0 if the workflow is not registered.

func (*Registry) Names

func (r *Registry) Names() []string

Names returns all registered workflow names.

func (*Registry) Versions

func (r *Registry) Versions(name string) []int

Versions returns the registered versions in ascending order.

type Reopener

type Reopener interface {
	// ReopenRun sets state = running, error = "" and completed_at = null
	// (and stamps updated_at) only if the run is not already running and
	// ReplayGeneration equals expectedGeneration. It increments the generation
	// atomically. When the run is running or the generation changed it returns
	// an error wrapping dispatch.ErrInvalidState that names the state;
	// when the run does not exist, dispatch.ErrRunNotFound. The check and
	// the write are one atomic step: of two concurrent reopens, exactly
	// one wins.
	ReopenRun(ctx context.Context, runID id.RunID, expectedGeneration int64) error
}

Reopener is the conditional claim that makes replay-from-step safe.

Replaying a run from a step re-executes it under the same run ID, so two replays started together would run the tail of the workflow twice, side by side. The claim is the state change itself: only one caller can move a run out of a finished state into running.

type ReplayPlan

type ReplayPlan struct {
	RunID    id.RunID `json:"run_id"`
	FromStep string   `json:"from_step"`

	// Version is the run's stamped version, the one the replay runs on.
	// A run stamped 0 predates versioning and runs on version 1.
	Version int `json:"version"`

	// Reruns names the checkpointed steps after FromStep whose
	// checkpoints the replay deletes, so they run again, in checkpoint
	// order. Steps the run never reached are not listed but run too.
	Reruns []string `json:"reruns"`

	// State is the run's state when the plan was made. ReplayFrom
	// refuses a running run, so a caller can refuse it early from here.
	State RunState `json:"state"`

	// Generation fences the claim against the run used for this plan.
	Generation int64 `json:"generation"`
}

ReplayPlan says what replaying a run from a step does. PlanReplay returns it without changing anything; ReplayFrom returns the plan it acted on.

type Run

type Run struct {
	dispatch.Entity

	ID          id.RunID   `json:"id"`
	Name        string     `json:"name"`
	State       RunState   `json:"state"`
	Input       []byte     `json:"input,omitempty"`
	Output      []byte     `json:"output,omitempty"`
	Error       string     `json:"error,omitempty"`
	ScopeAppID  string     `json:"scope_app_id,omitempty"`
	ScopeOrgID  string     `json:"scope_org_id,omitempty"`
	StartedAt   time.Time  `json:"started_at"`
	CompletedAt *time.Time `json:"completed_at,omitempty"`

	// Version is the workflow definition version this run executes.
	// Zero means version 1 (unversioned).
	Version int `json:"version,omitempty"`

	// ReplayGeneration advances on each successful replay claim. A plan
	// must still match this generation when it reopens the run.
	ReplayGeneration int64 `json:"replay_generation"`

	// ParentRunID links this run to its parent (nil for top-level runs).
	ParentRunID *id.RunID `json:"parent_run_id,omitempty"`
}

Run represents a single execution of a workflow.

func Start

func Start[T any](ctx context.Context, runner *Runner, name string, input T) (*Run, error)

Start starts a new workflow run with a typed input. The input is JSON-marshaled and stored on the Run.

type RunEmitter

type RunEmitter interface {
	StepEmitter
	EmitWorkflowStarted(ctx context.Context, run *Run)
	EmitWorkflowCompleted(ctx context.Context, run *Run, elapsed time.Duration)
	EmitWorkflowFailed(ctx context.Context, run *Run, err error)
}

RunEmitter emits workflow-level lifecycle events. This interface is satisfied by ext.Registry (via an adapter in the engine package) to break the import cycle between workflow and ext.

type RunGetter

type RunGetter interface {
	GetRun(ctx context.Context, runID id.RunID) (*Run, error)
}

RunGetter retrieves a workflow run by ID. Satisfied by workflow.Store.

type RunPage

type RunPage struct {
	Runs       []*Run
	NextCursor string
	Complete   bool
}

RunPage is one page of ListRunsPage. NextCursor and Complete mean what they mean on job.Page.

type RunState

type RunState string

RunState represents the lifecycle state of a workflow run.

const (
	// RunStateRunning means the workflow is currently executing.
	RunStateRunning RunState = "running"
	// RunStateCompleted means the workflow finished successfully.
	RunStateCompleted RunState = "completed"
	// RunStateFailed means the workflow failed terminally.
	RunStateFailed RunState = "failed"
)

type Runner

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

Runner orchestrates workflow execution: creating runs, building the Workflow context, invoking handlers, and managing state.

func NewRunner

func NewRunner(
	registry *Registry,
	store Store,
	eventStore event.Store,
	emitter RunEmitter,
	logger log.Logger,
) *Runner

NewRunner creates a workflow runner.

func (*Runner) GetTimeline

func (r *Runner) GetTimeline(ctx context.Context, runID id.RunID) ([]TimelineEntry, error)

GetTimeline returns an ordered timeline of all steps executed in a workflow run. Each entry corresponds to a checkpoint, sorted by creation time. This enables point-in-time inspection of workflow state.

func (*Runner) InspectStep

func (r *Runner) InspectStep(ctx context.Context, runID id.RunID, stepName string) ([]byte, error)

InspectStep returns the raw checkpoint data for a specific step in a workflow run. This allows decoding and examining the result that was saved when the step completed.

func (*Runner) MigrateRun

func (r *Runner) MigrateRun(ctx context.Context, runID id.RunID, toVersion int) error

MigrateRun upgrades a workflow run to a new version. It updates the run's Version field and re-executes on the new handler. Checkpoints are preserved so completed steps are skipped during replay on the new version's handler.

func (*Runner) PlanReplay

func (r *Runner) PlanReplay(ctx context.Context, runID id.RunID, fromStep string) (*ReplayPlan, error)

PlanReplay reports what ReplayFrom would do for the run and step, without changing anything. It fails when the run does not exist (wrapping dispatch.ErrRunNotFound), when fromStep has no checkpoint, or when the run's stamped version is not registered (both wrapping dispatch.ErrInvalidState). A running run plans fine; its State says ReplayFrom would refuse it.

func (*Runner) Registry

func (r *Runner) Registry() *Registry

Registry returns the workflow registry.

func (*Runner) ReplayFrom

func (r *Runner) ReplayFrom(ctx context.Context, runID id.RunID, fromStep string) (*ReplayPlan, error)

ReplayFrom re-runs a finished run from a step. Checkpoints up to and including fromStep are kept, so those steps are skipped; every later checkpoint is deleted and those steps run again. The run executes on its stamped version, never a newer one registered since.

The refusals are PlanReplay's, plus: a running run, or one another replay claims first, wraps dispatch.ErrInvalidState; once Shutdown has begun, ErrRunnerShutdown. Of two concurrent calls exactly one starts.

The run executes on the runner's background launcher and ReplayFrom returns the plan as soon as it has started. The run keeps the values of ctx but not its cancellation: Shutdown cancels it instead.

func (*Runner) ReplayFromGeneration

func (r *Runner) ReplayFromGeneration(ctx context.Context, runID id.RunID, fromStep string, generation int64) (*ReplayPlan, error)

ReplayFromGeneration requires the generation returned by PlanReplay. A stale confirmation is refused before claiming or pruning the run.

func (*Runner) Resume

func (r *Runner) Resume(ctx context.Context, runID id.RunID) error

Resume resumes a workflow run that was in "running" state (crash recovery). It re-executes the handler; steps with checkpoints are skipped automatically. The run continues on its stamped version (not necessarily the latest).

func (*Runner) ResumeAll

func (r *Runner) ResumeAll(ctx context.Context) error

ResumeAll finds all runs in "running" state and resumes them. Called at startup for crash recovery.

func (*Runner) Shutdown

func (r *Runner) Shutdown(ctx context.Context) error

Shutdown stops the background launcher. It refuses every later ReplayFrom with ErrRunnerShutdown, cancels the context the in-flight background runs execute under, and waits for them to return or for ctx to expire, whichever is first. On expiry it returns an error wrapping ctx.Err() and leaves the runs to finish on their own.

A run whose steps honour cancellation fails with the context error. On a durable store the write recording that usually fails too, since it shares the cancelled context, so the run stays running and the next ResumeAll picks it up from its checkpoints.

Shutdown is safe to call more than once; a later call waits again.

func (*Runner) SpawnChildRaw

func (r *Runner) SpawnChildRaw(ctx context.Context, parentRunID id.RunID, name string, input []byte) (*Run, error)

SpawnChildRaw starts a child workflow asynchronously, returning immediately after the run is persisted. The child executes in a background goroutine. Implements ChildStarter.

func (*Runner) StartChildRaw

func (r *Runner) StartChildRaw(ctx context.Context, parentRunID id.RunID, name string, input []byte) (*Run, error)

StartChildRaw starts a child workflow synchronously, blocking until it completes. The child run's ParentRunID is set to the given parent. Implements ChildStarter.

func (*Runner) StartRaw

func (r *Runner) StartRaw(ctx context.Context, name string, input []byte) (*Run, error)

StartRaw starts a workflow run with pre-serialized JSON input. The run is stamped with the latest registered version.

func (*Runner) WithTelemetry

func (r *Runner) WithTelemetry(t *telemetry.Telemetry) *Runner

WithTelemetry sets where the runner records its spans and metrics: a "run workflow" span per run, a "run workflow step" span per step that actually runs (a step replayed from its checkpoint gets none), and the run and step duration histograms. It returns r so callers can chain it onto NewRunner. engine.Build passes the engine's own.

type RunnerFunc

type RunnerFunc func(wf *Workflow, input []byte) error

RunnerFunc is a type-erased workflow runner that accepts raw JSON input. The typed Definition[T] is converted to a RunnerFunc at registration time by closing over JSON unmarshal + the typed handler.

type StepEmitter

type StepEmitter interface {
	EmitStepCompleted(ctx context.Context, run *Run, stepName string, elapsed time.Duration)
	EmitStepFailed(ctx context.Context, run *Run, stepName string, err error)
}

StepEmitter is called by the Workflow to emit step lifecycle events. This interface is satisfied by ext.Registry (via an adapter in the runner package) to break the import cycle between workflow and ext.

type Store

type Store interface {
	Reopener

	// CreateRun persists a new workflow run.
	CreateRun(ctx context.Context, run *Run) error

	// GetRun retrieves a workflow run by ID.
	GetRun(ctx context.Context, runID id.RunID) (*Run, error)

	// UpdateRun persists changes to an existing workflow run. It refuses a
	// ReplayGeneration that no longer matches with dispatch.ErrInvalidState,
	// so an old execution cannot overwrite a later replay's claim.
	UpdateRun(ctx context.Context, run *Run) error

	// ListRuns returns workflow runs matching the given options.
	ListRuns(ctx context.Context, opts ListOpts) ([]*Run, error)

	// SaveCheckpoint persists checkpoint data for a workflow step.
	// If a checkpoint already exists for the same run/step, it is replaced.
	SaveCheckpoint(ctx context.Context, runID id.RunID, stepName string, data []byte) error

	// GetCheckpoint retrieves checkpoint data for a specific workflow step.
	// Returns nil data if no checkpoint exists.
	GetCheckpoint(ctx context.Context, runID id.RunID, stepName string) ([]byte, error)

	// ListCheckpoints returns checkpoints in ascending creation-time/ID order.
	ListCheckpoints(ctx context.Context, runID id.RunID) ([]*Checkpoint, error)

	// ListChildRuns returns all child workflow runs for a parent.
	ListChildRuns(ctx context.Context, parentRunID id.RunID) ([]*Run, error)

	// DeleteCheckpointsAfter removes checkpoints strictly after the named
	// step in creation-time/ID order, preserving the target and earlier ties.
	DeleteCheckpointsAfter(ctx context.Context, runID id.RunID, afterStep string) error
}

Store defines the persistence contract for workflows.

type TimelineEntry

type TimelineEntry struct {
	StepName  string    `json:"step_name"`
	Data      []byte    `json:"data,omitempty"`
	CreatedAt time.Time `json:"created_at"`
}

TimelineEntry represents a single step in a workflow's execution history. The timeline is ordered by checkpoint creation time, providing a chronological view of how the workflow executed.

type Workflow

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

Workflow is the execution context passed to workflow handler functions. It provides durable step execution, parallel fan-out, event waiting, durable sleep, saga compensations, and child workflow composition. Each method automatically checkpoints results to enable crash recovery.

func NewWorkflowContext

func NewWorkflowContext(
	ctx context.Context,
	run *Run,
	store Store,
	eventStore event.Store,
	emitter StepEmitter,
	logger log.Logger,
) *Workflow

NewWorkflowContext creates a new Workflow execution context. This is called by the workflow runner, not by users.

func (*Workflow) Compensations

func (w *Workflow) Compensations() []Compensation

Compensations returns the list of registered compensations (for runner use).

func (*Workflow) Context

func (w *Workflow) Context() context.Context

Context returns the underlying context.Context.

func (*Workflow) Parallel

func (w *Workflow) Parallel(groupName string, steps ...func(ctx context.Context) error) (err error)

Parallel executes multiple step functions concurrently using errgroup. If any step fails, the others are cancelled and the first error is returned. Each sub-step gets its own checkpoint with the name "parallel:<group>:<index>". A group-level checkpoint marks overall completion for resume.

func (*Workflow) Run

func (w *Workflow) Run() *Run

Run returns the workflow run.

func (*Workflow) RunCompensations

func (w *Workflow) RunCompensations() error

RunCompensations executes all registered compensations in reverse order. Each compensation is checkpointed for crash recovery. Called by the runner when a workflow fails.

func (*Workflow) RunID

func (w *Workflow) RunID() id.RunID

RunID returns the workflow run ID.

func (*Workflow) SetChildStarter

func (w *Workflow) SetChildStarter(cs ChildStarter)

SetChildStarter injects the child workflow starter. Called by the runner.

func (*Workflow) Sleep

func (w *Workflow) Sleep(name string, d time.Duration) (err error)

Sleep pauses the workflow for the specified duration. On crash recovery, if a checkpoint exists for this sleep step, it is skipped immediately. The sleep can be interrupted by context cancellation.

func (*Workflow) Step

func (w *Workflow) Step(name string, fn func(ctx context.Context) error) (err error)

Step executes a named step function. If a checkpoint exists for this step name, the step is skipped (idempotent replay). Otherwise the function is executed and a checkpoint is saved on success. On failure, EmitWorkflowStepFailed is called and the error is returned.

func (*Workflow) StepWithCompensation

func (w *Workflow) StepWithCompensation(
	name string,
	execute func(ctx context.Context) error,
	compensate func(ctx context.Context) error,
) error

StepWithCompensation executes a named step with an associated compensation function. If the step succeeds, the compensation is pushed onto a LIFO stack. When the workflow fails later, all registered compensations run in reverse order to undo completed work (saga pattern).

func (*Workflow) WaitForAll

func (w *Workflow) WaitForAll(names []string, timeout time.Duration) (_ []*event.Event, err error)

WaitForAll blocks until events matching ALL specified names are received or the timeout expires. Each received event is individually checkpointed. Returns an error if any subscription times out.

func (*Workflow) WaitForAny

func (w *Workflow) WaitForAny(names []string, timeout time.Duration) (_ *event.Event, err error)

WaitForAny blocks until an event matching ANY of the given names is received or the timeout expires. Returns the first event received, or nil on timeout.

func (*Workflow) WaitForEvent

func (w *Workflow) WaitForEvent(name string, timeout time.Duration) (_ *event.Event, err error)

WaitForEvent blocks until an event matching the given name is published or the timeout expires. On success the event is acknowledged and checkpointed. On timeout, an empty checkpoint is saved and nil is returned. On resume from checkpoint, the cached event is returned without re-waiting.

func (*Workflow) WaitForMatch

func (w *Workflow) WaitForMatch(
	name string,
	timeout time.Duration,
	match func(*event.Event) bool,
) (_ *event.Event, err error)

WaitForMatch blocks until an event matching the given name AND predicate is received, or the timeout expires. Events that don't match the predicate are skipped (not acknowledged). Returns nil on timeout.

Jump to

Keyboard shortcuts

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