A command for a cluster works on hundreds of nodes. fanout.Map
runs a function on each, a bounded number at a time. It reports a step with
a target for each item, folded, so that the tree shows node sets.
✓ fetching the base image 5.0s 2.0 GiB at 410 MiB/s reinstall ◦ 0:10.5 ╎ reinstalling the nodes ◉◉◉◉◉◌◌◌◌◌ 6/12 ◦ 4 running ◦ 2 queued ╎ ◡ exe07 2.8s/30s POST /redfish/v1/Systems/1 ╎ ◡ exe08 2.1s/30s POST /redfish/v1/Systems/1 ╎ ◡ exe09 1.4s/30s POST /redfish/v1/Systems/1 ╎ ◡ exe10 0.7s/30s POST /redfish/v1/Systems/1 ╎ ✓ exe[01-06]
Procedure#
- Give the step a name and a
Limitinfanout.MapOptions. - Give
Describewhen a display should name an item by its node, its host or its role. - Write the function for one item. Return its result and its error.
- Read the outcomes in the order of the items. Each has the value and the error of its item.
- Return the error of
Map. By default it is afanout.FailureError, which names the items that failed.
capture := &progresstest.Capture{}
bus := progress.NewBus(progress.BusOptions{Sinks: []progress.Sink{capture}})
ctx := progress.WithBus(context.Background(), bus)
nodes := []string{"exe01", "exe02", "exe03", "exe04"}
outcomes, err := fanout.Map(ctx, nodes, fanout.MapOptions[string]{Step: "uptime", Limit: 2},
func(_ context.Context, node string) (string, error) {
if node == "exe03" {
return "", fmt.Errorf("%s: no answer", node)
}
return strings.ToUpper(node), nil
})
for i, o := range outcomes {
fmt.Println(nodes[i], o.Value, o.Err)
}
fmt.Println(err)
bus.Close()
fmt.Print(capture.Tree())exe01 EXE01 <nil>
exe02 EXE02 <nil>
exe03 exe03: no answer
exe04 EXE04 <nil>
1 of 4 failed: exe03
step uptime total=4 limit=2 [fold]: failed (target): 1 of 4 failed: exe03
target exe03: failed (target): {}: no answer
target exe[01-02,04]: okfanout/example_test.go. CI runs it and checks its output.More than a limit#
| Need | Use |
|---|---|
| No more than two calls at a time to each gateway, whatever the limit | fanout.Keyed, and MapOptions.Acquire |
| Batches, one after the other, with a pause; stop at the first that fails | fanout.Batches |
| Leave an item out on purpose | progress.Skip: its target ends skipped |
| Your own exit code for a failed step | MapOptions.Summarize, which builds on FailureError |
| Bytes or items for each target | ItemInfo.Unit and .Size, and progress.AdvanceWriter |
| Prove the limit in a test | fanouttest.InFlight |
gateways := []string{"gw1.example.org", "gw2.example.org"}
keys := &fanout.Keyed{Limit: 2}
var mu sync.Mutex
now, peak := map[string]int{}, map[string]int{}
fanout.Each(context.Background(), 8, 8, func(i int) {
gateway := gateways[i%len(gateways)]
release, err := keys.Acquire(context.Background(), gateway)
if err != nil {
return
}
defer release()
mu.Lock()
now[gateway]++
peak[gateway] = max(peak[gateway], now[gateway])
mu.Unlock()
time.Sleep(time.Millisecond)
mu.Lock()
now[gateway]--
mu.Unlock()
})
for _, gateway := range gateways {
fmt.Printf("%s: at most 2 at once: %t\n", gateway, peak[gateway] <= 2)
}gw1.example.org: at most 2 at once: true
gw2.example.org: at most 2 at once: truefanout/example_test.go. CI runs it and checks its output.CAUTION
A panic in the function of an item does not end the program. Map recovers
it, ends the target as failed with a fanout.PanicError, and
writes the stack to the PanicLog.
Prove the limit#
A test that only counts calls does not prove that four ran at once.
fanouttest.InFlight holds each call until the
number you expect is under way together.
fake := &uptime{}
fake.calls.Hold = 3
fake.calls.Patience = time.Minute
fanout.Each(context.Background(), 3, 3, func(i int) {
fake.Ask(fmt.Sprintf("exe%02d", i+1))
})
fmt.Println(fake.calls.Peak(), fake.calls.Started())3 3fanout/fanouttest/example_test.go. CI runs it and checks its output.