github.com/weaviate/weaviate@v1.24.6/adapters/repos/db/lsmkv/store.go (about)

     1  //                           _       _
     2  // __      _____  __ ___   ___  __ _| |_ ___
     3  // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \
     4  //  \ V  V /  __/ (_| |\ V /| | (_| | ||  __/
     5  //   \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___|
     6  //
     7  //  Copyright © 2016 - 2024 Weaviate B.V. All rights reserved.
     8  //
     9  //  CONTACT: hello@weaviate.io
    10  //
    11  
    12  package lsmkv
    13  
    14  import (
    15  	"context"
    16  	"fmt"
    17  	"os"
    18  	"path"
    19  	"path/filepath"
    20  	"strings"
    21  	"sync"
    22  
    23  	enterrors "github.com/weaviate/weaviate/entities/errors"
    24  
    25  	"github.com/pkg/errors"
    26  	"github.com/sirupsen/logrus"
    27  	"github.com/weaviate/weaviate/entities/cyclemanager"
    28  	"github.com/weaviate/weaviate/entities/errorcompounder"
    29  	"github.com/weaviate/weaviate/entities/storagestate"
    30  )
    31  
    32  // Store groups multiple buckets together, it "owns" one folder on the file
    33  // system
    34  type Store struct {
    35  	dir           string
    36  	rootDir       string
    37  	bucketsByName map[string]*Bucket
    38  	logger        logrus.FieldLogger
    39  	metrics       *Metrics
    40  
    41  	cycleCallbacks *storeCycleCallbacks
    42  
    43  	// Prevent concurrent manipulations to the bucketsByNameMap, most notably
    44  	// when initializing buckets in parallel
    45  	bucketAccessLock sync.RWMutex
    46  	bucketByNameLock map[string]*sync.Mutex
    47  }
    48  
    49  // New initializes a new [Store] based on the root dir. If state is present on
    50  // disk, it is loaded, if the folder is empty a new store is initialized in
    51  // there.
    52  func New(dir, rootDir string, logger logrus.FieldLogger, metrics *Metrics,
    53  	shardCompactionCallbacks, shardFlushCallbacks cyclemanager.CycleCallbackGroup,
    54  ) (*Store, error) {
    55  	s := &Store{
    56  		dir:              dir,
    57  		rootDir:          rootDir,
    58  		bucketsByName:    map[string]*Bucket{},
    59  		bucketByNameLock: make(map[string]*sync.Mutex),
    60  		logger:           logger,
    61  		metrics:          metrics,
    62  	}
    63  	s.initCycleCallbacks(shardCompactionCallbacks, shardFlushCallbacks)
    64  
    65  	return s, s.init()
    66  }
    67  
    68  func (s *Store) Bucket(name string) *Bucket {
    69  	s.bucketAccessLock.RLock()
    70  	defer s.bucketAccessLock.RUnlock()
    71  
    72  	return s.bucketsByName[name]
    73  }
    74  
    75  func (s *Store) UpdateBucketsStatus(targetStatus storagestate.Status) {
    76  	// UpdateBucketsStatus is a write operation on the bucket itself, but from
    77  	// the perspective of our bucket access map this is a read-only operation,
    78  	// hence an RLock()
    79  	s.bucketAccessLock.RLock()
    80  	defer s.bucketAccessLock.RUnlock()
    81  
    82  	for _, b := range s.bucketsByName {
    83  		if b == nil {
    84  			continue
    85  		}
    86  
    87  		b.UpdateStatus(targetStatus)
    88  	}
    89  
    90  	if targetStatus == storagestate.StatusReadOnly {
    91  		s.logger.WithField("action", "lsm_compaction").
    92  			WithField("path", s.dir).
    93  			Warn("compaction halted due to shard READONLY status")
    94  	}
    95  }
    96  
    97  func (s *Store) init() error {
    98  	if err := os.MkdirAll(s.dir, 0o700); err != nil {
    99  		return err
   100  	}
   101  	return nil
   102  }
   103  
   104  func (s *Store) bucketDir(bucketName string) string {
   105  	return path.Join(s.dir, bucketName)
   106  }
   107  
   108  // CreateOrLoadBucket registers a bucket with the given name. If state on disk
   109  // exists for this bucket it is loaded, otherwise created. Pass [BucketOptions]
   110  // to configure the strategy of a bucket. The strategy defaults to "replace".
   111  // For example, to load or create a map-type bucket, do:
   112  //
   113  //	ctx := context.Background()
   114  //	err := store.CreateOrLoadBucket(ctx, "my_bucket_name", WithStrategy(StrategyReplace))
   115  //	if err != nil { /* handle error */ }
   116  //
   117  //	// you can now access the bucket using store.Bucket()
   118  //	b := store.Bucket("my_bucket_name")
   119  func (s *Store) CreateOrLoadBucket(ctx context.Context, bucketName string,
   120  	opts ...BucketOption,
   121  ) error {
   122  	var bucketLock *sync.Mutex
   123  
   124  	s.bucketAccessLock.Lock()
   125  
   126  	_, ok := s.bucketByNameLock[bucketName]
   127  	if !ok {
   128  		s.bucketByNameLock[bucketName] = &sync.Mutex{}
   129  	}
   130  	bucketLock = s.bucketByNameLock[bucketName]
   131  
   132  	s.bucketAccessLock.Unlock()
   133  
   134  	bucketLock.Lock()
   135  	defer bucketLock.Unlock()
   136  
   137  	if b := s.Bucket(bucketName); b != nil {
   138  		return nil
   139  	}
   140  
   141  	// bucket can be concurrently loaded with another buckets but
   142  	// the same bucket will be loaded only once
   143  	b, err := NewBucket(ctx, s.bucketDir(bucketName), s.rootDir, s.logger, s.metrics,
   144  		s.cycleCallbacks.compactionCallbacks, s.cycleCallbacks.flushCallbacks, opts...)
   145  	if err != nil {
   146  		s.bucketAccessLock.Lock()
   147  		delete(s.bucketByNameLock, bucketName)
   148  		s.bucketAccessLock.Unlock()
   149  		return err
   150  	}
   151  
   152  	s.setBucket(bucketName, b)
   153  
   154  	return nil
   155  }
   156  
   157  func (s *Store) setBucket(name string, b *Bucket) {
   158  	s.bucketAccessLock.Lock()
   159  	defer s.bucketAccessLock.Unlock()
   160  
   161  	s.bucketsByName[name] = b
   162  }
   163  
   164  func (s *Store) Shutdown(ctx context.Context) error {
   165  	s.bucketAccessLock.RLock()
   166  	defer s.bucketAccessLock.RUnlock()
   167  
   168  	for name, bucket := range s.bucketsByName {
   169  		if err := bucket.Shutdown(ctx); err != nil {
   170  			return errors.Wrapf(err, "shutdown bucket %q", name)
   171  		}
   172  	}
   173  
   174  	return nil
   175  }
   176  
   177  func (s *Store) WriteWALs() error {
   178  	s.bucketAccessLock.RLock()
   179  	defer s.bucketAccessLock.RUnlock()
   180  
   181  	for name, bucket := range s.bucketsByName {
   182  		if err := bucket.WriteWAL(); err != nil {
   183  			return errors.Wrapf(err, "bucket %q", name)
   184  		}
   185  	}
   186  
   187  	return nil
   188  }
   189  
   190  // bucketJobStatus is used to safely track the status of
   191  // a job applied to each of a store's buckets when run
   192  // in parallel
   193  type bucketJobStatus struct {
   194  	sync.Mutex
   195  	buckets map[*Bucket]error
   196  }
   197  
   198  func newBucketJobStatus() *bucketJobStatus {
   199  	return &bucketJobStatus{
   200  		buckets: make(map[*Bucket]error),
   201  	}
   202  }
   203  
   204  type jobFunc func(context.Context, *Bucket) (interface{}, error)
   205  
   206  type rollbackFunc func(context.Context, *Bucket) error
   207  
   208  func (s *Store) ListFiles(ctx context.Context, basePath string) ([]string, error) {
   209  	listFiles := func(ctx context.Context, b *Bucket) (interface{}, error) {
   210  		basePath, err := filepath.Rel(basePath, b.dir)
   211  		if err != nil {
   212  			return nil, fmt.Errorf("bucket relative path: %w", err)
   213  		}
   214  		return b.ListFiles(ctx, basePath)
   215  	}
   216  
   217  	result, err := s.runJobOnBuckets(ctx, listFiles, nil)
   218  	if err != nil {
   219  		return nil, err
   220  	}
   221  
   222  	var files []string
   223  	for _, res := range result {
   224  		files = append(files, res.([]string)...)
   225  	}
   226  
   227  	return files, nil
   228  }
   229  
   230  // runJobOnBuckets applies a jobFunc to each bucket in the store in parallel.
   231  // The jobFunc allows for the job to return an arbitrary value.
   232  // Additionally, a rollbackFunc may be provided which will be run on the target
   233  // bucket in the event of an unsuccessful job.
   234  func (s *Store) runJobOnBuckets(ctx context.Context,
   235  	jobFunc jobFunc, rollbackFunc rollbackFunc,
   236  ) ([]interface{}, error) {
   237  	var (
   238  		status      = newBucketJobStatus()
   239  		resultQueue = make(chan interface{}, len(s.bucketsByName))
   240  		wg          = sync.WaitGroup{}
   241  	)
   242  
   243  	for _, bucket := range s.bucketsByName {
   244  		wg.Add(1)
   245  		b := bucket
   246  		f := func() {
   247  			status.Lock()
   248  			defer status.Unlock()
   249  			res, err := jobFunc(ctx, b)
   250  			resultQueue <- res
   251  			status.buckets[b] = err
   252  			wg.Done()
   253  		}
   254  		enterrors.GoWrapper(f, s.logger)
   255  	}
   256  
   257  	wg.Wait()
   258  	close(resultQueue)
   259  
   260  	var errs errorcompounder.ErrorCompounder
   261  	for _, err := range status.buckets {
   262  		errs.Add(err)
   263  	}
   264  
   265  	if errs.Len() != 0 {
   266  		// if any of the bucket jobs failed, and a
   267  		// rollbackFunc has been provided, attempt
   268  		// to roll back. if this fails, the err is
   269  		// added to the compounder
   270  		for b, jobErr := range status.buckets {
   271  			if jobErr != nil && rollbackFunc != nil {
   272  				if rollbackErr := rollbackFunc(ctx, b); rollbackErr != nil {
   273  					errs.AddWrap(rollbackErr, "bucket job rollback")
   274  				}
   275  			}
   276  		}
   277  
   278  		return nil, errs.ToError()
   279  	}
   280  
   281  	var finalResult []interface{}
   282  	for res := range resultQueue {
   283  		finalResult = append(finalResult, res)
   284  	}
   285  
   286  	return finalResult, nil
   287  }
   288  
   289  func (s *Store) GetBucketsByName() map[string]*Bucket {
   290  	s.bucketAccessLock.RLock()
   291  	defer s.bucketAccessLock.RUnlock()
   292  
   293  	newMap := map[string]*Bucket{}
   294  	for name, bucket := range s.bucketsByName {
   295  		newMap[name] = bucket
   296  	}
   297  
   298  	return newMap
   299  }
   300  
   301  // Creates bucket, first removing any files if already exist
   302  // Bucket can not be registered in bucketsByName before removal
   303  func (s *Store) CreateBucket(ctx context.Context, bucketName string,
   304  	opts ...BucketOption,
   305  ) error {
   306  	if b := s.Bucket(bucketName); b != nil {
   307  		return fmt.Errorf("bucket %s exists and is already in use", bucketName)
   308  	}
   309  
   310  	bucketDir := s.bucketDir(bucketName)
   311  	if err := os.RemoveAll(bucketDir); err != nil {
   312  		return errors.Wrapf(err, "failed removing bucket %s files", bucketName)
   313  	}
   314  
   315  	b, err := NewBucket(ctx, bucketDir, s.rootDir, s.logger, s.metrics,
   316  		s.cycleCallbacks.compactionCallbacks, s.cycleCallbacks.flushCallbacks, opts...)
   317  	if err != nil {
   318  		return err
   319  	}
   320  
   321  	s.setBucket(bucketName, b)
   322  	return nil
   323  }
   324  
   325  // Replaces 1st bucket with 2nd one. Both buckets have to registered in bucketsByName.
   326  // 2nd bucket swaps the 1st one in bucketsByName using 1st one's name, 2nd one's name is deleted.
   327  // Dir path of 2nd bucket is changed to dir of 1st bucket as well as all other related paths of
   328  // bucket resources (segment group, memtables, commit log).
   329  // Dir path of 1st bucket is temporarily suffixed with "___del", later on bucket is shutdown and
   330  // its files deleted.
   331  // 2nd bucket becomes 1st bucket
   332  func (s *Store) ReplaceBuckets(ctx context.Context, bucketName, replacementBucketName string) error {
   333  	s.bucketAccessLock.Lock()
   334  	defer s.bucketAccessLock.Unlock()
   335  
   336  	bucket := s.bucketsByName[bucketName]
   337  	if bucket == nil {
   338  		return fmt.Errorf("bucket '%s' not found", bucketName)
   339  	}
   340  	replacementBucket := s.bucketsByName[replacementBucketName]
   341  	if replacementBucket == nil {
   342  		return fmt.Errorf("replacement bucket '%s' not found", replacementBucketName)
   343  	}
   344  	s.bucketsByName[bucketName] = replacementBucket
   345  	delete(s.bucketsByName, replacementBucketName)
   346  
   347  	currBucketDir := bucket.dir
   348  	newBucketDir := bucket.dir + "___del"
   349  	currReplacementBucketDir := replacementBucket.dir
   350  	newReplacementBucketDir := currBucketDir
   351  
   352  	if err := os.Rename(currBucketDir, newBucketDir); err != nil {
   353  		return errors.Wrapf(err, "failed moving orig bucket dir '%s'", currBucketDir)
   354  	}
   355  	if err := os.Rename(currReplacementBucketDir, newReplacementBucketDir); err != nil {
   356  		return errors.Wrapf(err, "failed moving replacement bucket dir '%s'", currReplacementBucketDir)
   357  	}
   358  
   359  	s.updateBucketDir(bucket, currBucketDir, newBucketDir)
   360  	s.updateBucketDir(replacementBucket, currReplacementBucketDir, newReplacementBucketDir)
   361  
   362  	if err := bucket.Shutdown(ctx); err != nil {
   363  		return errors.Wrapf(err, "failed shutting down bucket old '%s'", bucketName)
   364  	}
   365  	if err := os.RemoveAll(newBucketDir); err != nil {
   366  		return errors.Wrapf(err, "failed removing dir '%s'", newBucketDir)
   367  	}
   368  
   369  	return nil
   370  }
   371  
   372  func (s *Store) RenameBucket(ctx context.Context, bucketName, newBucketName string) error {
   373  	s.bucketAccessLock.Lock()
   374  	defer s.bucketAccessLock.Unlock()
   375  
   376  	currBucket := s.bucketsByName[bucketName]
   377  	if currBucket == nil {
   378  		return fmt.Errorf("bucket '%s' not found", bucketName)
   379  	}
   380  	newBucket := s.bucketsByName[newBucketName]
   381  	if newBucket != nil {
   382  		return fmt.Errorf("bucket '%s' already exists", newBucketName)
   383  	}
   384  	s.bucketsByName[newBucketName] = currBucket
   385  	delete(s.bucketsByName, bucketName)
   386  
   387  	currBucketDir := currBucket.dir
   388  	newBucketDir := s.bucketDir(newBucketName)
   389  
   390  	if err := os.Rename(currBucketDir, newBucketDir); err != nil {
   391  		return errors.Wrapf(err, "failed renaming bucket dir '%s' to '%s'", currBucketDir, newBucketDir)
   392  	}
   393  
   394  	s.updateBucketDir(currBucket, currBucketDir, newBucketDir)
   395  	return nil
   396  }
   397  
   398  func (s *Store) updateBucketDir(bucket *Bucket, bucketDir, newBucketDir string) {
   399  	updatePath := func(src string) string {
   400  		return strings.Replace(src, bucketDir, newBucketDir, 1)
   401  	}
   402  
   403  	bucket.flushLock.Lock()
   404  	bucket.dir = newBucketDir
   405  	if bucket.active != nil {
   406  		bucket.active.path = updatePath(bucket.active.path)
   407  		bucket.active.commitlog.path = updatePath(bucket.active.commitlog.path)
   408  	}
   409  	if bucket.flushing != nil {
   410  		bucket.flushing.path = updatePath(bucket.flushing.path)
   411  		bucket.flushing.commitlog.path = updatePath(bucket.flushing.commitlog.path)
   412  	}
   413  	bucket.flushLock.Unlock()
   414  
   415  	bucket.disk.maintenanceLock.Lock()
   416  	bucket.disk.dir = newBucketDir
   417  	for _, segment := range bucket.disk.segments {
   418  		segment.path = updatePath(segment.path)
   419  	}
   420  	bucket.disk.maintenanceLock.Unlock()
   421  }