genselect: GenerationMax, the one rule for a generation's maximum in a box's own bucket (moves Task 4)
Select and GenerationMax read a key's coordinates through one helper, currentFile. platform-1e's answer on #236 (Q5): no cross-region reads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+184
−14
internal/genselect/generationmax_test.go+108 −0internal/genselect/select.go+76 −14
diff --git a/internal/genselect/generationmax_test.go b/internal/genselect/generationmax_test.go| new file mode 100644 |
| index 000000000..88c25319b |
| --- /dev/null |
| +++ b/internal/genselect/generationmax_test.go |
| @@ -0,0 +1,108 @@ |
| +package genselect |
| + |
| +import ( |
| + "context" |
| + "errors" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/hostclaim" |
| + "github.com/carlosframework/platform/internal/lease" |
| +) |
| + |
| +// GenerationMax is the one rule for a generation's remote maximum in a box's |
| +// own bucket (moves Task 4, platform-1e's answer on #236): the highest |
| +// maximum among the generation's current level files, read as Select reads |
| +// them, so a selectable generation's maximum is Select's. |
| +func TestGenerationMax(t *testing.T) { |
| + ctx := context.Background() |
| + t.Run("the marker's generation is Select's maximum", func(t *testing.T) { |
| + f := newFixture(t) |
| + f.file("g0", 1, 1, 14) |
| + f.file("g0", 0, 12, 14) |
| + plan, err := Select(ctx, f.store, f.req) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + got, err := GenerationMax(ctx, f.store, "data", f.req.Holder.Location, "g0") |
| + if err != nil || got != 14 || got != plan.MaxTXID { |
| + t.Fatalf("GenerationMax %d %v, Select %d", got, err, plan.MaxTXID) |
| + } |
| + }) |
| + 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") |
| + 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 { |
| + t.Fatalf("g1 %d %v", got, err) |
| + } |
| + }) |
| + t.Run("the origin is the location's own level files", func(t *testing.T) { |
| + f := newFixture(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 { |
| + t.Fatalf("%q: %d %v", gen, got, err) |
| + } |
| + } |
| + }) |
| + t.Run("a retention tombstone is not a current file", func(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 { |
| + 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) { |
| + t.Fatalf("no files: %v", err) |
| + } |
| + for name, in := range map[string][3]string{ |
| + "no bucket": {"", f.req.Holder.Location, "g0"}, |
| + "no location": {"data", "", "g0"}, |
| + "a climbing path": {"data", "instances/../h", "g0"}, |
| + "a nested gen": {"data", f.req.Holder.Location, "g0/x"}, |
| + "a dot generation": {"data", f.req.Holder.Location, ".."}, |
| + "an absolute path": {"data", "/instances/h", "g0"}, |
| + "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) { |
| + t.Fatalf("%s: %v", name, err) |
| + } |
| + } |
| + if _, err := GenerationMax(ctx, nil, "data", f.req.Holder.Location, "g0"); !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) { |
| + 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) { |
| + t.Fatalf("a malformed level file: %v", err) |
| + } |
| + unversioned := altered{Reader: f.store, list: func(vs []lease.ObjectVersion) ([]lease.ObjectVersion, error) { |
| + for i := range vs { |
| + vs[i].VersionID = "null" |
| + } |
| + return vs, nil |
| + }} |
| + if _, err := GenerationMax(ctx, unversioned, "data", f.req.Holder.Location, "g0"); !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) { |
| + t.Fatalf("a snapshot not from 1: %v", err) |
| + } |
| + }) |
| +} |
diff --git a/internal/genselect/select.go b/internal/genselect/select.go| index 2dc4acad0..411f3b618 100644 |
| --- a/internal/genselect/select.go |
| +++ b/internal/genselect/select.go |
| @@ -298,6 +298,78 @@ func SelectPrepared(ctx context.Context, s Reader, req Request, prepared Prepare |
| return selectFiles(ctx, s, req.Bucket, base, versions, latest, p, pin) |
| } |
| +// 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) { |
| + location = strings.TrimSuffix(location, "/") |
| + if s == nil || bucket == "" || location == "" || (gen != "" && gen != hostclaim.RootGeneration && !generation(gen)) { |
| + return 0, ErrInput |
| + } |
| + for _, part := range strings.Split(location, "/") { |
| + if !component(part) { |
| + return 0, ErrInput |
| + } |
| + } |
| + base := prefix(location+"/", origin(gen)) |
| + _, latest, err := listVersions(ctx, s, bucket, base) |
| + if err != nil { |
| + return 0, err |
| + } |
| + max, found := uint64(0), false |
| + for _, v := range latest { |
| + f, ok, err := currentFile(v, base) |
| + if err != nil { |
| + return 0, err |
| + } |
| + if !ok { |
| + continue |
| + } |
| + if f.Level == 9 && f.MinTXID != 1 { |
| + return 0, ErrRoot |
| + } |
| + if !found || f.MaxTXID > max { |
| + max, found = f.MaxTXID, true |
| + } |
| + } |
| + if !found { |
| + return 0, ErrEmpty |
| + } |
| + return max, nil |
| +} |
| + |
| +// currentFile reads one latest version under base: a current level file |
| +// (ok), not one (a retention tombstone, another generation's key, a marker), |
| +// or a key shaped as a level file that does not parse (ErrListing). Select |
| +// and GenerationMax read the coordinates through it alone. |
| +func currentFile(v lease.ObjectVersion, base string) (File, bool, error) { |
| + if _, ok := coordinates(v.Key, base); !ok { |
| + if strings.HasPrefix(v.Key, base) { |
| + parts := strings.Split(strings.TrimPrefix(v.Key, base), "/") |
| + if len(parts) == 2 && len(parts[0]) == 4 && strings.HasSuffix(parts[1], ".ltx") { |
| + if level, e := strconv.ParseUint(parts[0], 16, 16); e == nil && level <= 9 { |
| + return File{}, false, ErrListing |
| + } |
| + } |
| + } |
| + return File{}, false, nil |
| + } |
| + if v.DeleteMarker { |
| + return File{}, false, nil |
| + } // Retention tombstones are not current files. |
| + f, err := exactFile(v, base) |
| + if err != nil { |
| + return File{}, false, err |
| + } |
| + return f, true, nil |
| +} |
| + |
| // Both entry points share exact-version root selection and continuation. |
| func selectFiles(ctx context.Context, s Reader, bucket, base string, versions []lease.ObjectVersion, |
| latest map[string]lease.ObjectVersion, p *Plan, pin *Pin) (*Plan, error) { |
| @@ -319,23 +391,13 @@ func selectFiles(ctx context.Context, s Reader, bucket, base string, versions [] |
| } |
| var current []File |
| for _, v := range latest { |
| - f, ok := coordinates(v.Key, base) |
| - if !ok && strings.HasPrefix(v.Key, base) { |
| - rel := strings.TrimPrefix(v.Key, base) |
| - parts := strings.Split(rel, "/") |
| - if len(parts) == 2 && len(parts[0]) == 4 && strings.HasSuffix(parts[1], ".ltx") { |
| - if level, e := strconv.ParseUint(parts[0], 16, 16); e == nil && level <= 9 { |
| - return nil, ErrListing |
| - } |
| - } |
| - } |
| - if !ok || v.DeleteMarker { |
| - continue |
| - } // Retention tombstones are not current files. |
| - f, err := exactFile(v, base) |
| + f, ok, err := currentFile(v, base) |
| if err != nil { |
| return nil, err |
| } |
| + if !ok { |
| + continue |
| + } |
| if f.Level == 9 && f.MinTXID != 1 { |
| return nil, ErrRoot |
| } |