Skip to content

Commit

Permalink
feat: make paging behavior more consistent, add tests (#70)
Browse files Browse the repository at this point in the history
  • Loading branch information
kevmo314 authored Jan 28, 2024
1 parent a41083a commit 698a5c4
Show file tree
Hide file tree
Showing 3 changed files with 200 additions and 21 deletions.
84 changes: 71 additions & 13 deletions pkg/btree/multi.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,20 @@ package btree
import (
"encoding/binary"
"errors"
"fmt"
"io"
)

/**
* LinkedMetaPage is a linked list of meta pages. Each page contains
* a pointer to the root of the B+ tree, a pointer to the next meta page,
* and the remainder of the page is allocated as free space for metadata.
*
* A page exists if and only if the offset is not math.MaxUint64 and the
* read/write/seek pager can read one full page at the offset. The last
* page in the linked list will have a next pointer with offset
* math.MaxUint64.
*/
type LinkedMetaPage struct {
rws ReadWriteSeekPager
offset uint64
Expand Down Expand Up @@ -66,20 +77,16 @@ func (m *LinkedMetaPage) Next() (*LinkedMetaPage, error) {
if err := binary.Read(m.rws, binary.LittleEndian, &next); err != nil {
return nil, err
}
if next.Offset == 0 {
return nil, nil
}
return &LinkedMetaPage{rws: m.rws, offset: next.Offset}, nil
}

func (m *LinkedMetaPage) AddNext() (*LinkedMetaPage, error) {
// check that the next pointer is zero
curr, err := m.Next()
if err != nil {
return nil, err
}
if curr != nil {
return nil, errors.New("next pointer is not zero")
if curr.offset != ^uint64(0) {
return nil, errors.New("next pointer already exists")
}
offset, err := m.rws.NewPage()
if err != nil {
Expand All @@ -104,24 +111,75 @@ func (m *LinkedMetaPage) MemoryPointer() MemoryPointer {
}

func (m *LinkedMetaPage) Exists() (bool, error) {
length, err := m.rws.Seek(0, io.SeekEnd)
if err != nil {
if m.offset == ^uint64(0) {
return false, nil
}
// attempt to read the page
if _, err := m.rws.Seek(int64(m.offset), io.SeekStart); err != nil {
return false, err
}
return length > int64(m.offset), nil
if _, err := m.rws.Read(make([]byte, m.rws.PageSize())); err != nil {
if err == io.EOF {
return false, nil
}
return false, err
}
return true, nil
}

func (m *LinkedMetaPage) Reset() error {
// write a full page of zeros
emptyPage := make([]byte, m.rws.PageSize())
binary.BigEndian.PutUint64(emptyPage[12:20], ^uint64(0))
if _, err := m.rws.Seek(int64(m.offset), io.SeekStart); err != nil {
return err
}
// write 28 bytes of zeros
if _, err := m.rws.Write(make([]byte, 28)); err != nil {
if _, err := m.rws.Write(emptyPage); err != nil {
return err
}
return nil
}

func NewMultiBPTree(t ReadWriteSeekPager, offset uint64) *LinkedMetaPage {
return &LinkedMetaPage{rws: t, offset: offset}
// Collect returns a slice of all linked meta pages from this page to the end.
// This function is useful for debugging and testing, however generally it should
// not be used for functional code.
func (m *LinkedMetaPage) Collect() ([]*LinkedMetaPage, error) {
var pages []*LinkedMetaPage
node := m
for {
exists, err := node.Exists()
if err != nil {
return nil, err
}
if !exists {
break
}
pages = append(pages, node)
next, err := node.Next()
if err != nil {
return nil, err
}
node = next
}
return pages, nil
}

func (m *LinkedMetaPage) String() string {
nm, err := m.Next()
if err != nil {
panic(err)
}
root, err := m.Root()
if err != nil {
panic(err)
}
return fmt.Sprintf("LinkedMetaPage{offset: %x,\tnext: %x,\troot: %x}", m.offset, nm.offset, root.Offset)
}

func NewMultiBPTree(t ReadWriteSeekPager, page int) (*LinkedMetaPage, error) {
offset, err := t.Page(0)
if err != nil {
return nil, err
}
return &LinkedMetaPage{rws: t, offset: uint64(offset)}, nil
}
134 changes: 127 additions & 7 deletions pkg/btree/multi_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ func TestMultiBPTree(t *testing.T) {
if err != nil {
t.Fatal(err)
}
tree := NewMultiBPTree(p, uint64(p.PageSize()))
tree, err := NewMultiBPTree(p, 0)
if err != nil {
t.Fatal(err)
}
exists, err := tree.Exists()
if err != nil {
t.Fatal(err)
}
if exists {
t.Fatal("expected not found")
t.Fatalf("expected not found, got page %v", tree)
}
})

Expand All @@ -30,7 +33,10 @@ func TestMultiBPTree(t *testing.T) {
if err != nil {
t.Fatal(err)
}
tree := NewMultiBPTree(p, uint64(p.PageSize()))
tree, err := NewMultiBPTree(p, 0)
if err != nil {
t.Fatal(err)
}
if err := tree.Reset(); err != nil {
t.Fatal(err)
}
Expand All @@ -53,7 +59,10 @@ func TestMultiBPTree(t *testing.T) {
if err != nil {
t.Fatal(err)
}
tree := NewMultiBPTree(p, uint64(p.PageSize()))
tree, err := NewMultiBPTree(p, 0)
if err != nil {
t.Fatal(err)
}
if err := tree.Reset(); err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -92,7 +101,10 @@ func TestMultiBPTree(t *testing.T) {
if err != nil {
t.Fatal(err)
}
tree := NewMultiBPTree(p, uint64(p.PageSize()))
tree, err := NewMultiBPTree(p, 0)
if err != nil {
t.Fatal(err)
}
if err := tree.Reset(); err != nil {
t.Fatal(err)
}
Expand All @@ -115,7 +127,10 @@ func TestMultiBPTree(t *testing.T) {
if err != nil {
t.Fatal(err)
}
tree := NewMultiBPTree(p, uint64(p.PageSize()))
tree, err := NewMultiBPTree(p, 0)
if err != nil {
t.Fatal(err)
}
if err := tree.Reset(); err != nil {
t.Fatal(err)
}
Expand All @@ -134,7 +149,10 @@ func TestMultiBPTree(t *testing.T) {
if err != nil {
t.Fatal(err)
}
tree := NewMultiBPTree(p, uint64(p.PageSize()))
tree, err := NewMultiBPTree(p, 0)
if err != nil {
t.Fatal(err)
}
if err := tree.Reset(); err != nil {
t.Fatal(err)
}
Expand All @@ -149,4 +167,106 @@ func TestMultiBPTree(t *testing.T) {
t.Fatalf("got %v want %v", metadata, []byte("hello"))
}
})

t.Run("setting metadata too large fails", func(t *testing.T) {
b := buftest.NewSeekableBuffer()
p, err := NewPageFile(b)
if err != nil {
t.Fatal(err)
}
tree, err := NewMultiBPTree(p, 0)
if err != nil {
t.Fatal(err)
}
if err := tree.Reset(); err != nil {
t.Fatal(err)
}
if err := tree.SetMetadata(make([]byte, 4096)); err == nil {
t.Fatal("expected error")
}
})

t.Run("collect pages", func(t *testing.T) {
b := buftest.NewSeekableBuffer()
p, err := NewPageFile(b)
if err != nil {
t.Fatal(err)
}
tree, err := NewMultiBPTree(p, 0)
if err != nil {
t.Fatal(err)
}
if err := tree.Reset(); err != nil {
t.Fatal(err)
}

// Create a linked list of LinkedMetaPages
page1, err := tree.AddNext()
if err != nil {
t.Fatal(err)
}
page2, err := page1.AddNext()
if err != nil {
t.Fatal(err)
}
page3, err := page2.AddNext()
if err != nil {
t.Fatal(err)
}

// Collect the pages
collectedPages, err := page1.Collect()
if err != nil {
t.Fatal(err)
}

// Verify the collected pages
expectedPages := []*LinkedMetaPage{page1, page2, page3}
if !reflect.DeepEqual(collectedPages, expectedPages) {
t.Fatalf("got %v, want %v", collectedPages, expectedPages)
}
})

t.Run("singular list", func(t *testing.T) {
b := buftest.NewSeekableBuffer()
p, err := NewPageFile(b)
if err != nil {
t.Fatal(err)
}
tree, err := NewMultiBPTree(p, 0)
if err != nil {
t.Fatal(err)
}
if err := tree.Reset(); err != nil {
t.Fatal(err)
}
collectedPages, err := tree.Collect()
if err != nil {
t.Fatal(err)
}
expectedPages := []*LinkedMetaPage{tree}
if !reflect.DeepEqual(collectedPages, expectedPages) {
t.Fatalf("got %v, want %v", collectedPages, expectedPages)
}
})

t.Run("empty list", func(t *testing.T) {
b := buftest.NewSeekableBuffer()
p, err := NewPageFile(b)
if err != nil {
t.Fatal(err)
}

tree, err := NewMultiBPTree(p, 0)
if err != nil {
t.Fatal(err)
}
collectedPages, err := tree.Collect()
if err != nil {
t.Fatal(err)
}
if collectedPages != nil {
t.Fatalf("got %v, want nil", collectedPages)
}
})
}
3 changes: 2 additions & 1 deletion pkg/btree/pagefile.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ func (pf *PageFile) Page(i int) (int64, error) {
if i < 0 {
return 0, errors.New("page index cannot be negative")
}
return int64(i) * int64(pf.pageSize), nil
// i + 1 because the first page is reserved for the free page indexes
return int64(i+1) * int64(pf.pageSize), nil
}

func (pf *PageFile) NewPage() (int64, error) {
Expand Down

0 comments on commit 698a5c4

Please sign in to comment.