github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/search/doc_db.go (about)

     1  // Copyright 2020 Keybase Inc. All rights reserved.
     2  // Use of this source code is governed by a BSD
     3  // license that can be found in the LICENSE file.
     4  
     5  package search
     6  
     7  import (
     8  	"context"
     9  	"io"
    10  	"sync"
    11  
    12  	"github.com/keybase/client/go/kbfs/ldbutils"
    13  	"github.com/keybase/client/go/kbfs/libfs"
    14  	"github.com/keybase/client/go/kbfs/libkbfs"
    15  	"github.com/keybase/client/go/logger"
    16  	"github.com/pkg/errors"
    17  	"github.com/syndtr/goleveldb/leveldb/storage"
    18  	billy "gopkg.in/src-d/go-billy.v4"
    19  )
    20  
    21  // DocDb is a database that holds metadata about indexed documents.
    22  type DocDb struct {
    23  	config libkbfs.Config
    24  	log    logger.Logger
    25  
    26  	// Protect the DB from being shutdown while they're being
    27  	// accessed, and mutable data.
    28  	lock sync.RWMutex
    29  	db   *ldbutils.LevelDb // docID -> doc metadata.
    30  
    31  	shutdownCh chan struct{}
    32  
    33  	closer func()
    34  }
    35  
    36  // newDocDbFromStorage creates a new *DocDb with the passed-in
    37  // storage.Storage interface as the storage layer for the db.
    38  func newDocDbFromStorage(config libkbfs.Config, s storage.Storage) (
    39  	db *DocDb, err error) {
    40  	log := config.MakeLogger("DD")
    41  	closers := make([]io.Closer, 0, 1)
    42  	closer := func() {
    43  		for _, c := range closers {
    44  			closeErr := c.Close()
    45  			if closeErr != nil {
    46  				log.Warning("Error closing leveldb or storage: %+v", closeErr)
    47  			}
    48  		}
    49  	}
    50  	defer func() {
    51  		if err != nil {
    52  			err = errors.WithStack(err)
    53  			closer()
    54  		}
    55  	}()
    56  
    57  	dbOptions := ldbutils.LeveldbOptions(config.Mode())
    58  	docDb, err := ldbutils.OpenLevelDbWithOptions(s, dbOptions)
    59  	if err != nil {
    60  		return nil, err
    61  	}
    62  	closers = append(closers, docDb)
    63  
    64  	return &DocDb{
    65  		config:     config,
    66  		db:         docDb,
    67  		shutdownCh: make(chan struct{}),
    68  		closer:     closer,
    69  	}, nil
    70  }
    71  
    72  // newDocDb creates a new *DocDb with a
    73  // specified billy filesystem as the storage layer.
    74  func newDocDb(config libkbfs.Config, fs billy.Filesystem) (
    75  	db *DocDb, err error) {
    76  	log := config.MakeLogger("DD")
    77  	defer func() {
    78  		if err != nil {
    79  			log.Error("Error initializing doc db: %+v", err)
    80  		}
    81  	}()
    82  
    83  	s, err := libfs.OpenLevelDBStorage(fs, false)
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  
    88  	defer func() {
    89  		if err != nil {
    90  			s.Close()
    91  		}
    92  	}()
    93  	return newDocDbFromStorage(config, s)
    94  }
    95  
    96  type docMD struct {
    97  	// Exported only for serialization.
    98  	ParentDocID string `codec:"p"`
    99  	Name        string `codec:"n"`
   100  }
   101  
   102  // getMetadataLocked retrieves the metadata for a doc in the db, or
   103  // returns leveldb.ErrNotFound and a zero-valued metadata otherwise.
   104  func (db *DocDb) getMetadataLocked(docID string) (md docMD, err error) {
   105  	mdBytes, err := db.db.Get([]byte(docID), nil)
   106  	if err != nil {
   107  		return docMD{}, err
   108  	}
   109  	err = db.config.Codec().Decode(mdBytes, &md)
   110  	if err != nil {
   111  		return docMD{}, err
   112  	}
   113  	return md, nil
   114  }
   115  
   116  // checkAndLockDb checks whether the db is started.
   117  func (db *DocDb) checkDbLocked(
   118  	ctx context.Context, method string) error {
   119  	// First see if the context has expired since we began.
   120  	select {
   121  	case <-ctx.Done():
   122  		return ctx.Err()
   123  	default:
   124  	}
   125  
   126  	// shutdownCh has to be checked under lock, otherwise we can race.
   127  	select {
   128  	case <-db.shutdownCh:
   129  		return errors.WithStack(DbClosedError{method})
   130  	default:
   131  	}
   132  	if db.db == nil {
   133  		return errors.WithStack(DbClosedError{method})
   134  	}
   135  	return nil
   136  }
   137  
   138  // Get returns the info for the given doc.
   139  func (db *DocDb) Get(ctx context.Context, docID string) (
   140  	parentDocID, name string, err error) {
   141  	db.lock.RLock()
   142  	defer db.lock.RUnlock()
   143  	err = db.checkDbLocked(ctx, "DD(Get)")
   144  	if err != nil {
   145  		return "", "", err
   146  	}
   147  
   148  	md, err := db.getMetadataLocked(docID)
   149  	if err != nil {
   150  		return "", "", err
   151  	}
   152  	return md.ParentDocID, md.Name, nil
   153  }
   154  
   155  // Put saves the revisions for the given TLF.
   156  func (db *DocDb) Put(
   157  	ctx context.Context, docID, parentDocID, name string) error {
   158  	db.lock.Lock()
   159  	defer db.lock.Unlock()
   160  	err := db.checkDbLocked(ctx, "DD(Put)")
   161  	if err != nil {
   162  		return err
   163  	}
   164  
   165  	md := docMD{
   166  		ParentDocID: parentDocID,
   167  		Name:        name,
   168  	}
   169  	encodedMetadata, err := db.config.Codec().Encode(&md)
   170  	if err != nil {
   171  		return err
   172  	}
   173  
   174  	return db.db.Put([]byte(docID), encodedMetadata, nil)
   175  }
   176  
   177  // Delete removes the metadata for the TLF from the DB.
   178  func (db *DocDb) Delete(
   179  	ctx context.Context, docID string) error {
   180  	db.lock.Lock()
   181  	defer db.lock.Unlock()
   182  	err := db.checkDbLocked(ctx, "DD(Delete)")
   183  	if err != nil {
   184  		return err
   185  	}
   186  
   187  	return db.db.Delete([]byte(docID), nil)
   188  }
   189  
   190  // Shutdown closes this db.
   191  func (db *DocDb) Shutdown(ctx context.Context) {
   192  	db.lock.Lock()
   193  	defer db.lock.Unlock()
   194  	// shutdownCh has to be checked under lock, otherwise we can race.
   195  	select {
   196  	case <-db.shutdownCh:
   197  		db.log.CWarningf(ctx, "Shutdown called more than once")
   198  		return
   199  	default:
   200  	}
   201  	close(db.shutdownCh)
   202  	if db.db == nil {
   203  		return
   204  	}
   205  	db.closer()
   206  	db.db = nil
   207  }