| index 37370b1de..febca050c 100644 |
| --- a/internal/host/movesoffline_reports.go |
| +++ b/internal/host/movesoffline_reports.go |
| @@ -334,7 +334,9 @@ func (w *movesWorker) removeEmptyStrays(host string, rt *registry.Route) (remove |
| // without stopping at the cap: past capBytes it records overCap and goes |
| // on, until budget has passed or the count reaches twice capBytes, where it |
| // stops and records truncated. A missing files directory is an empty one. |
| -// An error is a walk that could not read part of the tree. |
| +// An error is a walk that could not read part of the tree, or |
| +// errMeasureBudget (with truncated) for one the budget cut short: only the |
| +// stop at twice the cap is a measure with truncated set. |
| func measureFiles(ctx context.Context, files, envPath string, budget time.Duration, capBytes int64) (bytes int64, entries int, overCap, truncated bool, err error) { |
| deadline := time.Now().Add(budget) |
| stop := errors.New("stop") |
| @@ -372,9 +374,9 @@ func measureFiles(ctx context.Context, files, envPath string, budget time.Durati |
| if err != nil { |
| return err |
| } |
| - if ctx.Err() != nil || time.Now().After(deadline) { |
| + if ctx.Err() != nil || !time.Now().Before(deadline) { |
| truncated = true |
| - return stop |
| + return errMeasureBudget |
| } |
| if p == files { |
| return nil |
| @@ -417,6 +419,20 @@ type replicaListing struct { |
| // beside each host's own file budget. |
| const hostFactsBudget = 2 * time.Minute |
| |
| +// hostFactsFilesBudget bounds one tick's file walks, all hosts together. A |
| +// variable so a test can spend it. |
| +var hostFactsFilesBudget = hostFactsBudget |
| + |
| +// errMeasureBudget is measureFiles' error for a walk its time budget cut |
| +// short: what it counted is not a measure. |
| +var errMeasureBudget = errors.New("the time budget ran out before the files were counted") |
| + |
| +// selfServeMovesEnv is the self-serve switch (the console's |
| +// CARLOS_SELF_SERVE_MOVES): set to "offline" in a box's environment, the |
| +// status tick collects every instance's facts whether or not a move names |
| +// the box. |
| +const selfServeMovesEnv = "CARLOS_SELF_SERVE_MOVES" |
| + |
| // hostFacts is the status tick's per-host block (status.HostFacts) for |
| // every instance row this box holds: the row's lifetime, the replica's |
| // bytes and objects from one listing of the box's replica prefix per tick, |
| @@ -441,6 +457,9 @@ func (w *movesWorker) hostFacts(ctx context.Context, routes []registry.Route, no |
| ctx, cancel := context.WithTimeout(ctx, hostFactsBudget) |
| defer cancel() |
| listing, listErr := w.listReplicas(ctx, now) |
| + // The walks share one budget of their own, from after the listing: a |
| + // host whose walk it never reaches, or cuts short, is not measured. |
| + filesDeadline := time.Now().Add(hostFactsFilesBudget) |
| out := make(map[string]status.HostFacts, len(rows)) |
| for _, rt := range rows { |
| f := status.HostFacts{CollectedAt: now.Unix(), Incarnation: rt.Incarnation} |
| @@ -456,14 +475,18 @@ func (w *movesWorker) hostFacts(ctx context.Context, routes []registry.Route, no |
| if err != nil { |
| errs = append(errs, "files: "+err.Error()) |
| } else { |
| - budget := precheckFilesBudget |
| + budget := min(precheckFilesBudget, time.Until(filesDeadline)) |
| if dl, ok := ctx.Deadline(); ok && time.Until(dl) < budget { |
| budget = time.Until(dl) |
| } |
| - var merr error |
| - f.FilesBytes, f.FilesEntries, f.OverCap, f.Truncated, merr = measureFiles(ctx, files, envPath, budget, archive.Cap) |
| - if merr != nil { |
| - errs = append(errs, "files: "+merr.Error()) |
| + if budget <= 0 { |
| + errs = append(errs, "files: not measured this tick: the time budget ran out before this host's walk") |
| + } else { |
| + var merr error |
| + f.FilesBytes, f.FilesEntries, f.OverCap, f.Truncated, merr = measureFiles(ctx, files, envPath, budget, archive.Cap) |
| + if merr != nil { |
| + errs = append(errs, "files: "+merr.Error()) |
| + } |
| } |
| sort.Strings(stray) |
| f.Strays = stray |
| @@ -537,13 +560,56 @@ func moveLanes(getenv func(string) string) []string { |
| } |
| |
| // selfServeStatusProbes fills the status tick's self-serve block: the box id |
| -// the edges writer publishes, the lanes, and, from the armed moves worker, |
| -// the per-host facts. A box with no armed worker publishes the id and the |
| -// lanes alone. |
| +// the edges writer publishes, the instance id (the tick's own, else the |
| +// worker's lazily resolved one) and the lanes, always; and, from the armed |
| +// moves worker, the per-host facts, only when self-serve moves are in play |
| +// for this box (selfServeInPlay). The tick runs this before the heartbeat, |
| +// so a box no self-serve move can use pays nothing for the walks and the |
| +// replica listing. |
| func selfServeStatusProbes(ctx context.Context, p *status.Probes, w *movesWorker, boxID string, getenv func(string) string, routes []registry.Route, now time.Time) { |
| p.BoxID = boxID |
| p.Lanes = moveLanes(getenv) |
| - if w != nil { |
| + if w == nil { |
| + return |
| + } |
| + if p.InstanceID == "" { |
| + p.InstanceID = w.cfg.instance() |
| + } |
| + if selfServeInPlay(w, getenv) { |
| p.Hosts = w.hostFacts(ctx, routes, now) |
| } |
| } |
| + |
| +// selfServeInPlay is whether the status tick collects the per-host facts: |
| +// the box's environment turns the self-serve switch on |
| +// (CARLOS_SELF_SERVE_MOVES=offline, the console's own switch, set on the |
| +// box by the deployment's configuration), or the box takes part in an |
| +// offline-queued or offline-staging move with pinned participants, as the |
| +// keep view's last listing read the records. A view whose listing failed |
| +// counts as taking part: the walks are capped, and a source box's facts |
| +// must not go missing because one listing did. |
| +func selfServeInPlay(w *movesWorker, getenv func(string) string) bool { |
| + if strings.TrimSpace(getenv(selfServeMovesEnv)) == "offline" { |
| + return true |
| + } |
| + v := w.cfg.Offline |
| + if v == nil { |
| + return false |
| + } |
| + snap := v.snap.Load() |
| + if snap == nil { |
| + return false |
| + } |
| + if snap.failed { |
| + return true |
| + } |
| + for _, m := range snap.moves { |
| + if m.Participants == nil || (m.State != provision.OfflineQueued && m.State != provision.OfflineStaging) { |
| + continue |
| + } |
| + if source, target, _, _ := v.box.offlineRoles(&m); source || target { |
| + return true |
| + } |
| + } |
| + return false |
| +} |