Skip to content

Commit 6d22714

Browse files
authored
Ensure partial resources implied by published checkpoint exist (#1057)
Ensure that when a mirror checkpoint is published, all the partial resources implied by that checkpoint's size are present.
1 parent ed4c212 commit 6d22714

2 files changed

Lines changed: 298 additions & 1 deletion

File tree

storage/posix/files.go

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"fmt"
2323
"io/fs"
2424
"iter"
25+
"math/bits"
2526
"net/http"
2627
"os"
2728
"path/filepath"
@@ -539,6 +540,17 @@ func (lrs *logResourceStorage) writeBundleIdempotent(ctx context.Context, index
539540
})
540541
}
541542

543+
// writeTileIdempotent takes care of writing out the serialised tile file if it doesn't already exist.
544+
func (lrs *logResourceStorage) writeTileIdempotent(ctx context.Context, level uint64, index uint64, partial uint8, tile []byte) error {
545+
return otel.TraceErr(ctx, "tessera.storage.posix.writeTileIdempotent", tracer, func(ctx context.Context, span trace.Span) error {
546+
tPath := layout.TilePath(level, index, partial)
547+
if err := lrs.s.createIdempotent(tPath, tile); err != nil {
548+
return err
549+
}
550+
return nil
551+
})
552+
}
553+
542554
// initialise ensures that the storage location is valid by loading the checkpoint from this location, or
543555
// creating a zero-sized one if it doesn't already exist.
544556
func (a *appender) initialise(ctx context.Context) error {
@@ -1201,7 +1213,7 @@ func (m *MirrorWriter) initialise(ctx context.Context) error {
12011213
return nil
12021214
}
12031215

1204-
// marshalTlogEntryBundle returns a tlog-tiles compatible serialization of the provided entrybundle.
1216+
// marshalTlogEntryBundle returns a tlog-tiles compatible serialization of the provided entry bundle.
12051217
func marshalTlogEntryBundle(b *api.EntryBundle) ([]byte, error) {
12061218
// Prealloc the max size we could possibly write out (about 16MB for a full bundle of max size entries).
12071219
// If this causes problems we may want to default this to some lower
@@ -1351,5 +1363,151 @@ func (m *MirrorWriter) UpdateCheckpoint(ctx context.Context, fn func(old []byte)
13511363
return fmt.Errorf("update function returned error: %w", err)
13521364
}
13531365

1366+
_, newSize, _, err := parse.CheckpointUnsafe(newCP)
1367+
if err != nil {
1368+
return fmt.Errorf("failed to parse checkpoint: %v", err)
1369+
}
1370+
1371+
treeSize, err := m.IntegratedSize(ctx)
1372+
if err != nil {
1373+
return fmt.Errorf("failed to get integrated size: %v", err)
1374+
}
1375+
if err := m.ensureGeometry(ctx, newSize, treeSize); err != nil {
1376+
return err
1377+
}
1378+
13541379
return m.s.createOverwrite(layout.CheckpointPath, newCP)
13551380
}
1381+
1382+
// ensureGeometry checks that the partial tiles and entry bundles implied by the new size are
1383+
// present in the stored resources.
1384+
//
1385+
// If an implied partial resource is not already present, this function will attempt to create
1386+
// it from a strictly larger resource whose presence is implied by treeSize.
1387+
func (m *MirrorWriter) ensureGeometry(ctx context.Context, cpSize, treeSize uint64) error {
1388+
if cpSize == 0 {
1389+
return nil
1390+
}
1391+
if cpSize > treeSize {
1392+
return fmt.Errorf("new size %d is greater than integrated size %d", cpSize, treeSize)
1393+
}
1394+
ml := maxLevel(cpSize)
1395+
idx := (cpSize - 1) >> layout.TileHeight
1396+
for l := uint64(0); l <= uint64(ml); l, idx = l+1, idx>>layout.TileHeight {
1397+
treeP := layout.PartialTileSize(l, idx, treeSize)
1398+
cpP := layout.PartialTileSize(l, idx, cpSize)
1399+
if l == 0 {
1400+
if err := m.ensurePartialBundle(ctx, idx, cpP, treeP); err != nil {
1401+
return err
1402+
}
1403+
}
1404+
if err := m.ensurePartialTile(ctx, l, idx, cpP, treeP); err != nil {
1405+
return err
1406+
}
1407+
}
1408+
return nil
1409+
}
1410+
1411+
// maxLevel returns the maximum tile level for a tree of the given size.
1412+
func maxLevel(sz uint64) int {
1413+
if sz == 0 {
1414+
return 0
1415+
}
1416+
return (bits.Len64(sz) - 1) / layout.TileHeight
1417+
}
1418+
1419+
// ensurePartialBundle ensures that the partial entry bundle implied by the new size is
1420+
// present in the stored resources.
1421+
//
1422+
// If the implied partial entry bundle is not already present, this function will attempt to create
1423+
// it from the entry bundle implied by treeSize.
1424+
func (m *MirrorWriter) ensurePartialBundle(ctx context.Context, idx uint64, cpP, treeP uint8) error {
1425+
if cpP == treeP {
1426+
return nil
1427+
}
1428+
1429+
// Check if the entry bundle already exists.
1430+
_, err := m.s.stat(m.logStorage.entriesPath(idx, cpP))
1431+
switch {
1432+
case errors.Is(err, os.ErrNotExist):
1433+
// Need to do the work.
1434+
case err != nil:
1435+
return fmt.Errorf("failed to stat entry bundle @%d.%d: %v", idx, cpP, err)
1436+
default:
1437+
// Already exists, we're done!
1438+
return nil
1439+
}
1440+
1441+
// Read bundle implied by the tree size...
1442+
d, err := m.logStorage.ReadEntryBundle(ctx, idx, treeP)
1443+
if err != nil {
1444+
return fmt.Errorf("failed to read entry bundle @%d.%d: %v", idx, treeP, err)
1445+
}
1446+
eb := &api.EntryBundle{}
1447+
if err := eb.UnmarshalText(d); err != nil {
1448+
return fmt.Errorf("failed to unmarshal entry bundle @%d.%d: %v", idx, treeP, err)
1449+
}
1450+
1451+
// Then trim it down to the size implied by the checkpoint, and write it out.
1452+
// Handle cpP == 0 where a full-bundle is implied - we should never actually hit this case since cpP must
1453+
// equal treeP in this case, but it doesn't hurt to be defensive.
1454+
if cpP > 0 {
1455+
eb.Entries = eb.Entries[:cpP]
1456+
}
1457+
d, err = marshalTlogEntryBundle(eb)
1458+
if err != nil {
1459+
return fmt.Errorf("failed to marshal entry bundle @%d.%d: %v", idx, cpP, err)
1460+
}
1461+
if err := m.logStorage.writeBundleIdempotent(ctx, idx, cpP, d); err != nil {
1462+
return fmt.Errorf("failed to write entry bundle @%d.%d: %v", idx, cpP, err)
1463+
}
1464+
return nil
1465+
}
1466+
1467+
// ensurePartialTile ensures that the partial tile implied by the new size is
1468+
// present in the stored resources.
1469+
//
1470+
// If the implied partial tile is not already present, this function will attempt to create
1471+
// it from the tile implied by treeSize.
1472+
func (m *MirrorWriter) ensurePartialTile(ctx context.Context, l uint64, idx uint64, cpP, treeP uint8) error {
1473+
if cpP == treeP {
1474+
return nil
1475+
}
1476+
1477+
// Check if the tile already exists.
1478+
_, err := m.s.stat(layout.TilePath(l, idx, cpP))
1479+
switch {
1480+
case errors.Is(err, os.ErrNotExist):
1481+
// Need to do the work.
1482+
case err != nil:
1483+
return fmt.Errorf("failed to stat tile @%d/%d.%d: %v", l, idx, cpP, err)
1484+
default:
1485+
// Already exists, we're done!
1486+
return nil
1487+
}
1488+
1489+
// Read tile implied by the tree size...
1490+
d, err := m.logStorage.ReadTile(ctx, l, idx, treeP)
1491+
if err != nil {
1492+
return fmt.Errorf("failed to read tile @%d/%d.%d: %v", l, idx, treeP, err)
1493+
}
1494+
t := &api.HashTile{}
1495+
if err := t.UnmarshalText(d); err != nil {
1496+
return fmt.Errorf("failed to unmarshal tile @%d/%d.%d: %v", l, idx, treeP, err)
1497+
}
1498+
1499+
// Then trim it down to the size implied by the checkpoint, and write it out.
1500+
// Handle cpP == 0 where a full-tile is implied - we should never actually hit this case since cpP must
1501+
// equal treeP in this case, but it doesn't hurt to be defensive.
1502+
if cpP > 0 {
1503+
t.Nodes = t.Nodes[:cpP]
1504+
}
1505+
d, err = t.MarshalText()
1506+
if err != nil {
1507+
return fmt.Errorf("failed to marshal tile @%d/%d.%d: %v", l, idx, cpP, err)
1508+
}
1509+
if err := m.logStorage.writeTileIdempotent(ctx, l, idx, cpP, d); err != nil {
1510+
return fmt.Errorf("failed to write tile @%d/%d.%d: %v", l, idx, cpP, err)
1511+
}
1512+
return nil
1513+
}

storage/posix/files_test.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ package posix
1717
import (
1818
"bytes"
1919
"context"
20+
"encoding/base64"
2021
"errors"
2122
"fmt"
2223
"net/http"
2324
"os"
2425
"path/filepath"
26+
"slices"
2527
"strings"
2628
"testing"
2729
"time"
@@ -757,3 +759,140 @@ func TestMirrorWriter_IntegrateBundles(t *testing.T) {
757759
})
758760
}
759761
}
762+
763+
func TestMaxLevel(t *testing.T) {
764+
for _, tc := range []struct {
765+
size uint64
766+
want int
767+
}{
768+
{0, 0},
769+
{1, 0},
770+
{256, 1},
771+
{257, 1},
772+
{32769, 1},
773+
{65535, 1},
774+
{65536, 2},
775+
} {
776+
t.Run(fmt.Sprintf("size_%d", tc.size), func(t *testing.T) {
777+
if got := maxLevel(tc.size); got != tc.want {
778+
t.Fatalf("maxLevel(%d): got %d, want %d", tc.size, got, tc.want)
779+
}
780+
})
781+
}
782+
}
783+
784+
func TestMirrorWriter_UpdateCheckpointGeometry(t *testing.T) {
785+
ctx := t.Context()
786+
s := &Storage{
787+
cfg: Config{
788+
Path: t.TempDir(),
789+
},
790+
}
791+
opts := tessera.NewMirrorOptions()
792+
mw, lr, err := s.MirrorWriter(ctx, opts)
793+
if err != nil {
794+
t.Fatalf("MirrorWriter: %v", err)
795+
}
796+
797+
// Create 600 entries to span multiple tile levels.
798+
entries := make([][]byte, 600)
799+
for i := range 600 {
800+
entries[i] = fmt.Appendf(nil, "entry %d", i)
801+
}
802+
803+
bundles := []*api.EntryBundle{
804+
{Entries: entries[0:256]},
805+
{Entries: entries[256:512]},
806+
{Entries: entries[512:600]},
807+
}
808+
bundlesIter := func(yield func(*api.EntryBundle, error) bool) {
809+
for _, b := range bundles {
810+
if !yield(b, nil) {
811+
return
812+
}
813+
}
814+
}
815+
816+
// Integrate to size 600.
817+
// Tiles layout should then be:
818+
// Level 1: [2]
819+
// Level 0: [256] [256] [88]
820+
// Entries: [256] [256] [88]
821+
size, _, err := mw.IntegrateBundles(ctx, 0, bundlesIter)
822+
if err != nil {
823+
t.Fatalf("IntegrateBundles: %v", err)
824+
}
825+
if size != 600 {
826+
t.Fatalf("expected integrated size 600, got %d", size)
827+
}
828+
829+
for _, test := range []struct {
830+
size uint64
831+
wantRHSizes []int
832+
}{
833+
{0, []int{}},
834+
{1, []int{1}},
835+
{129, []int{129}},
836+
{256, []int{256, 1}},
837+
{300, []int{44, 2}},
838+
{600, []int{88, 2}},
839+
} {
840+
t.Run(fmt.Sprintf("size_%d", test.size), func(t *testing.T) {
841+
h := make([]byte, 32)
842+
cpStr1 := fmt.Sprintf("origin\n%d\n%s\nsig\n", test.size, base64.StdEncoding.EncodeToString(h))
843+
if err := mw.UpdateCheckpoint(ctx, func(old []byte) ([]byte, error) {
844+
return []byte(cpStr1), nil
845+
}); err != nil {
846+
t.Fatalf("UpdateCheckpoint (size %d): %v", test.size, err)
847+
}
848+
849+
if test.size == 0 {
850+
return
851+
}
852+
853+
idx := (test.size - 1) / layout.EntryBundleWidth
854+
p := uint8(test.wantRHSizes[0])
855+
eb := mustReadEntryBundle(t, lr, idx, p)
856+
if l := len(eb.Entries); l != test.wantRHSizes[0] {
857+
t.Fatalf("expected %d entries in partial bundle %d.%d, got %d", test.wantRHSizes[0], idx, p, l)
858+
}
859+
if got, want := eb.Entries, bundles[idx].Entries[:test.wantRHSizes[0]]; !slices.EqualFunc(got, want, bytes.Equal) {
860+
t.Errorf("entrybundle doesn't match:\ngot %v\nwant %v", got, want)
861+
}
862+
// Verify the existence/correctness of the partial tiles for the given size.
863+
for level, p := range test.wantRHSizes {
864+
tile := mustReadTile(t, lr, uint64(level), idx, uint8(p))
865+
if len(tile.Nodes) != p {
866+
t.Errorf("expected %d nodes in partial tile %d/%d.%d, got %d", p, level, idx, p, len(tile.Nodes))
867+
}
868+
idx >>= layout.TileHeight
869+
}
870+
})
871+
}
872+
}
873+
874+
func mustReadTile(t *testing.T, lr tessera.LogReader, level, idx uint64, p uint8) *api.HashTile {
875+
t.Helper()
876+
d, err := lr.ReadTile(t.Context(), level, idx, p)
877+
if err != nil {
878+
t.Fatalf("failed to read tile: %v", err)
879+
}
880+
tile := &api.HashTile{}
881+
if err := tile.UnmarshalText(d); err != nil {
882+
t.Fatalf("failed to unmarshal tile: %v", err)
883+
}
884+
return tile
885+
}
886+
887+
func mustReadEntryBundle(t *testing.T, lr tessera.LogReader, idx uint64, p uint8) *api.EntryBundle {
888+
t.Helper()
889+
d, err := lr.ReadEntryBundle(t.Context(), idx, p)
890+
if err != nil {
891+
t.Fatalf("failed to read entry bundle: %v", err)
892+
}
893+
eb := &api.EntryBundle{}
894+
if err := eb.UnmarshalText(d); err != nil {
895+
t.Fatalf("failed to unmarshal entry bundle: %v", err)
896+
}
897+
return eb
898+
}

0 commit comments

Comments
 (0)