// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package boltdd import ( "bytes" "fmt" "path/filepath" "testing" "github.com/hashicorp/go-msgpack/codec" "github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/structs" "github.com/shoenig/test/must" "go.etcd.io/bbolt" ) const ( testDB = "nomad-test.db" testDBPerms = 0600 ) // a simple struct type for testing msg pack en/decoding type employee struct { Name string ID int } func setupBoltDB(t testing.TB) *DB { dir := t.TempDir() dbFilename := filepath.Join(dir, testDB) db, err := Open(dbFilename, testDBPerms, nil) must.NoError(t, err) t.Cleanup(func() { must.NoError(t, db.Close()) }) return db } func TestDB_Open(t *testing.T) { ci.Parallel(t) db := setupBoltDB(t) must.Zero(t, db.BoltDB().Stats().TxStats.Write) } func TestDB_Close(t *testing.T) { ci.Parallel(t) db := setupBoltDB(t) must.NoError(t, db.Close()) must.Eq(t, db.Update(func(tx *Tx) error { _, err := tx.CreateBucketIfNotExists([]byte("foo")) return err }), bbolt.ErrDatabaseNotOpen) must.Eq(t, db.Update(func(tx *Tx) error { _, err := tx.CreateBucket([]byte("foo")) return err }), bbolt.ErrDatabaseNotOpen) } func TestBucket_Create(t *testing.T) { ci.Parallel(t) db := setupBoltDB(t) name := []byte("create_test") must.NoError(t, db.Update(func(tx *Tx) error { // Trying to get a nonexistent bucket should return nil must.Nil(t, tx.Bucket(name)) // Creating a nonexistent bucket should work b, err := tx.CreateBucket(name) must.NoError(t, err) must.NotNil(t, b) // Recreating a bucket that exists should fail b, err = tx.CreateBucket(name) must.Error(t, err) must.Nil(t, b) // get or create should work b, err = tx.CreateBucketIfNotExists(name) must.NoError(t, err) must.NotNil(t, b) return nil })) // Bucket should be visible must.NoError(t, db.View(func(tx *Tx) error { must.NotNil(t, tx.Bucket(name)) return nil })) } func TestBucket_Iterate(t *testing.T) { ci.Parallel(t) db := setupBoltDB(t) bucket := []byte("iterate_test") must.NoError(t, db.Update(func(tx *Tx) error { b, err := tx.CreateBucketIfNotExists(bucket) must.NoError(t, err) must.NotNil(t, b) must.NoError(t, b.Put([]byte("ceo"), employee{Name: "dave", ID: 15})) must.NoError(t, b.Put([]byte("founder"), employee{Name: "mitchell", ID: 1})) must.NoError(t, b.Put([]byte("cto"), employee{Name: "armon", ID: 2})) return nil })) t.Run("success", func(t *testing.T) { var result []employee err := db.View(func(tx *Tx) error { b := tx.Bucket(bucket) return Iterate(b, nil, func(key []byte, e employee) { result = append(result, e) }) }) must.NoError(t, err) must.Eq(t, []employee{ {"dave", 15}, {"armon", 2}, {"mitchell", 1}, }, result) }) t.Run("failure", func(t *testing.T) { err := db.View(func(tx *Tx) error { b := tx.Bucket(bucket) // will fail to encode employee into an int return Iterate(b, nil, func(key []byte, i int) { must.Unreachable(t) }) }) must.Error(t, err) }) } func TestBucket_DeletePrefix(t *testing.T) { ci.Parallel(t) db := setupBoltDB(t) bucket := []byte("delete_prefix_test") must.NoError(t, db.Update(func(tx *Tx) error { b, err := tx.CreateBucketIfNotExists(bucket) must.NoError(t, err) must.NotNil(t, b) must.NoError(t, b.Put([]byte("exec_a"), employee{Name: "dave", ID: 15})) must.NoError(t, b.Put([]byte("intern_a"), employee{Name: "alice", ID: 7384})) must.NoError(t, b.Put([]byte("exec_c"), employee{Name: "armon", ID: 2})) must.NoError(t, b.Put([]byte("intern_b"), employee{Name: "bob", ID: 7312})) must.NoError(t, b.Put([]byte("exec_b"), employee{Name: "mitchell", ID: 1})) return nil })) // remove interns must.NoError(t, db.Update(func(tx *Tx) error { bkt := tx.Bucket(bucket) return bkt.DeletePrefix([]byte("intern_")) })) // assert 3 exec remain var result []employee err := db.View(func(tx *Tx) error { bkt := tx.Bucket(bucket) return Iterate(bkt, nil, func(k []byte, e employee) { result = append(result, e) }) }) must.NoError(t, err) must.Eq(t, []employee{ {"dave", 15}, {"mitchell", 1}, {"armon", 2}, }, result) } func TestBucket_DedupeWrites(t *testing.T) { ci.Parallel(t) db := setupBoltDB(t) bname := []byte("dedupewrites_test") k1name := []byte("k1") k2name := []byte("k2") // Put 2 keys must.NoError(t, db.Update(func(tx *Tx) error { b, err := tx.CreateBucket(bname) must.NoError(t, err) must.NoError(t, b.Put(k1name, k1name)) must.NoError(t, b.Put(k2name, k2name)) return nil })) // Assert there was at least 1 write origWrites := db.BoltDB().Stats().TxStats.Write must.Positive(t, origWrites) // Write the same values again and expect no new writes must.NoError(t, db.Update(func(tx *Tx) error { b := tx.Bucket(bname) must.NoError(t, b.Put(k1name, k1name)) must.NoError(t, b.Put(k2name, k2name)) return nil })) putWrites := db.BoltDB().Stats().TxStats.Write // Unfortunately every committed transaction causes two writes, so this // only saves 1 write operation must.Eq(t, origWrites+2, putWrites) // Write new values and assert more writes took place must.NoError(t, db.Update(func(tx *Tx) error { b := tx.Bucket(bname) must.NoError(t, b.Put(k1name, []byte("newval1"))) must.NoError(t, b.Put(k2name, []byte("newval2"))) return nil })) putWrites2 := db.BoltDB().Stats().TxStats.Write // Expect 3 additional writes: 2 for the transaction and one for the // dirty page must.Eq(t, putWrites+3, putWrites2) } func TestBucket_Delete(t *testing.T) { ci.Parallel(t) db := setupBoltDB(t) parentName := []byte("delete_test") parentKey := []byte("parent_key") childName := []byte("child") childKey := []byte("child_key") grandchildName1 := []byte("grandchild1") grandchildKey1 := []byte("grandchild_key1") grandchildName2 := []byte("grandchild2") grandchildKey2 := []byte("grandchild_key2") // Create a parent bucket with 1 child and 2 grandchildren must.NoError(t, db.Update(func(tx *Tx) error { pb, err := tx.CreateBucket(parentName) must.NoError(t, err) must.NoError(t, pb.Put(parentKey, parentKey)) child, err := pb.CreateBucket(childName) must.NoError(t, err) must.NoError(t, child.Put(childKey, childKey)) grandchild1, err := child.CreateBucket(grandchildName1) must.NoError(t, err) must.NoError(t, grandchild1.Put(grandchildKey1, grandchildKey1)) grandchild2, err := child.CreateBucket(grandchildName2) must.NoError(t, err) must.NoError(t, grandchild2.Put(grandchildKey2, grandchildKey2)) return nil })) // Verify grandchild keys wrote must.NoError(t, db.View(func(tx *Tx) error { grandchild1 := tx.Bucket(parentName).Bucket(childName).Bucket(grandchildName1) var v1 []byte must.NoError(t, grandchild1.Get(grandchildKey1, &v1)) must.Eq(t, grandchildKey1, v1) grandchild2 := tx.Bucket(parentName).Bucket(childName).Bucket(grandchildName2) var v2 []byte must.NoError(t, grandchild2.Get(grandchildKey2, &v2)) must.Eq(t, grandchildKey2, v2) return nil })) // Delete grandchildKey1 and grandchild2 must.NoError(t, db.Update(func(tx *Tx) error { child := tx.Bucket(parentName).Bucket(childName) must.NoError(t, child.DeleteBucket(grandchildName2)) grandchild1 := child.Bucket(grandchildName1) must.NoError(t, grandchild1.Delete(grandchildKey1)) return nil })) // Ensure grandchild2 alone was deleted must.NoError(t, db.View(func(tx *Tx) error { grandchild1 := tx.Bucket(parentName).Bucket(childName).Bucket(grandchildName1) var v1 []byte must.Error(t, grandchild1.Get(grandchildKey1, &v1)) must.Eq(t, ([]byte)(nil), v1) grandchild2 := tx.Bucket(parentName).Bucket(childName).Bucket(grandchildName2) must.Nil(t, grandchild2) return nil })) // Deleting child bucket should delete grandchild1 as well must.NoError(t, db.Update(func(tx *Tx) error { parent := tx.Bucket(parentName) must.NoError(t, parent.DeleteBucket(childName)) // Recreate child bucket and ensure childKey and grandchild are gone child, err := parent.CreateBucket(childName) must.NoError(t, err) var v []byte err = child.Get(childKey, &v) must.Error(t, err) must.True(t, IsErrNotFound(err)) must.Eq(t, ([]byte)(nil), v) must.Nil(t, child.Bucket(grandchildName1)) // Rewrite childKey1 to make sure it doesn't get de-dupe incorrectly must.NoError(t, child.Put(childKey, childKey)) return nil })) // Ensure childKey1 was rewritten and not de-duped incorrectly must.NoError(t, db.View(func(tx *Tx) error { var v []byte must.NoError(t, tx.Bucket(parentName).Bucket(childName).Get(childKey, &v)) must.Eq(t, childKey, v) return nil })) } func BenchmarkWriteDeduplication_On(b *testing.B) { db := setupBoltDB(b) bucketName := []byte("allocations") alloc := mock.Alloc() allocID := []byte(alloc.ID) must.NoError(b, db.Update(func(tx *Tx) error { allocs, err := tx.CreateBucket(bucketName) if err != nil { return err } return allocs.Put(allocID, alloc) })) b.ResetTimer() for i := 0; i < b.N; i++ { must.NoError(b, db.Update(func(tx *Tx) error { return tx.Bucket(bucketName).Put(allocID, alloc) })) } } func BenchmarkWriteDeduplication_Off(b *testing.B) { dir := b.TempDir() dbFilename := filepath.Join(dir, testDB) db, openErr := Open(dbFilename, testDBPerms, nil) must.NoError(b, openErr) b.Cleanup(func() { must.NoError(b, db.Close()) }) bucketName := []byte("allocations") alloc := mock.Alloc() allocID := []byte(alloc.ID) must.NoError(b, db.Update(func(tx *Tx) error { allocs, err := tx.CreateBucket(bucketName) if err != nil { return err } var buf bytes.Buffer if err = codec.NewEncoder(&buf, structs.MsgpackHandle).Encode(alloc); err != nil { return fmt.Errorf("failed to encode passed object: %v", err) } return allocs.Put(allocID, buf) })) b.ResetTimer() for i := 0; i < b.N; i++ { must.NoError(b, db.Update(func(tx *Tx) error { var buf bytes.Buffer if err := codec.NewEncoder(&buf, structs.MsgpackHandle).Encode(alloc); err != nil { return fmt.Errorf("failed to encode passed object: %v", err) } return tx.Bucket(bucketName).Put(allocID, buf) })) } }