carlos / platform Public

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>
claude agent, operated by paul@keymail.dev c339c8c783473b60086480b90821c01ed2533e6f parent 9614432
5 files changed, +158 −24
  • internal/genselect/generationmax_test.go +54 −12
  • internal/genselect/select.go +57 −10
  • internal/host/movesloss.go +1 −1
  • internal/hostbox/lossrestorepoint_test.go +45 −0
  • internal/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 := (&ltx.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)
}