go-network-configurator/services.go

78 lines
2.6 KiB
Go

package netconfig
import (
"context"
"fmt"
"time"
)
// serviceReadyPollInterval is how often waitForServiceReady re-probes. A daemon
// coming up at boot takes seconds, so probing a few times a second costs little
// and returns promptly once it arrives. It is a variable only so tests can wait
// on the loop without sleeping for real.
var serviceReadyPollInterval = 250 * time.Millisecond
// readyProbe reports whether a service is ready to be configured. An error is a
// probe that could not reach the service, which during boot is indistinguishable
// from a service that has not started yet; it is therefore not fatal on its own
// and is only surfaced if the wait runs out.
type readyProbe func() (bool, error)
// waitForServiceReady blocks until probe reports the named service ready, the
// timeout elapses, or ctx is cancelled.
//
// Some backends are configured through a running daemon rather than a file, so
// a configurator constructed before that daemon is up would hold a handle that
// reports nothing and accepts no changes. This program can be started early
// enough in boot to lose that race — from a unit ordered alongside the daemon
// rather than after it — which is what the wait is for.
//
// A timeout of zero or less does not wait: probe runs once and its result is
// reported, so a caller already ordered after the daemon fails fast instead of
// blocking. A service that is already up returns on the first probe and pays no
// delay either way.
func waitForServiceReady(ctx context.Context, name string, timeout time.Duration, probe readyProbe) error {
if timeout > 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, timeout)
defer cancel()
}
ticker := time.NewTicker(serviceReadyPollInterval)
defer ticker.Stop()
waited := false
for {
ready, probeErr := probe()
if ready {
if waited {
logger.Printf("%s: became ready", name)
}
return nil
}
// Not waiting: report why the single probe said no.
if timeout <= 0 {
if probeErr != nil {
return fmt.Errorf("%s is not ready: %w", name, probeErr)
}
return fmt.Errorf("%s is not ready", name)
}
if !waited {
waited = true
logger.Printf("%s: waiting up to %s for the service to become ready", name, timeout)
}
select {
case <-ctx.Done():
// The last probe error explains the wait better than "deadline
// exceeded" does, so prefer it when there was one.
if probeErr != nil {
return fmt.Errorf("timed out waiting for %s: %w", name, probeErr)
}
return fmt.Errorf("timed out waiting for %s: %w", name, ctx.Err())
case <-ticker.C:
}
}
}