github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/client/state/db_test.go (about) 1 package state 2 3 import ( 4 "os" 5 "reflect" 6 "sync" 7 "testing" 8 "time" 9 10 "github.com/hashicorp/nomad/ci" 11 trstate "github.com/hashicorp/nomad/client/allocrunner/taskrunner/state" 12 dmstate "github.com/hashicorp/nomad/client/devicemanager/state" 13 "github.com/hashicorp/nomad/client/dynamicplugins" 14 driverstate "github.com/hashicorp/nomad/client/pluginmanager/drivermanager/state" 15 "github.com/hashicorp/nomad/helper/testlog" 16 "github.com/hashicorp/nomad/nomad/mock" 17 "github.com/hashicorp/nomad/nomad/structs" 18 "github.com/kr/pretty" 19 "github.com/shoenig/test/must" 20 "github.com/stretchr/testify/require" 21 ) 22 23 // assert each implementation satisfies StateDB interface 24 var ( 25 _ StateDB = (*BoltStateDB)(nil) 26 _ StateDB = (*MemDB)(nil) 27 _ StateDB = (*NoopDB)(nil) 28 _ StateDB = (*ErrDB)(nil) 29 ) 30 31 func setupBoltStateDB(t *testing.T) *BoltStateDB { 32 dir := t.TempDir() 33 34 db, err := NewBoltStateDB(testlog.HCLogger(t), dir) 35 if err != nil { 36 if rmErr := os.RemoveAll(dir); rmErr != nil { 37 t.Logf("error removing boltdb dir: %v", rmErr) 38 } 39 t.Fatalf("error creating boltdb: %v", err) 40 } 41 42 t.Cleanup(func() { 43 if closeErr := db.Close(); closeErr != nil { 44 t.Errorf("error closing boltdb: %v", closeErr) 45 } 46 }) 47 48 return db.(*BoltStateDB) 49 } 50 51 func testDB(t *testing.T, f func(*testing.T, StateDB)) { 52 dbs := []StateDB{ 53 setupBoltStateDB(t), 54 NewMemDB(testlog.HCLogger(t)), 55 } 56 57 for _, db := range dbs { 58 t.Run(db.Name(), func(t *testing.T) { 59 f(t, db) 60 }) 61 } 62 } 63 64 // TestStateDB_Allocations asserts the behavior of GetAllAllocations, PutAllocation, and 65 // DeleteAllocationBucket for all operational StateDB implementations. 66 func TestStateDB_Allocations(t *testing.T) { 67 ci.Parallel(t) 68 69 testDB(t, func(t *testing.T, db StateDB) { 70 require := require.New(t) 71 72 // Empty database should return empty non-nil results 73 allocs, errs, err := db.GetAllAllocations() 74 require.NoError(err) 75 require.NotNil(allocs) 76 require.Empty(allocs) 77 require.NotNil(errs) 78 require.Empty(errs) 79 80 // Put allocations 81 alloc1 := mock.Alloc() 82 alloc2 := mock.BatchAlloc() 83 84 require.NoError(db.PutAllocation(alloc1)) 85 require.NoError(db.PutAllocation(alloc2)) 86 87 // Retrieve them 88 allocs, errs, err = db.GetAllAllocations() 89 require.NoError(err) 90 require.NotNil(allocs) 91 require.Len(allocs, 2) 92 for _, a := range allocs { 93 switch a.ID { 94 case alloc1.ID: 95 if !reflect.DeepEqual(a, alloc1) { 96 pretty.Ldiff(t, a, alloc1) 97 t.Fatalf("alloc %q unequal", a.ID) 98 } 99 case alloc2.ID: 100 if !reflect.DeepEqual(a, alloc2) { 101 pretty.Ldiff(t, a, alloc2) 102 t.Fatalf("alloc %q unequal", a.ID) 103 } 104 default: 105 t.Fatalf("unexpected alloc id %q", a.ID) 106 } 107 } 108 require.NotNil(errs) 109 require.Empty(errs) 110 111 // Add another 112 alloc3 := mock.SystemAlloc() 113 require.NoError(db.PutAllocation(alloc3)) 114 allocs, errs, err = db.GetAllAllocations() 115 require.NoError(err) 116 require.NotNil(allocs) 117 require.Len(allocs, 3) 118 require.Contains(allocs, alloc1) 119 require.Contains(allocs, alloc2) 120 require.Contains(allocs, alloc3) 121 require.NotNil(errs) 122 require.Empty(errs) 123 124 // Deleting a nonexistent alloc is a noop 125 require.NoError(db.DeleteAllocationBucket("asdf")) 126 allocs, _, err = db.GetAllAllocations() 127 require.NoError(err) 128 require.NotNil(allocs) 129 require.Len(allocs, 3) 130 131 // Delete alloc1 132 require.NoError(db.DeleteAllocationBucket(alloc1.ID)) 133 allocs, errs, err = db.GetAllAllocations() 134 require.NoError(err) 135 require.NotNil(allocs) 136 require.Len(allocs, 2) 137 require.Contains(allocs, alloc2) 138 require.Contains(allocs, alloc3) 139 require.NotNil(errs) 140 require.Empty(errs) 141 }) 142 } 143 144 // Integer division, rounded up. 145 func ceilDiv(a, b int) int { 146 return (a + b - 1) / b 147 } 148 149 // TestStateDB_Batch asserts the behavior of PutAllocation, PutNetworkStatus and 150 // DeleteAllocationBucket in batch mode, for all operational StateDB implementations. 151 func TestStateDB_Batch(t *testing.T) { 152 ci.Parallel(t) 153 154 testDB(t, func(t *testing.T, db StateDB) { 155 require := require.New(t) 156 157 // For BoltDB, get initial tx_id 158 var getTxID func() int 159 var prevTxID int 160 var batchDelay time.Duration 161 var batchSize int 162 if boltStateDB, ok := db.(*BoltStateDB); ok { 163 boltdb := boltStateDB.DB().BoltDB() 164 getTxID = func() int { 165 tx, err := boltdb.Begin(true) 166 require.NoError(err) 167 defer tx.Rollback() 168 return tx.ID() 169 } 170 prevTxID = getTxID() 171 batchDelay = boltdb.MaxBatchDelay 172 batchSize = boltdb.MaxBatchSize 173 } 174 175 // Write 1000 allocations and network statuses in batch mode 176 startTime := time.Now() 177 const numAllocs = 1000 178 var allocs []*structs.Allocation 179 for i := 0; i < numAllocs; i++ { 180 allocs = append(allocs, mock.Alloc()) 181 } 182 var wg sync.WaitGroup 183 for _, alloc := range allocs { 184 wg.Add(1) 185 go func(alloc *structs.Allocation) { 186 require.NoError(db.PutNetworkStatus(alloc.ID, mock.AllocNetworkStatus(), WithBatchMode())) 187 require.NoError(db.PutAllocation(alloc, WithBatchMode())) 188 wg.Done() 189 }(alloc) 190 } 191 wg.Wait() 192 193 // Check BoltDB actually combined PutAllocation calls into much fewer transactions. 194 // The actual number of transactions depends on how fast the goroutines are spawned, 195 // with every batchDelay (10ms by default) period saved in a separate transaction, 196 // plus each transaction is limited to batchSize writes (1000 by default). 197 // See boltdb MaxBatchDelay and MaxBatchSize parameters for more details. 198 if getTxID != nil { 199 numTransactions := getTxID() - prevTxID 200 writeTime := time.Now().Sub(startTime) 201 expectedNumTransactions := ceilDiv(2*numAllocs, batchSize) + ceilDiv(int(writeTime), int(batchDelay)) 202 require.LessOrEqual(numTransactions, expectedNumTransactions) 203 prevTxID = getTxID() 204 } 205 206 // Retrieve allocs and make sure they are the same (order can differ) 207 readAllocs, errs, err := db.GetAllAllocations() 208 require.NoError(err) 209 require.NotNil(readAllocs) 210 require.Len(readAllocs, len(allocs)) 211 require.NotNil(errs) 212 require.Empty(errs) 213 214 readAllocsById := make(map[string]*structs.Allocation) 215 for _, readAlloc := range readAllocs { 216 readAllocsById[readAlloc.ID] = readAlloc 217 } 218 for _, alloc := range allocs { 219 readAlloc, ok := readAllocsById[alloc.ID] 220 if !ok { 221 t.Fatalf("no alloc with ID=%q", alloc.ID) 222 } 223 if !reflect.DeepEqual(readAlloc, alloc) { 224 pretty.Ldiff(t, readAlloc, alloc) 225 t.Fatalf("alloc %q unequal", alloc.ID) 226 } 227 } 228 229 // Delete all allocs in batch mode 230 startTime = time.Now() 231 for _, alloc := range allocs { 232 wg.Add(1) 233 go func(alloc *structs.Allocation) { 234 require.NoError(db.DeleteAllocationBucket(alloc.ID, WithBatchMode())) 235 wg.Done() 236 }(alloc) 237 } 238 wg.Wait() 239 240 // Check BoltDB combined DeleteAllocationBucket calls into much fewer transactions. 241 if getTxID != nil { 242 numTransactions := getTxID() - prevTxID 243 writeTime := time.Now().Sub(startTime) 244 expectedNumTransactions := ceilDiv(numAllocs, batchSize) + ceilDiv(int(writeTime), int(batchDelay)) 245 require.LessOrEqual(numTransactions, expectedNumTransactions) 246 prevTxID = getTxID() 247 } 248 249 // Check all allocs were deleted. 250 readAllocs, errs, err = db.GetAllAllocations() 251 require.NoError(err) 252 require.Empty(readAllocs) 253 require.Empty(errs) 254 }) 255 } 256 257 // TestStateDB_TaskState asserts the behavior of task state related StateDB 258 // methods. 259 func TestStateDB_TaskState(t *testing.T) { 260 ci.Parallel(t) 261 262 testDB(t, func(t *testing.T, db StateDB) { 263 require := require.New(t) 264 265 // Getting nonexistent state should return nils 266 ls, ts, err := db.GetTaskRunnerState("allocid", "taskname") 267 require.NoError(err) 268 require.Nil(ls) 269 require.Nil(ts) 270 271 // Putting TaskState without first putting the allocation should work 272 state := structs.NewTaskState() 273 state.Failed = true // set a non-default value 274 require.NoError(db.PutTaskState("allocid", "taskname", state)) 275 276 // Getting should return the available state 277 ls, ts, err = db.GetTaskRunnerState("allocid", "taskname") 278 require.NoError(err) 279 require.Nil(ls) 280 require.Equal(state, ts) 281 282 // Deleting a nonexistent task should not error 283 require.NoError(db.DeleteTaskBucket("adsf", "asdf")) 284 require.NoError(db.DeleteTaskBucket("asllocid", "asdf")) 285 286 // Data should be untouched 287 ls, ts, err = db.GetTaskRunnerState("allocid", "taskname") 288 require.NoError(err) 289 require.Nil(ls) 290 require.Equal(state, ts) 291 292 // Deleting the task should remove the state 293 require.NoError(db.DeleteTaskBucket("allocid", "taskname")) 294 ls, ts, err = db.GetTaskRunnerState("allocid", "taskname") 295 require.NoError(err) 296 require.Nil(ls) 297 require.Nil(ts) 298 299 // Putting LocalState should work just like TaskState 300 origLocalState := trstate.NewLocalState() 301 require.NoError(db.PutTaskRunnerLocalState("allocid", "taskname", origLocalState)) 302 ls, ts, err = db.GetTaskRunnerState("allocid", "taskname") 303 require.NoError(err) 304 require.Equal(origLocalState, ls) 305 require.Nil(ts) 306 }) 307 } 308 309 // TestStateDB_DeviceManager asserts the behavior of device manager state related StateDB 310 // methods. 311 func TestStateDB_DeviceManager(t *testing.T) { 312 ci.Parallel(t) 313 314 testDB(t, func(t *testing.T, db StateDB) { 315 require := require.New(t) 316 317 // Getting nonexistent state should return nils 318 ps, err := db.GetDevicePluginState() 319 require.NoError(err) 320 require.Nil(ps) 321 322 // Putting PluginState should work 323 state := &dmstate.PluginState{} 324 require.NoError(db.PutDevicePluginState(state)) 325 326 // Getting should return the available state 327 ps, err = db.GetDevicePluginState() 328 require.NoError(err) 329 require.NotNil(ps) 330 require.Equal(state, ps) 331 }) 332 } 333 334 // TestStateDB_DriverManager asserts the behavior of device manager state related StateDB 335 // methods. 336 func TestStateDB_DriverManager(t *testing.T) { 337 ci.Parallel(t) 338 339 testDB(t, func(t *testing.T, db StateDB) { 340 require := require.New(t) 341 342 // Getting nonexistent state should return nils 343 ps, err := db.GetDriverPluginState() 344 require.NoError(err) 345 require.Nil(ps) 346 347 // Putting PluginState should work 348 state := &driverstate.PluginState{} 349 require.NoError(db.PutDriverPluginState(state)) 350 351 // Getting should return the available state 352 ps, err = db.GetDriverPluginState() 353 require.NoError(err) 354 require.NotNil(ps) 355 require.Equal(state, ps) 356 }) 357 } 358 359 // TestStateDB_DynamicRegistry asserts the behavior of dynamic registry state related StateDB 360 // methods. 361 func TestStateDB_DynamicRegistry(t *testing.T) { 362 ci.Parallel(t) 363 364 testDB(t, func(t *testing.T, db StateDB) { 365 require := require.New(t) 366 367 // Getting nonexistent state should return nils 368 ps, err := db.GetDynamicPluginRegistryState() 369 require.NoError(err) 370 require.Nil(ps) 371 372 // Putting PluginState should work 373 state := &dynamicplugins.RegistryState{} 374 require.NoError(db.PutDynamicPluginRegistryState(state)) 375 376 // Getting should return the available state 377 ps, err = db.GetDynamicPluginRegistryState() 378 require.NoError(err) 379 require.NotNil(ps) 380 require.Equal(state, ps) 381 }) 382 } 383 384 func TestStateDB_CheckResult_keyForCheck(t *testing.T) { 385 ci.Parallel(t) 386 387 allocID := "alloc1" 388 checkID := structs.CheckID("id1") 389 result := keyForCheck(allocID, checkID) 390 exp := allocID + "_" + string(checkID) 391 must.Eq(t, exp, string(result)) 392 } 393 394 func TestStateDB_CheckResult(t *testing.T) { 395 ci.Parallel(t) 396 397 qr := func(id string) *structs.CheckQueryResult { 398 return &structs.CheckQueryResult{ 399 ID: structs.CheckID(id), 400 Mode: "healthiness", 401 Status: "passing", 402 Output: "nomad: tcp ok", 403 Timestamp: 1, 404 Group: "group", 405 Task: "task", 406 Service: "service", 407 Check: "check", 408 } 409 } 410 411 testDB(t, func(t *testing.T, db StateDB) { 412 t.Run("put and get", func(t *testing.T) { 413 err := db.PutCheckResult("alloc1", qr("abc123")) 414 must.NoError(t, err) 415 results, err := db.GetCheckResults() 416 must.NoError(t, err) 417 must.MapContainsKeys(t, results, []string{"alloc1"}) 418 must.MapContainsKeys(t, results["alloc1"], []structs.CheckID{"abc123"}) 419 }) 420 }) 421 422 testDB(t, func(t *testing.T, db StateDB) { 423 t.Run("delete", func(t *testing.T) { 424 must.NoError(t, db.PutCheckResult("alloc1", qr("id1"))) 425 must.NoError(t, db.PutCheckResult("alloc1", qr("id2"))) 426 must.NoError(t, db.PutCheckResult("alloc1", qr("id3"))) 427 must.NoError(t, db.PutCheckResult("alloc1", qr("id4"))) 428 must.NoError(t, db.PutCheckResult("alloc2", qr("id5"))) 429 err := db.DeleteCheckResults("alloc1", []structs.CheckID{"id2", "id3"}) 430 must.NoError(t, err) 431 results, err := db.GetCheckResults() 432 must.NoError(t, err) 433 must.MapContainsKeys(t, results, []string{"alloc1", "alloc2"}) 434 must.MapContainsKeys(t, results["alloc1"], []structs.CheckID{"id1", "id4"}) 435 must.MapContainsKeys(t, results["alloc2"], []structs.CheckID{"id5"}) 436 }) 437 }) 438 439 testDB(t, func(t *testing.T, db StateDB) { 440 t.Run("purge", func(t *testing.T) { 441 must.NoError(t, db.PutCheckResult("alloc1", qr("id1"))) 442 must.NoError(t, db.PutCheckResult("alloc1", qr("id2"))) 443 must.NoError(t, db.PutCheckResult("alloc1", qr("id3"))) 444 must.NoError(t, db.PutCheckResult("alloc1", qr("id4"))) 445 must.NoError(t, db.PutCheckResult("alloc2", qr("id5"))) 446 err := db.PurgeCheckResults("alloc1") 447 must.NoError(t, err) 448 results, err := db.GetCheckResults() 449 must.NoError(t, err) 450 must.MapContainsKeys(t, results, []string{"alloc2"}) 451 must.MapContainsKeys(t, results["alloc2"], []structs.CheckID{"id5"}) 452 }) 453 }) 454 455 } 456 457 // TestStateDB_Upgrade asserts calling Upgrade on new databases always 458 // succeeds. 459 func TestStateDB_Upgrade(t *testing.T) { 460 ci.Parallel(t) 461 462 testDB(t, func(t *testing.T, db StateDB) { 463 require.NoError(t, db.Upgrade()) 464 }) 465 }