github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/libkb/leveldb_test.go (about) 1 // Copyright 2016 Keybase, Inc. All rights reserved. Use of 2 // this source code is governed by the included BSD license. 3 4 package libkb 5 6 import ( 7 "bytes" 8 "fmt" 9 10 "os" 11 "path/filepath" 12 "sync" 13 "testing" 14 "time" 15 16 "github.com/stretchr/testify/require" 17 ) 18 19 type teardowner struct { 20 sync.Mutex 21 22 actions []func() 23 torndown bool 24 } 25 26 func (td *teardowner) register(teardownAction func()) { 27 td.Lock() 28 defer td.Unlock() 29 if td.torndown { 30 panic("already torndown") 31 } 32 td.actions = append(td.actions, teardownAction) 33 } 34 35 func (td *teardowner) teardown() { 36 td.Lock() 37 defer td.Unlock() 38 if td.torndown { 39 panic("already torndown") 40 } 41 for _, a := range td.actions { 42 a() 43 } 44 } 45 46 func createTempLevelDbForTest(tc *TestContext, td *teardowner) (*LevelDb, error) { 47 dir, err := os.MkdirTemp("", "level-db-test-") 48 if err != nil { 49 return nil, err 50 } 51 52 db := NewLevelDb(tc.G, func() string { 53 return filepath.Join(dir, "test.leveldb") 54 }) 55 56 td.register(func() { 57 db.Close() 58 os.RemoveAll(dir) 59 }) 60 61 return db, nil 62 } 63 64 func doSomeIO() error { 65 dir, err := os.MkdirTemp("", "level-db-test-") 66 if err != nil { 67 return err 68 } 69 return os.WriteFile(filepath.Join(dir, "some-io"), []byte("O_O"), 0666) 70 } 71 72 func testLevelDbPut(db *LevelDb) (key DbKey, err error) { 73 key = DbKey{Key: "test-key", Typ: 0} 74 v := []byte{1, 2, 3, 4} 75 if err := db.Put(key, nil, v); err != nil { 76 return DbKey{}, err 77 } 78 if val, found, err := db.Get(key); err != nil { 79 return DbKey{}, err 80 } else if !found { 81 return DbKey{}, fmt.Errorf("stored object was not found by Get") 82 } else if !bytes.Equal(val, v) { 83 return DbKey{}, fmt.Errorf("stored object has incorrect data. expect %v, got %v", v, val) 84 } 85 86 return key, nil 87 } 88 89 func TestLevelDb(t *testing.T) { 90 var td teardowner 91 92 tests := []struct { 93 name string 94 testBody func(t *testing.T) 95 }{ 96 { 97 name: "simple", testBody: func(t *testing.T) { 98 tc := SetupTest(t, "LevelDb-simple", 0) 99 defer tc.Cleanup() 100 db, err := createTempLevelDbForTest(&tc, &td) 101 require.NoError(t, err) 102 103 key, err := testLevelDbPut(db) 104 require.NoError(t, err) 105 106 err = db.Delete(key) 107 require.NoError(t, err) 108 109 _, found, err := db.Get(key) 110 require.NoError(t, err) 111 require.False(t, found) 112 }, 113 }, 114 { 115 name: "cleaner", testBody: func(t *testing.T) { 116 tc := SetupTest(t, "LevelDb-cleaner", 0) 117 defer tc.Cleanup() 118 db, err := createTempLevelDbForTest(&tc, &td) 119 require.NoError(t, err) 120 121 key := DbKey{Key: "test-key", Typ: 0} 122 v, err := RandBytes(1024 * 1024) 123 require.NoError(t, err) 124 err = db.Put(key, nil, v) 125 require.NoError(t, err) 126 127 // this key will not be deleted since it is in the permanent 128 // table. 129 require.True(t, IsPermDbKey(DBDiskLRUEntries)) 130 permKey := DbKey{Key: "test-key", Typ: DBDiskLRUEntries} 131 err = db.Put(permKey, nil, v) 132 require.NoError(t, err) 133 134 // cleaner will not clean the key since it was recently used 135 err = db.cleaner.clean(true /* force */) 136 require.NoError(t, err) 137 _, found, err := db.Get(key) 138 require.NoError(t, err) 139 require.True(t, found) 140 _, found, err = db.Get(permKey) 141 require.NoError(t, err) 142 require.True(t, found) 143 144 db.cleaner.clearCache() 145 err = db.cleaner.clean(true /* force */) 146 require.NoError(t, err) 147 _, found, err = db.Get(key) 148 require.NoError(t, err) 149 require.False(t, found) 150 _, found, err = db.Get(permKey) 151 require.NoError(t, err) 152 require.True(t, found) 153 }, 154 }, 155 { 156 name: "concurrent", testBody: func(t *testing.T) { 157 tc := SetupTest(t, "LevelDb-concurrent", 0) 158 defer tc.Cleanup() 159 db, err := createTempLevelDbForTest(&tc, &td) 160 require.NoError(t, err) 161 162 var wg sync.WaitGroup 163 wg.Add(2) 164 // synchronize between two doWhileOpenAndNukeIfCorrupted calls to know 165 // for sure they can happen concurrently. 166 ch := make(chan struct{}) 167 go func() { 168 _ = db.doWhileOpenAndNukeIfCorrupted(func() error { 169 defer wg.Done() 170 select { 171 case <-time.After(8 * time.Second): 172 t.Error("doWhileOpenAndNukeIfCorrupted is not concurrent") 173 case <-ch: 174 } 175 return nil 176 }) 177 }() 178 go func() { 179 _ = db.doWhileOpenAndNukeIfCorrupted(func() error { 180 defer wg.Done() 181 select { 182 case <-time.After(8 * time.Second): 183 t.Error("doWhileOpenAndNukeIfCorrupted does not support concurrent ops") 184 case ch <- struct{}{}: 185 } 186 return nil 187 }) 188 }() 189 wg.Wait() 190 }, 191 }, 192 { 193 name: "nuke", testBody: func(t *testing.T) { 194 tc := SetupTest(t, "LevelDb-nuke", 0) 195 defer tc.Cleanup() 196 db, err := createTempLevelDbForTest(&tc, &td) 197 require.NoError(t, err) 198 199 key, err := testLevelDbPut(db) 200 require.NoError(t, err) 201 202 _, err = db.Nuke() 203 require.NoError(t, err) 204 205 _, found, err := db.Get(key) 206 require.NoError(t, err) 207 require.False(t, found) 208 209 // make sure db still works after nuking 210 _, err = testLevelDbPut(db) 211 require.NoError(t, err) 212 }, 213 }, 214 { 215 name: "use-after-close", testBody: func(t *testing.T) { 216 tc := SetupTest(t, "LevelDb-use-after-close", 0) 217 defer tc.Cleanup() 218 db, err := createTempLevelDbForTest(&tc, &td) 219 require.NoError(t, err) 220 221 // not closed yet; should be good 222 _, err = testLevelDbPut(db) 223 require.NoError(t, err) 224 225 err = db.Close() 226 require.NoError(t, err) 227 228 _, err = testLevelDbPut(db) 229 require.Error(t, err) 230 231 err = db.ForceOpen() 232 require.NoError(t, err) 233 }, 234 }, 235 { 236 name: "transactions", testBody: func(t *testing.T) { 237 tc := SetupTest(t, "LevelDb-transactions", 0) 238 defer tc.Cleanup() 239 db, err := createTempLevelDbForTest(&tc, &td) 240 require.NoError(t, err) 241 242 // have something in the DB 243 key, err := testLevelDbPut(db) 244 require.NoError(t, err) 245 246 var wg sync.WaitGroup 247 wg.Add(2) 248 249 // channels for communicating from first routine to 2nd. 250 chOpen := make(chan struct{}) 251 chCommitted := make(chan struct{}, 1) 252 253 go func() { 254 defer wg.Done() 255 256 tr, err := db.OpenTransaction() 257 if err != nil { 258 t.Error(err) 259 } 260 261 select { 262 case <-time.After(8 * time.Second): 263 t.Errorf("timeout") 264 case chOpen <- struct{}{}: 265 } 266 267 if err = tr.Put(key, nil, []byte{41}); err != nil { 268 t.Error(err) 269 } 270 271 // We do some IO here to give Go's runtime a chance to schedule 272 // different routines and channel operations, to *hopefully* make 273 // sure: 274 // 1) The channel operation is done; 275 // 2) If there exists, any broken OpenTransaction() implementation 276 // that does not block until this transaction finishes, the broken 277 // OpenTransaction() would have has returned 278 if err = doSomeIO(); err != nil { 279 t.Error(err) 280 } 281 282 // we send to a buffered channel right before Commit() to make sure 283 // the channel is ready to read right after the commit 284 chCommitted <- struct{}{} 285 286 if err = tr.Commit(); err != nil { 287 t.Error(err) 288 } 289 290 }() 291 292 go func() { 293 defer wg.Done() 294 295 // wait until the other transaction has opened 296 select { 297 case <-time.After(8 * time.Second): 298 t.Error("timeout") 299 case <-chOpen: 300 } 301 302 tr, err := db.OpenTransaction() 303 select { 304 case <-chCommitted: 305 // fine 306 default: 307 t.Error("second transaction did not block until first one finished") 308 } 309 if err != nil { 310 t.Error(err) 311 } 312 313 d, found, err := tr.Get(key) 314 if err != nil { 315 t.Error(err) 316 } 317 if !found { 318 t.Errorf("key %v is not found", found) 319 } 320 321 if err = tr.Put(key, nil, []byte{d[0] + 1}); err != nil { 322 t.Error(err) 323 } 324 if err = tr.Commit(); err != nil { 325 t.Error(err) 326 } 327 }() 328 329 wg.Wait() 330 331 data, found, err := db.Get(key) 332 require.NoError(t, err) 333 require.True(t, found) 334 require.Len(t, data, 1) 335 require.EqualValues(t, 42, data[0]) 336 }, 337 }, 338 { 339 name: "transaction-discard", testBody: func(t *testing.T) { 340 tc := SetupTest(t, "LevelDb-transaction-discard", 0) 341 defer tc.Cleanup() 342 db, err := createTempLevelDbForTest(&tc, &td) 343 require.NoError(t, err) 344 345 // have something in the DB 346 key, err := testLevelDbPut(db) 347 require.NoError(t, err) 348 349 tr, err := db.OpenTransaction() 350 require.NoError(t, err) 351 err = tr.Delete(key) 352 require.NoError(t, err) 353 tr.Discard() 354 355 _, found, err := db.Get(key) 356 require.NoError(t, err) 357 require.True(t, found) 358 }, 359 }, 360 } 361 362 for _, test := range tests { 363 if !t.Run(test.name, test.testBody) { 364 t.Fail() // mark as failed but continue with next test 365 } 366 } 367 368 td.teardown() 369 }