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 }