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 }