diff --git a/rbf/cursor.go b/rbf/cursor.go index 75b05b9bc..697956ebd 100644 --- a/rbf/cursor.go +++ b/rbf/cursor.go @@ -16,9 +16,15 @@ const ( BitmapN = (1 << 16) / 64 ) +// Cursor represents an object for traversing forward and backward in the +// keyspace. Users should initialize a cursor with either First(), Last(), or +// Seek() and then move forward or backward using Next() or Prev(), respectively. +// +// If you mutate the b-tree that a cursor is attached to then you'll need to +// re-initialize the cursor. This may be changed in the future though. type Cursor struct { tx *Tx - buffered bool + buffered bool // if true, Next() and Prev() do not move the cursor position // buffers array [ArrayMaxSize + 1]uint16 @@ -30,7 +36,7 @@ type Cursor struct { } type searchStack struct { - top int + top int // current depth in elems elems [32]stackElem } @@ -118,6 +124,7 @@ func maybeConvertOversizedRunToBitmap(runs []roaring.Interval16, bitN int, key u } // Add sets a bit on the underlying bitmap. +// If changed is true, this cursor will need to be reinitialized before use. func (c *Cursor) Add(v uint64) (changed bool, err error) { hi, lo := highbits(v), lowbits(v) // Move cursor to the key of the container. @@ -188,6 +195,7 @@ func (c *Cursor) Add(v uint64) (changed bool, err error) { } // Remove unsets a bit on the underlying bitmap. +// If changed is true, the cursor will need to be reinitialized before use. func (c *Cursor) Remove(v uint64) (changed bool, err error) { hi, lo := highbits(v), lowbits(v) @@ -354,6 +362,21 @@ func toPgno(val []byte) uint32 { return binary.LittleEndian.Uint32(val) } +// putLeafCell inserts or updates a cell into the currently position leaf page +// in the stack. If in.Key exists in the page, the cell is updated. Otherwise, +// cell is inserted. +// +// This method has two paths. First, a fast path is used if we know the insert +// will not cause the page to grow past the max page size. This path simply +// shifts bytes around in the page to make room for the new cell. The second +// path is the slow path where a page may split into two pages. This causes the +// entire page to be deserialized so that pages can more easily be split. +// +// The slow path may cause a cascade of changes to parent pages as a page split +// creates a new entry in the parent. If that entry overflows the parent then +// the parent branch page will split which could cascade up to the root. If the +// root page splits, a new branch page is created but retains the original root +// page number so that the root records do not need to be updated. func (c *Cursor) putLeafCell(in leafCell) (err error) { elem := &c.stack.elems[c.stack.top] leafPage, isHeap, err := c.tx.readPage(elem.pgno) // the last read leaf page @@ -610,6 +633,9 @@ func (c *Cursor) putLeafCellFast(in leafCell, isInsert bool) (err error) { } // deleteLeafCell removes a cell from the currently positioned page & index. +// If the removal causes the leaf page to have no more elements then its entry +// is removed from the parent. If the removal changes the first entry in the +// leaf page then the entry will be updated in the parent branch page. func (c *Cursor) deleteLeafCell(key uint64) (err error) { elem := &c.stack.elems[c.stack.top] leafPage, _, err := c.tx.readPage(elem.pgno) @@ -661,7 +687,9 @@ func (c *Cursor) deleteLeafCell(key uint64) (err error) { return nil } -// putBranchCells updates a branch page with one or more cells. +// putBranchCells updates a branch page with one or more cells. If the cells +// cause the branch page to split then a new entry will be created in the +// parent page. If this branch page is a root then a new root will be created. func (c *Cursor) putBranchCells(stackIndex int, newCells []branchCell) (err error) { elem := &c.stack.elems[stackIndex] @@ -725,8 +753,6 @@ func (c *Cursor) putBranchCells(stackIndex int, newCells []branchCell) (err erro } } - // TODO(BBJ): Check if key on page changes and update parent if so. - // TODO(BBJ): Update page in buffer & cursor stack. // If this is not a split, then exit now. @@ -745,7 +771,8 @@ func (c *Cursor) putBranchCells(stackIndex int, newCells []branchCell) (err erro return c.putBranchCells(stackIndex-1, parents) } -// updateBranchCell updates the key for cell in the branch. +// updateBranchCell updates the key for cell in the branch. Branch elements +// are fixed size so this cannot cause a page split. func (c *Cursor) updateBranchCell(stackIndex int, newKey uint64) (err error) { elem := &c.stack.elems[stackIndex] @@ -781,7 +808,10 @@ func (c *Cursor) updateBranchCell(stackIndex int, newKey uint64) (err error) { return nil } -// deleteBranchCell removes a cell from a branch page. +// deleteBranchCell removes a cell from a branch page. If no more elements +// exist in a branch page then it is removed. If an empty branch page is the +// root page then it is converted to an empty leaf page as branch pages are not +// allowed to be empty. func (c *Cursor) deleteBranchCell(stackIndex int, key uint64) (err error) { elem := &c.stack.elems[stackIndex] @@ -859,6 +889,8 @@ func (c *Cursor) deleteBranchCell(stackIndex int, key uint64) (err error) { } // writeRoot writes a new branch page at the root with the given cells. +// The root of a b-tree should always stay the same, even in the case of a +// split. This allows us to not rewrite the root record for the b-tree. func (c *Cursor) writeRoot(pgno uint32, cells []branchCell) error { var buf [PageSize]byte writePageNo(buf[:], pgno) @@ -940,7 +972,9 @@ func pageKeyAt(page []byte, index int) uint64 { return *(*uint64)(unsafe.Pointer(&page[offset])) } -// First moves to the first element of the btree. +// First moves to the first element of the btree. Returns io.EOF if no elements +// exist. The first call to Next() will not move the position but subsequent +// calls will move the position forward until it reaches the end and return io.EOF. func (c *Cursor) First() error { c.buffered = true @@ -980,7 +1014,10 @@ func (c *Cursor) First() error { } } -// Last moves to the last element of the btree. +// Last moves to the last element of the btree. Returns io.EOF if there are no +// elements. The first call to Prev() will not move the position but subsequent +// calls will move the position backward until it reaches the beginning and +// return io.EOF. func (c *Cursor) Last() error { // c.stack.elems[0].pgno = c.root c.buffered = true @@ -1101,7 +1138,8 @@ func (c *Cursor) Next() error { return c.goNextPage() } -// Prev moves to the previous element of the btree. +// Prev moves to the previous element of the btree. Returns io.EOF if at the +// beginning of the b-tree. func (c *Cursor) Prev() error { if c.buffered { c.buffered = false @@ -1211,6 +1249,9 @@ var _ = (&stackElem{}).clear var _ = (&stackElem{}).String var _ = (&stackElem{}).equal +// goNextPage moves up the stack until an element has additional elements +// available after the current position. It then traverses back down the stack +// to position at the next leaf page element. func (c *Cursor) goNextPage() error { for c.stack.top--; c.stack.top >= 0; c.stack.top-- { elem := &c.stack.elems[c.stack.top] diff --git a/rbf/tx.go b/rbf/tx.go index a47790d9c..9c7bc2d56 100644 --- a/rbf/tx.go +++ b/rbf/tx.go @@ -19,7 +19,10 @@ import ( var _ = txkey.ToString -// Tx represents a transaction. +// Tx represents an RBF transaction. Transactions provide guarantees such as +// atomicity for all writes that occur as well as serializable isolation. +// Transactions can be obtained by calling DB.Begin() and provide a snapshot +// view at the point-in-time they are started. type Tx struct { mu sync.RWMutex db *DB // parent db @@ -71,11 +74,13 @@ type Tx struct { stack []byte } +// DBPath returns the path to the directory that holds the parent database. func (tx *Tx) DBPath() string { return tx.db.Path } -// Writable returns true if the transaction can mutate data. +// Writable returns true if the transaction can mutate data. Using transaction +// methods that attempt to write will return ErrTxNotWritable. func (tx *Tx) Writable() bool { return tx.writable } @@ -95,7 +100,12 @@ func (tx *Tx) PageN() int { return int(readMetaPageN(tx.meta[:])) } -// Commit completes the transaction and persists data changes. +// Commit completes the transaction and persists data changes. If this method +// fails, changes may or may not have been persisted to disk. If no changes have +// been made during the transaction, this functions the same as a rollback. +// +// Attempting to commit an already committed or rolled back transaction will +// return an ErrTxClosed error. func (tx *Tx) Commit() error { tx.mu.Lock() defer tx.mu.Unlock() @@ -193,6 +203,9 @@ func (tx *Tx) truncateLastFreePage() (truncated bool, outErr error) { return true, nil } +// Rollback discards any changes that have been made by the transaction. +// A commit or rollback must always be called after a transaction finishes. +// If this is a writable transaction, the write lock will be released on the DB. func (tx *Tx) Rollback() { tx.rollback(false) } func (tx *Tx) rollback(hasDBLock bool) { @@ -225,11 +238,13 @@ func (tx *Tx) Root(name string) (uint32, error) { } func (tx *Tx) root(name string) (uint32, error) { + // Fetch list of roots (or retrieve from cache). records, err := tx.RootRecords() if err != nil { return 0, err } + // Lookup record by bitmap name and return its root page. pgno, ok := records.Get(name) if !ok { return 0, ErrBitmapNotFound @@ -260,7 +275,7 @@ func (tx *Tx) BitmapNames() ([]string, error) { return a, nil } -// BitmapExist returns true if bitmap exists. +// BitmapExist returns true if bitmap exists. Returns an error if name is empty. func (tx *Tx) BitmapExists(name string) (bool, error) { tx.mu.Lock() defer tx.mu.Unlock()