self-serve timings: a copy whose seal was not read has unknown bytes, no sample (final review F16)
sealed_at is marked only when the seal was read at the sealed observation, with its final copied bytes; a seal absent or rejected there leaves the bytes unknown (logged), so the finished move gives no copy timing and its duration never enters the copy's fixed-part samples as a zero-byte copy. Witness: TestUnknownSealBytesAreNoCopySample (e2e; faultStore.hidden hides the seal for the sealing phase, then the move finishes).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4 files changed,
+56
−5
internal/console/selfserve_estimate.go+3 −1internal/console/selfserve_job.go+7 −3internal/e2e/selfserve/job_world_test.go+8 −1internal/e2e/selfserve/seal_bytes_test.go+38 −0
diff --git a/internal/console/selfserve_estimate.go b/internal/console/selfserve_estimate.go| index 5403cabc9..53dfd8a7e 100644 |
| --- a/internal/console/selfserve_estimate.go |
| +++ b/internal/console/selfserve_estimate.go |
| @@ -322,7 +322,9 @@ ORDER BY at DESC, id DESC LIMIT ?`, q...) |
| // landing and the replica always carry some); its seconds are a whole |
| // transfer and would otherwise fall under the rate floor and pass for the |
| // land or promote term's fixed part. A final copy that carried nothing is |
| -// a true fixed-part sample, so copy needs none. |
| +// a true fixed-part sample, so copy needs none: sealed_at is marked only |
| +// with the seal's byte count read, so a copy whose bytes are unknown has |
| +// no end mark and gives no row (final review F16). |
| var selfServeTimingPhases = []struct { |
| phase, from, to, bytes string |
| needBytes bool |
diff --git a/internal/console/selfserve_job.go b/internal/console/selfserve_job.go| index 48b90322b..8d93663ff 100644 |
| --- a/internal/console/selfserve_job.go |
| +++ b/internal/console/selfserve_job.go |
| @@ -1875,11 +1875,15 @@ func (s *Server) selfServeSealing(ctx context.Context, t *leaseToken, row *SelfS |
| } |
| switch mv.State { |
| case provision.OfflineSealed: |
| - var copied int64 |
| + // sealed_at carries the final copy's bytes, read from the seal at |
| + // this observation. A seal absent or rejected here leaves them |
| + // unknown, which is not a zero-byte copy: no mark, so the move |
| + // gives no copy timing (final review F16). |
| if ev.seal != nil { |
| - copied = ev.seal.FinalCopiedBytes |
| + s.selfServeMark(row, "sealed_at", now, ev.seal.FinalCopiedBytes) |
| + } else { |
| + log.Printf("console: self-serve move %s (%s): sealed, the seal not read: the copy's bytes are unknown (no copy timing)", row.ID, row.Host) |
| } |
| - s.selfServeMark(row, "sealed_at", now, copied) |
| TestHooks.point("advance:sealing->landing") |
| _, err := s.store.AdvanceSelfServe(row.ID, ssSealing, ssLanding, "", selfServePhaseDeadline(row, ssLanding, now), time.Time{}, "", now) |
| return err |
diff --git a/internal/e2e/selfserve/job_world_test.go b/internal/e2e/selfserve/job_world_test.go| index 91a9c798b..cd8374bbf 100644 |
| --- a/internal/e2e/selfserve/job_world_test.go |
| +++ b/internal/e2e/selfserve/job_world_test.go |
| @@ -8,6 +8,7 @@ import ( |
| "encoding/json" |
| "errors" |
| "fmt" |
| + "os" |
| "path/filepath" |
| "strings" |
| "sync" |
| @@ -54,6 +55,9 @@ type faultStore struct { |
| seen func(ctx context.Context, key string) |
| // serialFailing fails every serial bump while set. |
| serialFailing bool |
| + // hidden, when set, answers a console read of any key it holds as |
| + // absent (an object the console cannot see for a while). |
| + hidden map[string]bool |
| } |
| type putFault int |
| @@ -121,11 +125,14 @@ func (f *faultStore) Put(ctx context.Context, key string, body []byte, ifMatch s |
| // be read), and forwards every other read. |
| func (f *faultStore) Get(ctx context.Context, key string) ([]byte, string, error) { |
| f.mu.Lock() |
| - unreadable := f.unreadable |
| + unreadable, hidden := f.unreadable, f.hidden[key] |
| f.mu.Unlock() |
| if unreadable && provision.IsRecordKey(key) { |
| return nil, "", errUnreadable |
| } |
| + if hidden { |
| + return nil, "", os.ErrNotExist |
| + } |
| return f.Store.Get(ctx, key) |
| } |
diff --git a/internal/e2e/selfserve/seal_bytes_test.go b/internal/e2e/selfserve/seal_bytes_test.go| new file mode 100644 |
| index 000000000..2b7b11e64 |
| --- /dev/null |
| +++ b/internal/e2e/selfserve/seal_bytes_test.go |
| @@ -0,0 +1,38 @@ |
| +//go:build e2e |
| + |
| +package selfserve |
| + |
| +import ( |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/console" |
| + "github.com/carlosframework/platform/internal/host" |
| + "github.com/carlosframework/platform/internal/moves" |
| +) |
| + |
| +// TestUnknownSealBytesAreNoCopySample (final review F16): the copy's bytes |
| +// are the seal's, read at the sealed observation. A seal the console |
| +// cannot see at that observation (absent for it, then restored) leaves the |
| +// copy's bytes unknown, never a true zero: the finished move gives no copy |
| +// timing, so its duration never enters the copy's fixed-part samples. |
| +func TestUnknownSealBytesAreNoCopySample(t *testing.T) { |
| + w := newJobWorld(t) |
| + r := w.cutTo("sealing") |
| + seal := moves.OfflineSealKey(host.E2EHost, r.ID) |
| + w.fs.mu.Lock() |
| + w.fs.hidden = map[string]bool{seal: true} |
| + w.fs.mu.Unlock() |
| + w.run(60, "past the seal", r.ID, func(r2 *console.SelfServeMove) bool { return r2.Phase != "sealing" || r2.Outcome != "" }) |
| + w.fs.mu.Lock() |
| + w.fs.hidden = nil |
| + w.fs.mu.Unlock() |
| + if d := w.done(r.ID); d.Outcome != "done" { |
| + t.Fatalf("finished %s", describe(d)) |
| + } |
| + if got, _ := w.cst.MoveTimings("", host.E2ETargetRegion, "copy", 20); len(got) != 0 { |
| + t.Fatalf("a copy whose bytes were never read became a timing sample: %+v", got) |
| + } |
| + if got, _ := w.cst.MoveTimings("", host.E2ETargetRegion, "stop", 20); len(got) != 1 { |
| + t.Fatalf("the move's other timings: stop %+v", got) |
| + } |
| +} |