| index 2d7bde6cb..6c62b013c 100644 |
| --- a/internal/host/movescut.go |
| +++ b/internal/host/movescut.go |
| @@ -65,10 +65,18 @@ const cutStepsPerPass = 8 |
| // Op journal steps the cut writes (moves.Journal). |
| const ( |
| cutStepEvidence = "evidence" // §5.3 steps 1 to 4, the StoppedReport JSON |
| + cutStepCoverage = "coverage" // cutCoverage: the stopped database's position against N |
| cutStepArchive = "archive" // the cut archive's identity, before any PUT |
| cutStepRevertBound = "revert_bound" // the revert's staging park evidence_at |
| ) |
| |
| +// cutCoverage is the journalled coverage decision (ruling 3): whether the |
| +// stopped database's own position equalled N after the final checkpoint. |
| +type cutCoverage struct { |
| + N uint64 `json:"n"` |
| + Covered bool `json:"covered"` |
| +} |
| + |
| // cutRun is one pass's state for one cutting record. |
| type cutRun struct { |
| w *movesWorker |
| @@ -531,7 +539,10 @@ func (c *cutRun) giveUpExecution(ctx context.Context, obs hostclaim.Observed, st |
| } |
| prefix, keep := moves.ReplicaSource(holder) |
| dbPath := c.dbPath() |
| - fromDB := regularFile(dbPath) |
| + fromDB, err := regularFile(dbPath) |
| + if err != nil { |
| + return moves.StoppedReport{}, err |
| + } |
| if fromDB { |
| if cfg.FinalCheckpoint == nil { |
| return moves.StoppedReport{}, errors.New("no final checkpoint on this box") |
| @@ -553,6 +564,26 @@ func (c *cutRun) giveUpExecution(ctx context.Context, obs hostclaim.Observed, st |
| if dbSHA, err = fileSHA256(dbPath); err != nil { |
| return moves.StoppedReport{}, fmt.Errorf("digest the stopped database: %w", err) |
| } |
| + // Coverage (ruling 3): the stopped database's own position against |
| + // N, decided here, with the database present after its checkpoint, |
| + // and journalled so the seal can still state it if the disk is lost |
| + // before the final round. Without a database nothing is journalled |
| + // and the seal says uncovered. |
| + if cfg.LocalPosition == nil { |
| + return moves.StoppedReport{}, errors.New("no local position on this box") |
| + } |
| + pos, err := cfg.LocalPosition(dbPath) |
| + if err != nil { |
| + return moves.StoppedReport{}, fmt.Errorf("read the stopped database's position: %w", err) |
| + } |
| + body, err := json.Marshal(cutCoverage{N: n, Covered: pos == n}) |
| + if err != nil { |
| + return moves.StoppedReport{}, err |
| + } |
| + j.Mark(cutStepCoverage, string(body)) |
| + if err := j.Write(cfg.DataDir); err != nil { |
| + return moves.StoppedReport{}, err |
| + } |
| } |
| timersSHA, err := c.snapshotTimers(ctx, own, vs) |
| if err != nil { |
| @@ -910,8 +941,14 @@ func (c *cutRun) finalRound(ctx context.Context, holder hostclaim.Holder, report |
| } |
| replica = append(replica, moves.SealedObject{Key: dstPrefix + rel, VersionID: ver, Size: o.Size}) |
| } |
| - fromDB := regularFile(c.dbPath()) |
| - covered := true |
| + // Coverage starts false (ruling 3): a database still here is asked for |
| + // its position against N; one that is gone is covered only if the |
| + // checkpoint journalled that it was, at this N (giveUpExecution). |
| + fromDB, err := regularFile(c.dbPath()) |
| + if err != nil { |
| + return false, err |
| + } |
| + covered := false |
| if fromDB { |
| if cfg.LocalPosition == nil { |
| return false, errors.New("no local position on this box") |
| @@ -921,6 +958,13 @@ func (c *cutRun) finalRound(ctx context.Context, holder hostclaim.Holder, report |
| return false, fmt.Errorf("read the stopped database's position: %w", err) |
| } |
| covered = pos == report.FinalTXID |
| + } else if j, err := c.journal(); err != nil { |
| + return false, err |
| + } else if raw, ok := j.Steps[cutStepCoverage]; ok { |
| + var cov cutCoverage |
| + if json.Unmarshal([]byte(raw), &cov) == nil && cov.N == report.FinalTXID { |
| + covered = cov.Covered |
| + } |
| } |
| now := cfg.Now().UTC() |
| seal := moves.Seal{V: moves.SealVersion, N: report.FinalTXID, Replica: replica, |
| @@ -1429,10 +1473,23 @@ func obligationID() string { |
| return hex.EncodeToString(b[:]) |
| } |
| |
| -// regularFile reports whether path is a regular file. |
| -func regularFile(path string) bool { |
| +// regularFile reports whether path is a regular file. Absence (ENOENT) is |
| +// false with no error; any other answer from the filesystem, an error or |
| +// something there that is not a regular file, is an error, never absence |
| +// (ruling 3): the cut decides coverage on it, so it must not read a |
| +// transient fault as "the database is gone". |
| +func regularFile(path string) (bool, error) { |
| st, err := os.Lstat(path) |
| - return err == nil && st.Mode().IsRegular() |
| + if errors.Is(err, os.ErrNotExist) { |
| + return false, nil |
| + } |
| + if err != nil { |
| + return false, fmt.Errorf("stat %s: %w", path, err) |
| + } |
| + if !st.Mode().IsRegular() { |
| + return false, fmt.Errorf("%s is not a regular file (%s)", path, st.Mode()) |
| + } |
| + return true, nil |
| } |
| |
| // fileSHA256 is "sha256:<hex>" of the file at path. |