A command that works on 480 hosts must show how far it got. The code that does the work is often a library, which other programs use too. So the library must not draw. It reports, and the command decides what to show. The kit does this with spans and a Bus.
The tree of spans#
A span is one unit of work. Work is a tree of spans, with one level for each kind of unit. The six kinds are an interface of the kit: they do not change.
A span goes in the context.Context that the work is done under. When the
code for a target makes a call, the call starts its span from that context.
So the call goes under its target, and nobody gives the target to the call
as a parameter. A library reports its work with no new parameter in its API.
Start a span with progress.Start. It gives back a new
context, for the work below the span, and the span. End the span with
progress.Span.End and the error of the work.
The Bus#
Each change to a span is one progress.Event of plain data:
a start, a run, an update, a line of output, an advance of its work, or an
end. The progress.Bus gives each event a number, Seq,
cleans its text, and gives it to each sink, in the order of the list of
sinks. Every sink sees the same events in the same order.
- The library starts a span for exe07. The event goes to the Bus.
- The Bus gives the event the next number. All sinks see this order.
- The Bus cleans each text with Sanitize. No sink must clean it again.
- The Bus gives the event to each sink, under its lock. A sink keeps the event and returns. It does not block.
- The tree draws on its own goroutine, ten times a second. The log writes on its own goroutine. The Bus is free for the next event.
A sink is anything with a Handle method: a display, the event log, an
MCP notifier, a test. Three rules keep the order safe:
- A sink does not block. The Bus calls
Handlewith its lock held. A sink updates its memory and returns. It does not write to a terminal or a file there, and it does not call the Bus. - A sink that panics comes off the Bus. Its stack goes to
BusOptions.PanicLog, and the work goes on. - Displays draw on their own time. A display draws from what
Handlekept, on a goroutine of its own. The event log writes from its own goroutine too.
Do not write to a terminal, a file or a network in Handle. The Bus holds
its lock while it calls the sink, so a slow sink stops all the work that
reports to the Bus.
How a span ends#
End takes the error of the work, and the error decides the status:
- A nil error ends the span ok.
- An error that is
progress.ErrSkipped, aserrors.Istells, ends it skipped: work left out on purpose, such as a call that a dry run does not make.progress.Skipmakes such an error with a reason. A rule of the program cannot turn a skip into a failure. - Any other error ends it failed or canceled. The class, such as
target,transportortimeout, comes fromprogress.Classify, and from the rule the program gives the Bus for errors that tell no class.
errors.Is looks through every error that an error joins. So an error that
joins a skip and a failure ends the span skipped. Work that gathers the
errors of its parts returns a skip only when every part was skipped.
No listener, no cost#
Without a Bus in the context, Start gives back the same context and a nil
*Span. Every method of a nil span does nothing. This path allocates
nothing, and two tests hold it to that. So a library can report all its
work, and a program that does not listen pays only for one lookup in the
context.
A closed vocabulary#
An event carries the fields of progress.Fields and nothing
else. There is no field for an argument vector, a script, standard input,
the environment or a header. So a secret in one of these cannot reach a
display or the log. Each text in an event goes through
progress.Sanitize first, which escapes it with termtext
and cuts it to its bound.
Runs and traces#
The events of one Bus are one run, and they map one to one onto a W3C
trace: the trace ID has 16 bytes and a span ID 8. A command that starts a
child can give it the trace context of the span that the child works for,
in TRACEPARENT. The child’s log then names that span as its parent, and
eventlog.NewForwarder can put the child’s events
under it. See Show the progress of a child process.