github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/helper/boltdd/boltdd_test.go (about)

     1  package boltdd
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"path/filepath"
     7  	"testing"
     8  
     9  	"github.com/hashicorp/go-msgpack/codec"
    10  	"github.com/hashicorp/nomad/ci"
    11  	"github.com/hashicorp/nomad/nomad/mock"
    12  	"github.com/hashicorp/nomad/nomad/structs"
    13  	"github.com/shoenig/test/must"
    14  	"go.etcd.io/bbolt"
    15  )
    16  
    17  const (
    18  	testDB      = "nomad-test.db"
    19  	testDBPerms = 0600
    20  )
    21  
    22  // a simple struct type for testing msg pack en/decoding
    23  type employee struct {
    24  	Name string
    25  	ID   int
    26  }
    27  
    28  func setupBoltDB(t testing.TB) *DB {
    29  	dir := t.TempDir()
    30  
    31  	dbFilename := filepath.Join(dir, testDB)
    32  	db, err := Open(dbFilename, testDBPerms, nil)
    33  	must.NoError(t, err)
    34  
    35  	t.Cleanup(func() {
    36  		must.NoError(t, db.Close())
    37  	})
    38  
    39  	return db
    40  }
    41  
    42  func TestDB_Open(t *testing.T) {
    43  	ci.Parallel(t)
    44  	db := setupBoltDB(t)
    45  	must.Zero(t, db.BoltDB().Stats().TxStats.Write)
    46  }
    47  
    48  func TestDB_Close(t *testing.T) {
    49  	ci.Parallel(t)
    50  
    51  	db := setupBoltDB(t)
    52  
    53  	must.NoError(t, db.Close())
    54  
    55  	must.Eq(t, db.Update(func(tx *Tx) error {
    56  		_, err := tx.CreateBucketIfNotExists([]byte("foo"))
    57  		return err
    58  	}), bbolt.ErrDatabaseNotOpen)
    59  
    60  	must.Eq(t, db.Update(func(tx *Tx) error {
    61  		_, err := tx.CreateBucket([]byte("foo"))
    62  		return err
    63  	}), bbolt.ErrDatabaseNotOpen)
    64  }
    65  
    66  func TestBucket_Create(t *testing.T) {
    67  	ci.Parallel(t)
    68  
    69  	db := setupBoltDB(t)
    70  
    71  	name := []byte("create_test")
    72  
    73  	must.NoError(t, db.Update(func(tx *Tx) error {
    74  		// Trying to get a nonexistent bucket should return nil
    75  		must.Nil(t, tx.Bucket(name))
    76  
    77  		// Creating a nonexistent bucket should work
    78  		b, err := tx.CreateBucket(name)
    79  		must.NoError(t, err)
    80  		must.NotNil(t, b)
    81  
    82  		// Recreating a bucket that exists should fail
    83  		b, err = tx.CreateBucket(name)
    84  		must.Error(t, err)
    85  		must.Nil(t, b)
    86  
    87  		// get or create should work
    88  		b, err = tx.CreateBucketIfNotExists(name)
    89  		must.NoError(t, err)
    90  		must.NotNil(t, b)
    91  		return nil
    92  	}))
    93  
    94  	// Bucket should be visible
    95  	must.NoError(t, db.View(func(tx *Tx) error {
    96  		must.NotNil(t, tx.Bucket(name))
    97  		return nil
    98  	}))
    99  }
   100  
   101  func TestBucket_Iterate(t *testing.T) {
   102  	ci.Parallel(t)
   103  
   104  	db := setupBoltDB(t)
   105  
   106  	bucket := []byte("iterate_test")
   107  
   108  	must.NoError(t, db.Update(func(tx *Tx) error {
   109  		b, err := tx.CreateBucketIfNotExists(bucket)
   110  		must.NoError(t, err)
   111  		must.NotNil(t, b)
   112  
   113  		must.NoError(t, b.Put([]byte("ceo"), employee{Name: "dave", ID: 15}))
   114  		must.NoError(t, b.Put([]byte("founder"), employee{Name: "mitchell", ID: 1}))
   115  		must.NoError(t, b.Put([]byte("cto"), employee{Name: "armon", ID: 2}))
   116  		return nil
   117  	}))
   118  
   119  	t.Run("success", func(t *testing.T) {
   120  		var result []employee
   121  		err := db.View(func(tx *Tx) error {
   122  			b := tx.Bucket(bucket)
   123  			return Iterate(b, nil, func(key []byte, e employee) {
   124  				result = append(result, e)
   125  			})
   126  		})
   127  		must.NoError(t, err)
   128  		must.Eq(t, []employee{
   129  			{"dave", 15}, {"armon", 2}, {"mitchell", 1},
   130  		}, result)
   131  	})
   132  
   133  	t.Run("failure", func(t *testing.T) {
   134  		err := db.View(func(tx *Tx) error {
   135  			b := tx.Bucket(bucket)
   136  			// will fail to encode employee into an int
   137  			return Iterate(b, nil, func(key []byte, i int) {
   138  				must.Unreachable(t)
   139  			})
   140  		})
   141  		must.Error(t, err)
   142  	})
   143  }
   144  
   145  func TestBucket_DeletePrefix(t *testing.T) {
   146  	ci.Parallel(t)
   147  
   148  	db := setupBoltDB(t)
   149  
   150  	bucket := []byte("delete_prefix_test")
   151  
   152  	must.NoError(t, db.Update(func(tx *Tx) error {
   153  		b, err := tx.CreateBucketIfNotExists(bucket)
   154  		must.NoError(t, err)
   155  		must.NotNil(t, b)
   156  
   157  		must.NoError(t, b.Put([]byte("exec_a"), employee{Name: "dave", ID: 15}))
   158  		must.NoError(t, b.Put([]byte("intern_a"), employee{Name: "alice", ID: 7384}))
   159  		must.NoError(t, b.Put([]byte("exec_c"), employee{Name: "armon", ID: 2}))
   160  		must.NoError(t, b.Put([]byte("intern_b"), employee{Name: "bob", ID: 7312}))
   161  		must.NoError(t, b.Put([]byte("exec_b"), employee{Name: "mitchell", ID: 1}))
   162  		return nil
   163  	}))
   164  
   165  	// remove interns
   166  	must.NoError(t, db.Update(func(tx *Tx) error {
   167  		bkt := tx.Bucket(bucket)
   168  		return bkt.DeletePrefix([]byte("intern_"))
   169  	}))
   170  
   171  	// assert 3 exec remain
   172  	var result []employee
   173  	err := db.View(func(tx *Tx) error {
   174  		bkt := tx.Bucket(bucket)
   175  		return Iterate(bkt, nil, func(k []byte, e employee) {
   176  			result = append(result, e)
   177  		})
   178  	})
   179  	must.NoError(t, err)
   180  	must.Eq(t, []employee{
   181  		{"dave", 15}, {"mitchell", 1}, {"armon", 2},
   182  	}, result)
   183  }
   184  
   185  func TestBucket_DedupeWrites(t *testing.T) {
   186  	ci.Parallel(t)
   187  
   188  	db := setupBoltDB(t)
   189  
   190  	bname := []byte("dedupewrites_test")
   191  	k1name := []byte("k1")
   192  	k2name := []byte("k2")
   193  
   194  	// Put 2 keys
   195  	must.NoError(t, db.Update(func(tx *Tx) error {
   196  		b, err := tx.CreateBucket(bname)
   197  		must.NoError(t, err)
   198  		must.NoError(t, b.Put(k1name, k1name))
   199  		must.NoError(t, b.Put(k2name, k2name))
   200  		return nil
   201  	}))
   202  
   203  	// Assert there was at least 1 write
   204  	origWrites := db.BoltDB().Stats().TxStats.Write
   205  	must.Positive(t, origWrites)
   206  
   207  	// Write the same values again and expect no new writes
   208  	must.NoError(t, db.Update(func(tx *Tx) error {
   209  		b := tx.Bucket(bname)
   210  		must.NoError(t, b.Put(k1name, k1name))
   211  		must.NoError(t, b.Put(k2name, k2name))
   212  		return nil
   213  	}))
   214  
   215  	putWrites := db.BoltDB().Stats().TxStats.Write
   216  
   217  	// Unfortunately every committed transaction causes two writes, so this
   218  	// only saves 1 write operation
   219  	must.Eq(t, origWrites+2, putWrites)
   220  
   221  	// Write new values and assert more writes took place
   222  	must.NoError(t, db.Update(func(tx *Tx) error {
   223  		b := tx.Bucket(bname)
   224  		must.NoError(t, b.Put(k1name, []byte("newval1")))
   225  		must.NoError(t, b.Put(k2name, []byte("newval2")))
   226  		return nil
   227  	}))
   228  
   229  	putWrites2 := db.BoltDB().Stats().TxStats.Write
   230  
   231  	// Expect 3 additional writes: 2 for the transaction and one for the
   232  	// dirty page
   233  	must.Eq(t, putWrites+3, putWrites2)
   234  }
   235  
   236  func TestBucket_Delete(t *testing.T) {
   237  	ci.Parallel(t)
   238  
   239  	db := setupBoltDB(t)
   240  
   241  	parentName := []byte("delete_test")
   242  	parentKey := []byte("parent_key")
   243  	childName := []byte("child")
   244  	childKey := []byte("child_key")
   245  	grandchildName1 := []byte("grandchild1")
   246  	grandchildKey1 := []byte("grandchild_key1")
   247  	grandchildName2 := []byte("grandchild2")
   248  	grandchildKey2 := []byte("grandchild_key2")
   249  
   250  	// Create a parent bucket with 1 child and 2 grandchildren
   251  	must.NoError(t, db.Update(func(tx *Tx) error {
   252  		pb, err := tx.CreateBucket(parentName)
   253  		must.NoError(t, err)
   254  
   255  		must.NoError(t, pb.Put(parentKey, parentKey))
   256  
   257  		child, err := pb.CreateBucket(childName)
   258  		must.NoError(t, err)
   259  
   260  		must.NoError(t, child.Put(childKey, childKey))
   261  
   262  		grandchild1, err := child.CreateBucket(grandchildName1)
   263  		must.NoError(t, err)
   264  
   265  		must.NoError(t, grandchild1.Put(grandchildKey1, grandchildKey1))
   266  
   267  		grandchild2, err := child.CreateBucket(grandchildName2)
   268  		must.NoError(t, err)
   269  
   270  		must.NoError(t, grandchild2.Put(grandchildKey2, grandchildKey2))
   271  		return nil
   272  	}))
   273  
   274  	// Verify grandchild keys wrote
   275  	must.NoError(t, db.View(func(tx *Tx) error {
   276  		grandchild1 := tx.Bucket(parentName).Bucket(childName).Bucket(grandchildName1)
   277  		var v1 []byte
   278  		must.NoError(t, grandchild1.Get(grandchildKey1, &v1))
   279  		must.Eq(t, grandchildKey1, v1)
   280  
   281  		grandchild2 := tx.Bucket(parentName).Bucket(childName).Bucket(grandchildName2)
   282  		var v2 []byte
   283  		must.NoError(t, grandchild2.Get(grandchildKey2, &v2))
   284  		must.Eq(t, grandchildKey2, v2)
   285  		return nil
   286  	}))
   287  
   288  	// Delete grandchildKey1 and grandchild2
   289  	must.NoError(t, db.Update(func(tx *Tx) error {
   290  		child := tx.Bucket(parentName).Bucket(childName)
   291  		must.NoError(t, child.DeleteBucket(grandchildName2))
   292  
   293  		grandchild1 := child.Bucket(grandchildName1)
   294  		must.NoError(t, grandchild1.Delete(grandchildKey1))
   295  		return nil
   296  	}))
   297  
   298  	// Ensure grandchild2 alone was deleted
   299  	must.NoError(t, db.View(func(tx *Tx) error {
   300  		grandchild1 := tx.Bucket(parentName).Bucket(childName).Bucket(grandchildName1)
   301  		var v1 []byte
   302  		must.Error(t, grandchild1.Get(grandchildKey1, &v1))
   303  		must.Eq(t, ([]byte)(nil), v1)
   304  
   305  		grandchild2 := tx.Bucket(parentName).Bucket(childName).Bucket(grandchildName2)
   306  		must.Nil(t, grandchild2)
   307  		return nil
   308  	}))
   309  
   310  	// Deleting child bucket should delete grandchild1 as well
   311  	must.NoError(t, db.Update(func(tx *Tx) error {
   312  		parent := tx.Bucket(parentName)
   313  		must.NoError(t, parent.DeleteBucket(childName))
   314  
   315  		// Recreate child bucket and ensure childKey and grandchild are gone
   316  		child, err := parent.CreateBucket(childName)
   317  		must.NoError(t, err)
   318  
   319  		var v []byte
   320  		err = child.Get(childKey, &v)
   321  		must.Error(t, err)
   322  		must.True(t, IsErrNotFound(err))
   323  		must.Eq(t, ([]byte)(nil), v)
   324  
   325  		must.Nil(t, child.Bucket(grandchildName1))
   326  
   327  		// Rewrite childKey1 to make sure it doesn't get de-dupe incorrectly
   328  		must.NoError(t, child.Put(childKey, childKey))
   329  		return nil
   330  	}))
   331  
   332  	// Ensure childKey1 was rewritten and not de-duped incorrectly
   333  	must.NoError(t, db.View(func(tx *Tx) error {
   334  		var v []byte
   335  		must.NoError(t, tx.Bucket(parentName).Bucket(childName).Get(childKey, &v))
   336  		must.Eq(t, childKey, v)
   337  		return nil
   338  	}))
   339  }
   340  
   341  func BenchmarkWriteDeduplication_On(b *testing.B) {
   342  	db := setupBoltDB(b)
   343  
   344  	bucketName := []byte("allocations")
   345  	alloc := mock.Alloc()
   346  	allocID := []byte(alloc.ID)
   347  
   348  	must.NoError(b, db.Update(func(tx *Tx) error {
   349  		allocs, err := tx.CreateBucket(bucketName)
   350  		if err != nil {
   351  			return err
   352  		}
   353  
   354  		return allocs.Put(allocID, alloc)
   355  	}))
   356  
   357  	b.ResetTimer()
   358  	for i := 0; i < b.N; i++ {
   359  		must.NoError(b, db.Update(func(tx *Tx) error {
   360  			return tx.Bucket(bucketName).Put(allocID, alloc)
   361  		}))
   362  	}
   363  }
   364  
   365  func BenchmarkWriteDeduplication_Off(b *testing.B) {
   366  	dir := b.TempDir()
   367  
   368  	dbFilename := filepath.Join(dir, testDB)
   369  	db, openErr := Open(dbFilename, testDBPerms, nil)
   370  	must.NoError(b, openErr)
   371  
   372  	b.Cleanup(func() {
   373  		must.NoError(b, db.Close())
   374  	})
   375  
   376  	bucketName := []byte("allocations")
   377  	alloc := mock.Alloc()
   378  	allocID := []byte(alloc.ID)
   379  
   380  	must.NoError(b, db.Update(func(tx *Tx) error {
   381  		allocs, err := tx.CreateBucket(bucketName)
   382  		if err != nil {
   383  			return err
   384  		}
   385  
   386  		var buf bytes.Buffer
   387  		if err = codec.NewEncoder(&buf, structs.MsgpackHandle).Encode(alloc); err != nil {
   388  			return fmt.Errorf("failed to encode passed object: %v", err)
   389  		}
   390  
   391  		return allocs.Put(allocID, buf)
   392  	}))
   393  
   394  	b.ResetTimer()
   395  	for i := 0; i < b.N; i++ {
   396  		must.NoError(b, db.Update(func(tx *Tx) error {
   397  			var buf bytes.Buffer
   398  			if err := codec.NewEncoder(&buf, structs.MsgpackHandle).Encode(alloc); err != nil {
   399  				return fmt.Errorf("failed to encode passed object: %v", err)
   400  			}
   401  			return tx.Bucket(bucketName).Put(allocID, buf)
   402  		}))
   403  	}
   404  }