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  }