genselect: GenerationMax honours the selection's authorized pin (Task 4 fix round 1, item 2)
When no current snapshot reaches the pin, the pinned snapshot's exact historical version counts, as Select restores it; the pin's shape is checked as selectFiles checks it. The claim writer's restore point and moves' loss validator pass the selection's pin. Tests: an ordinary selection whose only root is a historical version, through Select, GenerationMax and Writer.restorePoint.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5 files changed,
+158
−24
internal/genselect/generationmax_test.go+54 −12internal/genselect/select.go+57 −10internal/host/movesloss.go+1 −1internal/hostbox/lossrestorepoint_test.go+45 −0internal/hostbox/writer_loss.go+1 −1
diff --git a/internal/genselect/generationmax_test.go b/internal/genselect/generationmax_test.go| index 88c25319b..a60bcfef4 100644 |
| --- a/internal/genselect/generationmax_test.go |
| +++ b/internal/genselect/generationmax_test.go |
| @@ -3,6 +3,7 @@ package genselect |
| import ( |
| "context" |
| "errors" |
| + "fmt" |
| "testing" |
| "github.com/carlosframework/platform/internal/hostclaim" |
| @@ -23,7 +24,7 @@ func TestGenerationMax(t *testing.T) { |
| if err != nil { |
| t.Fatal(err) |
| } |
| - got, err := GenerationMax(ctx, f.store, "data", f.req.Holder.Location, "g0") |
| + got, err := GenerationMax(ctx, f.store, "data", f.req.Holder.Location, "g0", nil) |
| if err != nil || got != 14 || got != plan.MaxTXID { |
| t.Fatalf("GenerationMax %d %v, Select %d", got, err, plan.MaxTXID) |
| } |
| @@ -31,11 +32,11 @@ func TestGenerationMax(t *testing.T) { |
| t.Run("one generation's files only", func(t *testing.T) { |
| f := newFixture(t) |
| f.file("g1", 0, 1, 99) |
| - got, err := GenerationMax(ctx, f.store, "data", f.req.Holder.Location+"/", "g0") |
| + got, err := GenerationMax(ctx, f.store, "data", f.req.Holder.Location+"/", "g0", nil) |
| if err != nil || got != 11 { |
| t.Fatalf("g0 %d %v", got, err) |
| } |
| - if got, err = GenerationMax(ctx, f.store, "data", f.req.Holder.Location, "g1"); err != nil || got != 99 { |
| + if got, err = GenerationMax(ctx, f.store, "data", f.req.Holder.Location, "g1", nil); err != nil || got != 99 { |
| t.Fatalf("g1 %d %v", got, err) |
| } |
| }) |
| @@ -44,7 +45,7 @@ func TestGenerationMax(t *testing.T) { |
| f.file(hostclaim.RootGeneration, 9, 1, 7) |
| f.file(hostclaim.RootGeneration, 0, 8, 8) |
| for _, gen := range []string{hostclaim.RootGeneration, ""} { |
| - if got, err := GenerationMax(ctx, f.store, "data", f.req.Holder.Location, gen); err != nil || got != 8 { |
| + if got, err := GenerationMax(ctx, f.store, "data", f.req.Holder.Location, gen, nil); err != nil || got != 8 { |
| t.Fatalf("%q: %d %v", gen, got, err) |
| } |
| } |
| @@ -53,13 +54,52 @@ func TestGenerationMax(t *testing.T) { |
| f := newFixture(t) |
| late := f.file("g0", 0, 12, 12) |
| f.b.BareDelete("data", late.Key) |
| - if got, err := GenerationMax(ctx, f.store, "data", f.req.Holder.Location, "g0"); err != nil || got != 11 { |
| + if got, err := GenerationMax(ctx, f.store, "data", f.req.Holder.Location, "g0", nil); err != nil || got != 11 { |
| + t.Fatalf("%d %v", got, err) |
| + } |
| + }) |
| + t.Run("an authorized pin whose root survives only as a historical version", func(t *testing.T) { |
| + f := newFixture(t) |
| + late := f.file("g0", 0, 12, 12) |
| + for _, key := range []string{f.pin.Key, late.Key, f.req.Holder.Location + "/g-g0/0000/" + ltxName(11, 11)} { |
| + f.b.BareDelete("data", key) |
| + } |
| + plan, err := Select(ctx, f.store, f.req) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if plan.Root == nil || plan.Root.VersionID != f.pin.VersionID || plan.MaxTXID != 11 || plan.Pin == nil { |
| + t.Fatalf("the selection is not the pinned historical root: %+v", plan) |
| + } |
| + got, err := GenerationMax(ctx, f.store, "data", f.req.Holder.Location, "g0", plan.Pin) |
| + if err != nil || got != plan.MaxTXID { |
| + t.Fatalf("GenerationMax %d %v, Select %d", got, err, plan.MaxTXID) |
| + } |
| + if _, err := GenerationMax(ctx, f.store, "data", f.req.Holder.Location, "g0", nil); !errors.Is(err, ErrEmpty) { |
| + t.Fatalf("without the pin: %v", err) |
| + } |
| + missing := *plan.Pin |
| + missing.SnapshotVersion = "not-a-version" |
| + if _, err := GenerationMax(ctx, f.store, "data", f.req.Holder.Location, "g0", &missing); !errors.Is(err, ErrPinnedMissing) { |
| + t.Fatalf("a pin whose version is gone: %v", err) |
| + } |
| + elsewhere := *plan.Pin |
| + elsewhere.SnapshotKey = f.req.Holder.Location + "/g-g1/0009/" + ltxName(1, 11) |
| + if _, err := GenerationMax(ctx, f.store, "data", f.req.Holder.Location, "g0", &elsewhere); !errors.Is(err, ErrPinnedMismatch) { |
| + t.Fatalf("a pin of another generation: %v", err) |
| + } |
| + }) |
| + t.Run("a pin below a current root is not needed", func(t *testing.T) { |
| + f := newFixture(t) |
| + f.file("g0", 9, 1, 13) |
| + got, err := GenerationMax(ctx, f.store, "data", f.req.Holder.Location, "g0", &f.marker.Pin) |
| + if err != nil || got != 13 { |
| t.Fatalf("%d %v", got, err) |
| } |
| }) |
| t.Run("refusals", func(t *testing.T) { |
| f := newFixture(t) |
| - if _, err := GenerationMax(ctx, f.store, "data", f.req.Holder.Location, "none"); !errors.Is(err, ErrEmpty) { |
| + if _, err := GenerationMax(ctx, f.store, "data", f.req.Holder.Location, "none", nil); !errors.Is(err, ErrEmpty) { |
| t.Fatalf("no files: %v", err) |
| } |
| for name, in := range map[string][3]string{ |
| @@ -72,23 +112,23 @@ func TestGenerationMax(t *testing.T) { |
| "an empty element": {"data", "instances//h", "g0"}, |
| "a trailing double": {"data", "instances/h//", "g0"}, |
| } { |
| - if _, err := GenerationMax(ctx, f.store, in[0], in[1], in[2]); !errors.Is(err, ErrInput) { |
| + if _, err := GenerationMax(ctx, f.store, in[0], in[1], in[2], nil); !errors.Is(err, ErrInput) { |
| t.Fatalf("%s: %v", name, err) |
| } |
| } |
| - if _, err := GenerationMax(ctx, nil, "data", f.req.Holder.Location, "g0"); !errors.Is(err, ErrInput) { |
| + if _, err := GenerationMax(ctx, nil, "data", f.req.Holder.Location, "g0", nil); !errors.Is(err, ErrInput) { |
| t.Fatalf("no reader: %v", err) |
| } |
| listing := altered{Reader: f.store, list: func([]lease.ObjectVersion) ([]lease.ObjectVersion, error) { |
| return nil, errors.New("access denied") |
| }} |
| - if _, err := GenerationMax(ctx, listing, "data", f.req.Holder.Location, "g0"); !errors.Is(err, ErrListing) { |
| + if _, err := GenerationMax(ctx, listing, "data", f.req.Holder.Location, "g0", nil); !errors.Is(err, ErrListing) { |
| t.Fatalf("an unreadable listing: %v", err) |
| } |
| malformed := altered{Reader: f.store, list: func(vs []lease.ObjectVersion) ([]lease.ObjectVersion, error) { |
| return append(vs, lease.ObjectVersion{Key: f.req.Holder.Location + "/g-g0/0000/bogus.ltx", VersionID: "v", IsLatest: true}), nil |
| }} |
| - if _, err := GenerationMax(ctx, malformed, "data", f.req.Holder.Location, "g0"); !errors.Is(err, ErrListing) { |
| + if _, err := GenerationMax(ctx, malformed, "data", f.req.Holder.Location, "g0", nil); !errors.Is(err, ErrListing) { |
| t.Fatalf("a malformed level file: %v", err) |
| } |
| unversioned := altered{Reader: f.store, list: func(vs []lease.ObjectVersion) ([]lease.ObjectVersion, error) { |
| @@ -97,12 +137,14 @@ func TestGenerationMax(t *testing.T) { |
| } |
| return vs, nil |
| }} |
| - if _, err := GenerationMax(ctx, unversioned, "data", f.req.Holder.Location, "g0"); !errors.Is(err, ErrVersionID) { |
| + if _, err := GenerationMax(ctx, unversioned, "data", f.req.Holder.Location, "g0", nil); !errors.Is(err, ErrVersionID) { |
| t.Fatalf("no version ids: %v", err) |
| } |
| f.file("g0", 9, 2, 20) |
| - if _, err := GenerationMax(ctx, f.store, "data", f.req.Holder.Location, "g0"); !errors.Is(err, ErrRoot) { |
| + if _, err := GenerationMax(ctx, f.store, "data", f.req.Holder.Location, "g0", nil); !errors.Is(err, ErrRoot) { |
| t.Fatalf("a snapshot not from 1: %v", err) |
| } |
| }) |
| } |
| + |
| +func ltxName(min, max uint64) string { return fmt.Sprintf("%016x-%016x.ltx", min, max) } |
diff --git a/internal/genselect/select.go b/internal/genselect/select.go| index 411f3b618..8eacb0185 100644 |
| --- a/internal/genselect/select.go |
| +++ b/internal/genselect/select.go |
| @@ -300,14 +300,17 @@ func SelectPrepared(ctx context.Context, s Reader, req Request, prepared Prepare |
| // GenerationMax is generation gen's remote maximum under location in |
| // bucket: the highest maximum TXID among its current level files (the |
| -// latest version of each key, never a retention tombstone), read with the |
| -// coordinates and version rules Select reads, so a generation Select would |
| -// choose has Select's maximum. It is the one rule for a generation's |
| -// maximum where a box reads its own bucket: the claim writer's loss restore |
| -// point (hostbox.Writer) and moves' loss baseline validator read it alike. |
| -// "" or hostclaim.RootGeneration is the location's own level files. A |
| -// generation with no current level file is ErrEmpty. |
| -func GenerationMax(ctx context.Context, s Reader, bucket, location, gen string) (uint64, error) { |
| +// latest version of each key, never a retention tombstone) and, when pin |
| +// is the selection's authorized pin and no current snapshot reaches it, |
| +// the pinned snapshot's exact historical version, as Select restores it. |
| +// It reads with the coordinates, version and pin rules Select reads, so a |
| +// generation Select would choose under pin has Select's maximum. It is the |
| +// one rule for a generation's maximum where a box reads its own bucket: |
| +// the claim writer's loss restore point (hostbox.Writer) and moves' loss |
| +// baseline validator read it alike. "" or hostclaim.RootGeneration is the |
| +// location's own level files. A generation with no current level file and |
| +// no pinned root is ErrEmpty. |
| +func GenerationMax(ctx context.Context, s Reader, bucket, location, gen string, pin *Pin) (uint64, error) { |
| location = strings.TrimSuffix(location, "/") |
| if s == nil || bucket == "" || location == "" || (gen != "" && gen != hostclaim.RootGeneration && !generation(gen)) { |
| return 0, ErrInput |
| @@ -318,11 +321,29 @@ func GenerationMax(ctx context.Context, s Reader, bucket, location, gen string) |
| } |
| } |
| base := prefix(location+"/", origin(gen)) |
| - _, latest, err := listVersions(ctx, s, bucket, base) |
| + floor := uint64(0) |
| + if pin != nil { |
| + // The pin's shape, as selectFiles checks it. |
| + if pin.SnapshotTXID == 0 { |
| + if pin.SnapshotKey != "" || pin.SnapshotVersion != "" || pin.SnapshotCRC64 != 0 { |
| + return 0, ErrPinnedMismatch |
| + } |
| + } else { |
| + f, ok := coordinates(pin.SnapshotKey, base) |
| + if !ok || f.Level != 9 || f.MinTXID != 1 || f.MaxTXID != pin.SnapshotTXID { |
| + return 0, ErrPinnedMismatch |
| + } |
| + if !versionID(pin.SnapshotVersion) { |
| + return 0, ErrVersionID |
| + } |
| + floor = pin.SnapshotTXID |
| + } |
| + } |
| + versions, latest, err := listVersions(ctx, s, bucket, base) |
| if err != nil { |
| return 0, err |
| } |
| - max, found := uint64(0), false |
| + max, found, rooted := uint64(0), false, false |
| for _, v := range latest { |
| f, ok, err := currentFile(v, base) |
| if err != nil { |
| @@ -334,6 +355,32 @@ func GenerationMax(ctx context.Context, s Reader, bucket, location, gen string) |
| if f.Level == 9 && f.MinTXID != 1 { |
| return 0, ErrRoot |
| } |
| + // A current snapshot at or past the pin is a root Select can take; |
| + // a late duplicate at the pinned key never is. |
| + if f.Level == 9 && f.MaxTXID >= floor && (floor == 0 || f.Key != pin.SnapshotKey) { |
| + rooted = true |
| + } |
| + if !found || f.MaxTXID > max { |
| + max, found = f.MaxTXID, true |
| + } |
| + } |
| + if floor > 0 && !rooted { |
| + // Select restores the pinned snapshot's exact version. |
| + var pinned *lease.ObjectVersion |
| + for _, v := range versions { |
| + if v.Key == pin.SnapshotKey && v.VersionID == pin.SnapshotVersion { |
| + cp := v |
| + pinned = &cp |
| + break |
| + } |
| + } |
| + if pinned == nil { |
| + return 0, ErrPinnedMissing |
| + } |
| + f, err := exactFile(*pinned, base) |
| + if err != nil { |
| + return 0, err |
| + } |
| if !found || f.MaxTXID > max { |
| max, found = f.MaxTXID, true |
| } |
diff --git a/internal/host/movesloss.go b/internal/host/movesloss.go| index 45cb69aa0..3a483f25f 100644 |
| --- a/internal/host/movesloss.go |
| +++ b/internal/host/movesloss.go |
| @@ -222,7 +222,7 @@ func (r movesRecoveryOwners) frozenMaximum(ctx context.Context, v recoveryProbe, |
| if ok && (recorded.Generation != baseline.Generation || recorded.MaxTXID != baseline.MaxTXID) { |
| return fmt.Errorf("%w: the restore point is not the recorded source baseline", hostbox.ErrChanged) |
| } |
| - max, err := genselect.GenerationMax(ctx, m.Data, m.Bucket, v.holder.Location, baseline.Generation) |
| + max, err := genselect.GenerationMax(ctx, m.Data, m.Bucket, v.holder.Location, baseline.Generation, baseline.Pin) |
| if err != nil { |
| return fmt.Errorf("%w: the source baseline's maximum: %v", hostbox.ErrChanged, err) |
| } |
diff --git a/internal/hostbox/lossrestorepoint_test.go b/internal/hostbox/lossrestorepoint_test.go| index 7a98719b6..7a834a10e 100644 |
| --- a/internal/hostbox/lossrestorepoint_test.go |
| +++ b/internal/hostbox/lossrestorepoint_test.go |
| @@ -4,9 +4,11 @@ package hostbox |
| import ( |
| "context" |
| + "encoding/json" |
| "errors" |
| "strings" |
| "testing" |
| + "time" |
| "github.com/carlosframework/platform/internal/genselect" |
| "github.com/carlosframework/platform/internal/hostclaim" |
| @@ -91,3 +93,46 @@ func TestLossRestorePointIsTheGenerationMax(t *testing.T) { |
| } |
| } |
| } |
| + |
| +// Fix round 1, item 2: the restore point honours the authorized pin, as |
| +// Select does. An ordinary selection whose only usable root is the marker's |
| +// pinned snapshot, surviving only as a historical version (its current |
| +// version a retention tombstone), is accepted at the pin's maximum. |
| +func TestLossRestorePointHonoursThePinnedRoot(t *testing.T) { |
| + _, store, o := workFixture(t) |
| + o, err := hostclaim.Update(t.Context(), store, o, func(c *hostclaim.Claim) error { |
| + c.Holder.Generation = "src" |
| + c.Op = hostclaim.Op{Kind: hostclaim.KindRestoring, ID: "op", Phase: hostclaim.PhaseProbing, Exec: "box", Participants: []string{"box"}, Intent: map[string]string{"target": "target"}} |
| + return nil |
| + }) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + h := o.Claim.Holder |
| + data := leasetest.NewVersionedBucket() |
| + data.AddBucket("data") |
| + header, err := (<x.Header{Version: ltx.Version, Flags: ltx.HeaderFlagNoChecksum, PageSize: 4096, MinTXID: 1, MaxTXID: 5}).MarshalBinary() |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + root := strings.TrimSuffix(h.Location, "/") + "/g-src/0009/" + ltx.FormatFilename(1, 5) |
| + version := data.Put("data", root, header) |
| + marker, err := json.Marshal(genselect.Marker{Op: "src", Incarnation: h.Incarnation, Host: o.Host, Box: "box", InstalledAt: time.Unix(1, 0).UTC(), |
| + Pin: genselect.Pin{SnapshotKey: root, SnapshotVersion: version, SnapshotTXID: 5}}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + data.Put("data", strings.TrimSuffix(h.Location, "/")+"/generation.json", marker) |
| + data.BareDelete("data", root) |
| + w := Writer{Claims: store, Box: "box", Region: "ie", Bucket: "data", Data: data} |
| + plan, err := w.Baseline(t.Context(), o) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if plan.Root == nil || plan.Root.VersionID != version || plan.MaxTXID != 5 { |
| + t.Fatalf("the selection is not the pinned historical root: %+v", plan) |
| + } |
| + if err := w.restorePoint(t.Context(), o, hostclaim.Admission{RemoteMaxTXID: plan.MaxTXID}, plan); err != nil { |
| + t.Fatalf("a valid pinned restore point was refused: %v", err) |
| + } |
| +} |
diff --git a/internal/hostbox/writer_loss.go b/internal/hostbox/writer_loss.go| index 60bf96bc4..bec9de09d 100644 |
| --- a/internal/hostbox/writer_loss.go |
| +++ b/internal/hostbox/writer_loss.go |
| @@ -93,7 +93,7 @@ func (w Writer) restorePoint(ctx context.Context, o hostclaim.Observed, in hostc |
| if w.Data == nil || w.Bucket == "" { |
| return ErrUnsupported |
| } |
| - max, err := genselect.GenerationMax(ctx, w.Data, w.Bucket, o.Claim.Holder.Location, baseline.Generation) |
| + max, err := genselect.GenerationMax(ctx, w.Data, w.Bucket, o.Claim.Holder.Location, baseline.Generation, baseline.Pin) |
| if err != nil { |
| return fmt.Errorf("%w: the restore point's generation: %w", ErrChanged, err) |
| } |