❯ go-clikit
v0.10.0 pkg.go.dev ↗

concepts › spans and the bus

Spans and the Bus

A library reports its work as a tree of spans in its context. The Bus gives each change to every sink, in one order.

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.

commandreinstallstepreinstalling the nodesbatchrack 3 of 8 (optional)targetexe07callPOST /redfish/v1/…waitfor the BMC to answer
The six kinds of span. Each kind goes in the kind above it. A batch is optional: only a staggered action has batches.

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.

libraryprogress.StartBusSeq 41Seq 42waits for an eventnumbers the eventescapes and cuts each textcalls each sink, in orderfree for the next eventTreekeeps it; draws ten times a secondEvent logqueues one JSONL lineCapturea test keeps every eventevent
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 Handle with 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 Handle kept, on a goroutine of its own. The event log writes from its own goroutine too.
CAUTION

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:

queuedrunningRunEndokfailedcanceledskippeda nil erroran error; Classify tells its classan interrupt or a deadlinean error that is ErrSkipped
The life of a span. Only a target that a pool announced starts queued. The class says in one word why a span failed.
  • A nil error ends the span ok.
  • An error that is progress.ErrSkipped, as errors.Is tells, ends it skipped: work left out on purpose, such as a call that a dry run does not make. progress.Skip makes 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, transport or timeout, comes from progress.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.