diff options
author | Ben Johnson <benbjohnson@yahoo.com> | 2014-02-09 15:52:19 -0700 |
---|---|---|
committer | Ben Johnson <benbjohnson@yahoo.com> | 2014-02-10 14:04:01 -0700 |
commit | 509e93dff4cedf88d91ba2c99385da0b4e41eb6a (patch) | |
tree | b6dc2efd0908df4ca7ff270181e46a1e4d85a77c | |
parent | Clean up. (diff) | |
download | dedo-509e93dff4cedf88d91ba2c99385da0b4e41eb6a.tar.gz dedo-509e93dff4cedf88d91ba2c99385da0b4e41eb6a.tar.xz |
Add freelist.
-rw-r--r-- | Makefile | 5 | ||||
-rw-r--r-- | assert.go | 2 | ||||
-rw-r--r-- | bucket.go | 14 | ||||
-rw-r--r-- | cursor.go | 10 | ||||
-rw-r--r-- | db.go | 91 | ||||
-rw-r--r-- | db_test.go | 20 | ||||
-rw-r--r-- | error_test.go | 12 | ||||
-rw-r--r-- | freelist.go | 94 | ||||
-rw-r--r-- | freelist_test.go | 95 | ||||
-rw-r--r-- | meta.go | 6 | ||||
-rw-r--r-- | meta_test.go | 18 | ||||
-rw-r--r-- | node.go | 20 | ||||
-rw-r--r-- | os.go | 5 | ||||
-rw-r--r-- | page.go | 5 | ||||
-rw-r--r-- | page_test.go | 21 | ||||
-rw-r--r-- | rwtransaction.go | 34 | ||||
-rw-r--r-- | rwtransaction_test.go | 39 | ||||
-rw-r--r-- | transaction.go | 14 | ||||
-rw-r--r-- | transaction_test.go | 7 |
19 files changed, 412 insertions, 100 deletions
@@ -1,4 +1,3 @@ -PKG=./... TEST=. BENCH=. COVERPROFILE=/tmp/c.out @@ -7,7 +6,7 @@ bench: benchpreq go test -v -test.bench=$(BENCH) ./.bench cover: fmt - go test -coverprofile=$(COVERPROFILE) . + go test -coverprofile=$(COVERPROFILE) -test.run=$(TEST) . go tool cover -html=$(COVERPROFILE) rm $(COVERPROFILE) @@ -15,6 +14,6 @@ fmt: @go fmt ./... test: fmt - @go test -v -cover -test.run=$(TEST) $(PKG) + @go test -v -cover -test.run=$(TEST) .PHONY: bench cover fmt test @@ -7,6 +7,6 @@ import "fmt" // _assert will panic with a given formatted message if the given condition is false. func _assert(condition bool, msg string, v ...interface{}) { if !condition { - panic(fmt.Sprintf("assertion failed: " + msg, v...)) + panic(fmt.Sprintf("assertion failed: "+msg, v...)) } } @@ -23,17 +23,3 @@ func (b *Bucket) cursor() *Cursor { stack: make([]pageElementRef, 0), } } - -func (b *Bucket) Stat() *Stat { - // TODO: Calculate size, depth, page count (by type), entry count, readers, etc. - return nil -} - -type Stat struct { - PageSize int - Depth int - BranchPageCount int - LeafPageCount int - OverflowPageCount int - EntryCount int -} @@ -43,9 +43,7 @@ func (c *Cursor) Get(key []byte) []byte { } func (c *Cursor) search(key []byte, p *page) { - if (p.flags & (p_branch | p_leaf)) == 0 { - panic("invalid page type: " + p.typ()) - } + _assert((p.flags&(p_branch|p_leaf)) != 0, "invalid page type: "+p.typ()) e := pageElementRef{page: p} c.stack = append(c.stack, e) @@ -95,11 +93,6 @@ func (c *Cursor) top() (*page, uint16) { return ptr.page, ptr.index } -// page returns the page that the cursor is currently pointing at. -func (c *Cursor) page() *page { - return c.stack[len(c.stack)-1].page -} - // element returns the leaf element that the cursor is currently positioned on. func (c *Cursor) element() *leafPageElement { ref := c.stack[len(c.stack)-1] @@ -123,4 +116,3 @@ func (c *Cursor) node(t *RWTransaction) *node { _assert(n.pgid == c.stack[len(c.stack)-1].page.id, "node/page mismatch b: %d != %d", n.pgid, c.stack[len(c.stack)-1].page.id) return n } - @@ -25,10 +25,14 @@ type DB struct { meta0 *meta meta1 *meta pageSize int - opened bool - mutex sync.Mutex + opened bool rwtransaction *RWTransaction transactions []*Transaction + freelist *freelist + + rwlock sync.Mutex // Allows only one writer at a time. + metalock sync.Mutex // Protects meta page access. + mmaplock sync.RWMutex // Protects mmap access during remapping. } // NewDB creates a new DB instance. @@ -45,8 +49,8 @@ func (db *DB) Path() string { // If the file does not exist then it will be created automatically. func (db *DB) Open(path string, mode os.FileMode) error { var err error - db.mutex.Lock() - defer db.mutex.Unlock() + db.metalock.Lock() + defer db.metalock.Unlock() // Initialize OS/Syscall references. // These are overridden by mocks during some tests. @@ -99,6 +103,10 @@ func (db *DB) Open(path string, mode os.FileMode) error { return err } + // Read in the freelist. + db.freelist = &freelist{pending: make(map[txnid][]pgid)} + db.freelist.read(db.page(db.meta().freelist)) + // Mark the database as opened and return. db.opened = true return nil @@ -113,7 +121,7 @@ func (db *DB) mmap() error { return &Error{"file size too small", err} } - // TEMP(benbjohnson): Set max size to 1MB. + // TODO(benbjohnson): Determine appropriate mmap size by db size. size := 2 << 30 // Memory-map the data file as a byte slice. @@ -154,7 +162,7 @@ func (db *DB) init() error { m.version = version m.pageSize = uint32(db.pageSize) m.version = version - m.free = 2 + m.freelist = 2 m.buckets = 3 m.pgid = 4 m.txnid = txnid(i) @@ -182,20 +190,21 @@ func (db *DB) init() error { // Close releases all resources related to the database. func (db *DB) Close() { - db.mutex.Lock() - defer db.mutex.Unlock() + db.metalock.Lock() + defer db.metalock.Unlock() db.close() } func (db *DB) close() { // TODO: Undo everything in Open(). + db.freelist = nil } // Transaction creates a read-only transaction. // Multiple read-only transactions can be used concurrently. func (db *DB) Transaction() (*Transaction, error) { - db.mutex.Lock() - defer db.mutex.Unlock() + db.metalock.Lock() + defer db.metalock.Unlock() // Exit if the database is not open yet. if !db.opened { @@ -206,30 +215,60 @@ func (db *DB) Transaction() (*Transaction, error) { t := &Transaction{} t.init(db) + // Keep track of transaction until it closes. + db.transactions = append(db.transactions, t) + return t, nil } // RWTransaction creates a read/write transaction. // Only one read/write transaction is allowed at a time. func (db *DB) RWTransaction() (*RWTransaction, error) { - db.mutex.Lock() - defer db.mutex.Unlock() + db.metalock.Lock() + defer db.metalock.Unlock() - // TODO: db.writerMutex.Lock() - // TODO: Add unlock to RWTransaction.Commit() / Abort() + // Obtain writer lock. This is released by the RWTransaction when it closes. + db.rwlock.Lock() // Exit if the database is not open yet. if !db.opened { + db.rwlock.Unlock() return nil, DatabaseNotOpenError } // Create a transaction associated with the database. t := &RWTransaction{nodes: make(map[pgid]*node)} t.init(db) + db.rwtransaction = t + + // Free any pages associated with closed read-only transactions. + var minid txnid = 0xFFFFFFFFFFFFFFFF + for _, t := range db.transactions { + if t.id() < minid { + minid = t.id() + } + } + if minid > 0 { + db.freelist.release(minid - 1) + } return t, nil } +// removeTransaction removes a transaction from the database. +func (db *DB) removeTransaction(t *Transaction) { + db.metalock.Lock() + defer db.metalock.Unlock() + + // Remove the transaction. + for i, txn := range db.transactions { + if txn == t { + db.transactions = append(db.transactions[:i], db.transactions[i+1:]...) + break + } + } +} + // Bucket retrieves a reference to a bucket. func (db *DB) Bucket(name string) (*Bucket, error) { t, err := db.Transaction() @@ -372,8 +411,32 @@ func (db *DB) meta() *meta { return db.meta1 } +// allocate returns a contiguous block of memory starting at a given page. +func (db *DB) allocate(count int) *page { + // Allocate a temporary buffer for the page. + buf := make([]byte, count*db.pageSize) + p := (*page)(unsafe.Pointer(&buf[0])) + p.overflow = uint32(count - 1) + + // Use pages from the freelist if they are available. + if p.id = db.freelist.allocate(count); p.id != 0 { + return p + } + + // TODO(benbjohnson): Resize mmap(). + + // If there are no free pages then allocate from the end of the file. + p.id = db.rwtransaction.meta.pgid + db.rwtransaction.meta.pgid += pgid(count) + + return p +} + // sync flushes the file descriptor to disk. func (db *DB) sync(force bool) error { + if db.opened { + return DatabaseAlreadyOpenedError + } if err := syscall.Fsync(int(db.file.Fd())); err != nil { return err } @@ -167,6 +167,26 @@ func TestDBDelete(t *testing.T) { }) } +// Ensure that the database can be copied to a writer. +func TestDBCopy(t *testing.T) { + t.Skip("pending") // TODO(benbjohnson) +} + +// Ensure that the database can be copied to a file path. +func TestDBCopyFile(t *testing.T) { + t.Skip("pending") // TODO(benbjohnson) +} + +// Ensure that the database can sync to the file system. +func TestDBSync(t *testing.T) { + t.Skip("pending") // TODO(benbjohnson) +} + +// Ensure that an error is returned when a database write fails. +func TestDBWriteFail(t *testing.T) { + t.Skip("pending") // TODO(benbjohnson) +} + // withDB executes a function with a database reference. func withDB(fn func(*DB, string)) { f, _ := ioutil.TempFile("", "bolt-") diff --git a/error_test.go b/error_test.go new file mode 100644 index 0000000..0306985 --- /dev/null +++ b/error_test.go @@ -0,0 +1,12 @@ +package bolt + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +// Ensure that nested errors are appropriately formatted. +func TestError(t *testing.T) { + e := &Error{"one error", &Error{"two error", nil}} + assert.Equal(t, e.Error(), "one error: two error") +} diff --git a/freelist.go b/freelist.go new file mode 100644 index 0000000..cd0bffa --- /dev/null +++ b/freelist.go @@ -0,0 +1,94 @@ +package bolt + +import ( + "sort" + "unsafe" +) + +// freelist represents a list of all pages that are available for allocation. +// It also tracks pages that have been freed but are still in use by open transactions. +type freelist struct { + ids []pgid + pending map[txnid][]pgid +} + +// all returns a list of all free ids and all pending ids in one sorted list. +func (f *freelist) all() []pgid { + ids := make([]pgid, len(f.ids)) + copy(ids, f.ids) + + for _, list := range f.pending { + ids = append(ids, list...) + } + + sort.Sort(reverseSortedPgids(ids)) + return ids +} + +// allocate returns the starting page id of a contiguous list of pages of a given size. +// If a contiguous block cannot be found then 0 is returned. +func (f *freelist) allocate(n int) pgid { + var count int + var previd pgid = 0 + for i, id := range f.ids { + // Reset count if this is not contiguous. + if previd == 0 || previd-id != 1 { + count = 1 + } + + // If we found a contiguous block then remove it and return it. + if count == n { + f.ids = append(f.ids[:i-(n-1)], f.ids[i+1:]...) + _assert(id > 1, "cannot allocate page 0 or 1: %d", id) + return id + } + + previd = id + count++ + } + return 0 +} + +// free releases a page and its overflow for a given transaction id. +func (f *freelist) free(txnid txnid, p *page) { + var ids = f.pending[txnid] + _assert(p.id > 1, "cannot free page 0 or 1: %d", p.id) + for i := 0; i < int(p.overflow+1); i++ { + ids = append(ids, p.id+pgid(i)) + } + f.pending[txnid] = ids +} + +// release moves all page ids for a transaction id (or older) to the freelist. +func (f *freelist) release(txnid txnid) { + for tid, ids := range f.pending { + if tid <= txnid { + f.ids = append(f.ids, ids...) + delete(f.pending, tid) + } + } + sort.Sort(reverseSortedPgids(f.ids)) +} + +// read initializes the freelist from a freelist page. +func (f *freelist) read(p *page) { + ids := ((*[maxAllocSize]pgid)(unsafe.Pointer(&p.ptr)))[0:p.count] + f.ids = make([]pgid, len(ids)) + copy(f.ids, ids) +} + +// write writes the page ids onto a freelist page. All free and pending ids are +// saved to disk since in the event of a program crash, all pending ids will +// become free. +func (f *freelist) write(p *page) { + ids := f.all() + p.flags |= p_freelist + p.count = uint16(len(ids)) + copy(((*[maxAllocSize]pgid)(unsafe.Pointer(&p.ptr)))[:], ids) +} + +type reverseSortedPgids []pgid + +func (s reverseSortedPgids) Len() int { return len(s) } +func (s reverseSortedPgids) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s reverseSortedPgids) Less(i, j int) bool { return s[i] > s[j] } diff --git a/freelist_test.go b/freelist_test.go new file mode 100644 index 0000000..b760acb --- /dev/null +++ b/freelist_test.go @@ -0,0 +1,95 @@ +package bolt + +import ( + "testing" + "unsafe" + + "github.com/stretchr/testify/assert" +) + +// Ensure that a page is added to a transaction's freelist. +func TestFreelistFree(t *testing.T) { + f := &freelist{pending: make(map[txnid][]pgid)} + f.free(100, &page{id: 12}) + assert.Equal(t, f.pending[100], []pgid{12}) +} + +// Ensure that a page and its overflow is added to a transaction's freelist. +func TestFreelistFreeOverflow(t *testing.T) { + f := &freelist{pending: make(map[txnid][]pgid)} + f.free(100, &page{id: 12, overflow: 3}) + assert.Equal(t, f.pending[100], []pgid{12, 13, 14, 15}) +} + +// Ensure that a transaction's free pages can be released. +func TestFreelistRelease(t *testing.T) { + f := &freelist{pending: make(map[txnid][]pgid)} + f.free(100, &page{id: 12, overflow: 1}) + f.free(100, &page{id: 9}) + f.free(102, &page{id: 39}) + f.release(100) + f.release(101) + assert.Equal(t, f.ids, []pgid{13, 12, 9}) + f.release(102) + assert.Equal(t, f.ids, []pgid{39, 13, 12, 9}) +} + +// Ensure that a freelist can find contiguous blocks of pages. +func TestFreelistAllocate(t *testing.T) { + f := &freelist{ids: []pgid{18, 13, 12, 9, 7, 6, 5, 4, 3}} + assert.Equal(t, f.allocate(2), pgid(12)) + assert.Equal(t, f.allocate(1), pgid(18)) + assert.Equal(t, f.allocate(3), pgid(5)) + assert.Equal(t, f.allocate(3), pgid(0)) + assert.Equal(t, f.allocate(2), pgid(3)) + assert.Equal(t, f.allocate(1), pgid(9)) + assert.Equal(t, f.allocate(0), pgid(0)) + assert.Equal(t, f.ids, []pgid{}) +} + +// Ensure that a freelist can deserialize from a freelist page. +func TestFreelistRead(t *testing.T) { + // Create a page. + var buf [4096]byte + page := (*page)(unsafe.Pointer(&buf[0])) + page.flags = p_freelist + page.count = 2 + + // Insert 2 page ids. + ids := (*[3]pgid)(unsafe.Pointer(&page.ptr)) + ids[0] = 23 + ids[1] = 50 + + // Deserialize page into a freelist. + f := &freelist{pending: make(map[txnid][]pgid)} + f.read(page) + + // Ensure that there are two page ids in the freelist. + assert.Equal(t, len(f.ids), 2) + assert.Equal(t, f.ids[0], pgid(23)) + assert.Equal(t, f.ids[1], pgid(50)) +} + +// Ensure that a freelist can serialize into a freelist page. +func TestFreelistWrite(t *testing.T) { + // Create a freelist and write it to a page. + var buf [4096]byte + f := &freelist{ids: []pgid{12, 39}, pending: make(map[txnid][]pgid)} + f.pending[100] = []pgid{28, 11} + f.pending[101] = []pgid{3} + p := (*page)(unsafe.Pointer(&buf[0])) + f.write(p) + + // Read the page back out. + f2 := &freelist{pending: make(map[txnid][]pgid)} + f2.read(p) + + // Ensure that the freelist is correct. + // All pages should be present and in reverse order. + assert.Equal(t, len(f2.ids), 5) + assert.Equal(t, f2.ids[0], pgid(39)) + assert.Equal(t, f2.ids[1], pgid(28)) + assert.Equal(t, f2.ids[2], pgid(12)) + assert.Equal(t, f2.ids[3], pgid(11)) + assert.Equal(t, f2.ids[4], pgid(3)) +} @@ -8,7 +8,7 @@ type meta struct { pageSize uint32 flags uint32 buckets pgid - free pgid + freelist pgid pgid pgid txnid txnid } @@ -28,10 +28,10 @@ func (m *meta) copy(dest *meta) { dest.magic = m.magic dest.version = m.version dest.pageSize = m.pageSize + dest.buckets = m.buckets + dest.freelist = m.freelist dest.pgid = m.pgid - dest.free = m.free dest.txnid = m.txnid - dest.buckets = m.buckets } // write writes the meta onto a page. diff --git a/meta_test.go b/meta_test.go new file mode 100644 index 0000000..c082c64 --- /dev/null +++ b/meta_test.go @@ -0,0 +1,18 @@ +package bolt + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +// Ensure that meta with bad magic is invalid. +func TestMetaValidateMagic(t *testing.T) { + m := &meta{magic: 0x01234567} + assert.Equal(t, m.validate(), InvalidError) +} + +// Ensure that meta with a bad version is invalid. +func TestMetaValidateVersion(t *testing.T) { + m := &meta{magic: magic, version: 200} + assert.Equal(t, m.validate(), VersionMismatchError) +} @@ -9,13 +9,13 @@ import ( // node represents an in-memory, deserialized page. type node struct { transaction *RWTransaction - isLeaf bool - unbalanced bool - key []byte - depth int - pgid pgid - parent *node - inodes inodes + isLeaf bool + unbalanced bool + key []byte + depth int + pgid pgid + parent *node + inodes inodes } // minKeys returns the minimum number of inodes this node should have. @@ -76,7 +76,7 @@ func (n *node) nextSibling() *node { return nil } index := n.parent.childIndex(n) - if index >= n.parent.numChildren() - 1 { + if index >= n.parent.numChildren()-1 { return nil } return n.parent.childAt(index + 1) @@ -354,7 +354,3 @@ type inode struct { } type inodes []inode - -func (s inodes) Len() int { return len(s) } -func (s inodes) Swap(i, j int) { s[i], s[j] = s[j], s[i] } -func (s inodes) Less(i, j int) bool { return bytes.Compare(s[i].key, s[j].key) == -1 } @@ -6,7 +6,6 @@ import ( type _os interface { OpenFile(name string, flag int, perm os.FileMode) (file file, err error) - Stat(name string) (fi os.FileInfo, err error) Getpagesize() int } @@ -23,10 +22,6 @@ func (o *sysos) OpenFile(name string, flag int, perm os.FileMode) (file file, er return os.OpenFile(name, flag, perm) } -func (o *sysos) Stat(name string) (fi os.FileInfo, err error) { - return os.Stat(name) -} - func (o *sysos) Getpagesize() int { return os.Getpagesize() } @@ -81,11 +81,6 @@ func (p *page) branchPageElements() []branchPageElement { return ((*[maxNodesPerPage]branchPageElement)(unsafe.Pointer(&p.ptr)))[:] } -// freelist retrieves a list of page ids from a freelist page. -func (p *page) freelist() []pgid { - return ((*[maxNodesPerPage]pgid)(unsafe.Pointer(&p.ptr)))[0:p.count] -} - // dump writes n bytes of the page to STDERR as hex output. func (p *page) hexdump(n int) { buf := (*[maxAllocSize]byte)(unsafe.Pointer(p))[:n] diff --git a/page_test.go b/page_test.go new file mode 100644 index 0000000..05b0509 --- /dev/null +++ b/page_test.go @@ -0,0 +1,21 @@ +package bolt + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +// Ensure that the page type can be returned in human readable format. +func TestPageTyp(t *testing.T) { + assert.Equal(t, (&page{flags: p_branch}).typ(), "branch") + assert.Equal(t, (&page{flags: p_leaf}).typ(), "leaf") + assert.Equal(t, (&page{flags: p_meta}).typ(), "meta") + assert.Equal(t, (&page{flags: p_buckets}).typ(), "buckets") + assert.Equal(t, (&page{flags: p_freelist}).typ(), "freelist") + assert.Equal(t, (&page{flags: 20000}).typ(), "unknown<4e20>") +} + +// Ensure that the hexdump debugging function doesn't blow up. +func TestPageDump(t *testing.T) { + (&page{id: 256}).hexdump(16) +} diff --git a/rwtransaction.go b/rwtransaction.go index 2e1685b..5058dd3 100644 --- a/rwtransaction.go +++ b/rwtransaction.go @@ -49,8 +49,7 @@ func (t *RWTransaction) DeleteBucket(name string) error { // Remove from buckets page. t.buckets.del(name) - // TODO: Free all pages. - // TODO: Remove cursor. + // TODO(benbjohnson): Free all pages. return nil } @@ -97,8 +96,12 @@ func (t *RWTransaction) Delete(name string, key []byte) error { // Commit writes all changes to disk. func (t *RWTransaction) Commit() error { + defer t.close() + // TODO(benbjohnson): Use vectorized I/O to write out dirty pages. + // TODO(benbjohnson): Move rebalancing to occur immediately after deletion (?). + // Rebalance and spill data onto dirty pages. t.rebalance() t.spill() @@ -128,26 +131,14 @@ func (t *RWTransaction) Rollback() { } func (t *RWTransaction) close() { - // Clear nodes. - t.nodes = nil - - // TODO: Release writer lock. + t.db.rwlock.Unlock() } // allocate returns a contiguous block of memory starting at a given page. func (t *RWTransaction) allocate(count int) *page { - // TODO(benbjohnson): Use pages from the freelist. - - // Allocate a set of contiguous pages from the end of the file. - buf := make([]byte, count*t.db.pageSize) - p := (*page)(unsafe.Pointer(&buf[0])) - p.id = t.meta.pgid - p.overflow = uint32(count - 1) + p := t.db.allocate(count) - // Increment the last page id. - t.meta.pgid += pgid(count) - - // Save it in our page cache. + // Save to our page cache. t.pages[p.id] = p return p @@ -196,6 +187,11 @@ func (t *RWTransaction) spill() { nodes = append(nodes, n.parent) } + // Add node's page to the freelist. + if n.pgid > 0 { + t.db.freelist.free(t.id(), t.page(n.pgid)) + } + // Write nodes to dirty pages. for i, newNode := range newNodes { // Allocate contiguous space for the node. @@ -267,10 +263,6 @@ func (t *RWTransaction) writeMeta() error { return nil } -// TODO(benbjohnson): Look up node by page id instead of by stack. Determine depth recursively by parent. -// TODO(benbjohnson): prevSibling() -// TODO(benbjohnson): nextSibling() - // node creates a node from a page and associates it with a given parent. func (t *RWTransaction) node(pgid pgid, parent *node) *node { // Retrieve node if it has already been fetched. diff --git a/rwtransaction_test.go b/rwtransaction_test.go index 5bf3f9f..88a246f 100644 --- a/rwtransaction_test.go +++ b/rwtransaction_test.go @@ -17,11 +17,12 @@ func TestRWTransaction(t *testing.T) { txn, err := db.RWTransaction() assert.NotNil(t, txn) assert.NoError(t, err) + assert.Equal(t, txn.DB(), db) }) } // Ensure that a bucket can be created and retrieved. -func TestTransactionCreateBucket(t *testing.T) { +func TestRWTransactionCreateBucket(t *testing.T) { withOpenDB(func(db *DB, path string) { // Create a bucket. err := db.CreateBucket("widgets") @@ -35,7 +36,7 @@ func TestTransactionCreateBucket(t *testing.T) { } // Ensure that a bucket cannot be created twice. -func TestTransactionRecreateBucket(t *testing.T) { +func TestRWTransactionRecreateBucket(t *testing.T) { withOpenDB(func(db *DB, path string) { // Create a bucket. err := db.CreateBucket("widgets") @@ -48,7 +49,7 @@ func TestTransactionRecreateBucket(t *testing.T) { } // Ensure that a bucket is created with a non-blank name. -func TestTransactionCreateBucketWithoutName(t *testing.T) { +func TestRWTransactionCreateBucketWithoutName(t *testing.T) { withOpenDB(func(db *DB, path string) { err := db.CreateBucket("") assert.Equal(t, err, &Error{"bucket name cannot be blank", nil}) @@ -56,7 +57,7 @@ func TestTransactionCreateBucketWithoutName(t *testing.T) { } // Ensure that a bucket name is not too long. -func TestTransactionCreateBucketWithLongName(t *testing.T) { +func TestRWTransactionCreateBucketWithLongName(t *testing.T) { withOpenDB(func(db *DB, path string) { err := db.CreateBucket(strings.Repeat("X", 255)) assert.NoError(t, err) @@ -66,6 +67,36 @@ func TestTransactionCreateBucketWithLongName(t *testing.T) { }) } +// Ensure that a bucket can be deleted. +func TestRWTransactionDeleteBucket(t *testing.T) { + t.Skip("pending") // TODO(benbjohnson) +} + +// Ensure that an error is returned when inserting into a bucket that doesn't exist. +func TestRWTransactionPutBucketNotFound(t *testing.T) { + t.Skip("pending") // TODO(benbjohnson) +} + +// Ensure that an error is returned when inserting with an empty key. +func TestRWTransactionPutEmptyKey(t *testing.T) { + t.Skip("pending") // TODO(benbjohnson) +} + +// Ensure that an error is returned when inserting with a key that's too large. +func TestRWTransactionPutKeyTooLarge(t *testing.T) { + t.Skip("pending") // TODO(benbjohnson) +} + +// Ensure that an error is returned when inserting with data that's too large. +func TestRWTransactionPutDataTooLarge(t *testing.T) { + t.Skip("pending") // TODO(benbjohnson) +} + +// Ensure that an error is returned when deleting from a bucket that doesn't exist. +func TestRWTransactionDeleteBucketNotFound(t *testing.T) { + t.Skip("pending") // TODO(benbjohnson) +} + // Ensure that a bucket can write random keys and values across multiple txns. func TestRWTransactionPutSingle(t *testing.T) { index := 0 diff --git a/transaction.go b/transaction.go index d21693a..1b940d6 100644 --- a/transaction.go +++ b/transaction.go @@ -10,7 +10,6 @@ const ( type txnid uint64 type Transaction struct { - id int db *DB meta *meta buckets *buckets @@ -27,8 +26,13 @@ func (t *Transaction) init(db *DB) { t.buckets.read(t.page(t.meta.buckets)) } +// id returns the transaction id. +func (t *Transaction) id() txnid { + return t.meta.txnid +} + func (t *Transaction) Close() { - // TODO: Close buckets. + t.db.removeTransaction(t) } func (t *Transaction) DB() *DB { @@ -73,12 +77,6 @@ func (t *Transaction) Get(name string, key []byte) []byte { return c.Get(key) } -// stat returns information about a bucket's internal structure. -func (t *Transaction) stat(name string) *Stat { - // TODO - return nil -} - // page returns a reference to the page with a given id. // If page has been written to then a temporary bufferred page is returned. func (t *Transaction) page(id pgid) *page { diff --git a/transaction_test.go b/transaction_test.go index 55e8bde..6041f2f 100644 --- a/transaction_test.go +++ b/transaction_test.go @@ -6,6 +6,11 @@ import ( "github.com/stretchr/testify/assert" ) +// Ensure that the database can retrieve a list of buckets. +func TestTransactionBuckets(t *testing.T) { + t.Skip("pending") // TODO(benbjohnson) +} + // Ensure that a Transaction can retrieve a bucket. func TestTransactionBucketMissing(t *testing.T) { withOpenDB(func(db *DB, path string) { @@ -19,7 +24,7 @@ func TestTransactionBucketMissing(t *testing.T) { } // Ensure that a Transaction retrieving a non-existent key returns nil. -func TestTransactionGetMising(t *testing.T) { +func TestTransactionGetMissing(t *testing.T) { withOpenDB(func(db *DB, path string) { db.CreateBucket("widgets") db.Put("widgets", []byte("foo"), []byte("bar")) |