github.com/ystia/yorc/v4@v4.3.0/storage/internal/file/store.go (about) 1 // Copyright 2018 Bull S.A.S. Atos Technologies - Bull, Rue Jean Jaures, B.P.68, 78340, Les Clayes-sous-Bois, France. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package file 16 17 import ( 18 "context" 19 "encoding/hex" 20 "github.com/ystia/yorc/v4/config" 21 "io/ioutil" 22 "os" 23 "path" 24 "path/filepath" 25 "strings" 26 "sync" 27 "time" 28 29 "github.com/dgraph-io/ristretto" 30 "github.com/pkg/errors" 31 "golang.org/x/sync/errgroup" 32 33 "github.com/ystia/yorc/v4/helper/collections" 34 "github.com/ystia/yorc/v4/log" 35 "github.com/ystia/yorc/v4/storage/encoding" 36 "github.com/ystia/yorc/v4/storage/encryption" 37 "github.com/ystia/yorc/v4/storage/store" 38 "github.com/ystia/yorc/v4/storage/utils" 39 ) 40 41 const indexFileNotExist = uint64(1) 42 43 const defaultConcurrencyLimit = 1000 44 45 type fileStore struct { 46 id string 47 properties config.DynamicMap 48 // For locking the locks map 49 // (no two goroutines may create a lock for a filename that doesn't have a lock yet). 50 locksLock *sync.Mutex 51 // For locking file access. 52 fileLocks map[string]*sync.RWMutex 53 filenameExtension string 54 directory string 55 codec encoding.Codec 56 withCache bool 57 cache *ristretto.Cache 58 withEncryption bool 59 encryptor *encryption.Encryptor 60 concurrencyLimit int 61 } 62 63 // NewStore returns a new File store 64 func NewStore(cfg config.Configuration, storeID string, properties config.DynamicMap, withCache, withEncryption bool) (store.Store, error) { 65 var err error 66 67 if properties == nil { 68 properties = config.DynamicMap{} 69 } 70 71 fs := &fileStore{ 72 id: storeID, 73 properties: properties, 74 codec: encoding.JSON, 75 filenameExtension: "json", 76 directory: properties.GetStringOrDefault("root_dir", path.Join(cfg.WorkingDirectory, "store")), 77 locksLock: new(sync.Mutex), 78 fileLocks: make(map[string]*sync.RWMutex), 79 withCache: withCache, 80 withEncryption: withEncryption, 81 concurrencyLimit: properties.GetIntOrDefault("concurrency_limit", defaultConcurrencyLimit), 82 } 83 84 // Instantiate cache if necessary 85 if withCache { 86 if err = fs.initCache(); err != nil { 87 return nil, errors.Wrapf(err, "failed to instantiate cache for file store") 88 } 89 } 90 91 // Instantiate encryptor if necessary 92 if withEncryption { 93 err = fs.buildEncryptor() 94 if err != nil { 95 return nil, err 96 } 97 } 98 99 return fs, nil 100 } 101 102 func (s *fileStore) initCache() error { 103 var err error 104 // Set Cache config 105 numCounters := s.properties.GetInt64("cache_num_counters") 106 if numCounters == 0 { 107 // 100 000 108 numCounters = 1e5 109 } 110 maxCost := s.properties.GetInt64("cache_max_cost") 111 if maxCost == 0 { 112 // 10 MB 113 maxCost = 1e7 114 } 115 bufferItems := s.properties.GetInt64("cache_buffer_items") 116 if bufferItems == 0 { 117 bufferItems = 64 118 } 119 120 log.Printf("Instantiate new cache with numCounters:%d, maxCost:%d, bufferItems: %d", numCounters, maxCost, bufferItems) 121 s.cache, err = ristretto.NewCache(&ristretto.Config{ 122 NumCounters: numCounters, // number of keys to track frequency 123 MaxCost: maxCost, // maximum cost of cache 124 BufferItems: bufferItems, // number of keys per Get buffer. 125 }) 126 return err 127 } 128 129 func (s *fileStore) buildEncryptor() error { 130 var err error 131 // Check a 32-bits passphrase key is provided 132 secretKey := s.properties.GetString("passphrase") 133 if secretKey == "" { 134 return errors.Errorf("Missing passphrase for file store encryption with ID:%q", s.id) 135 } 136 if len(secretKey) != 32 { 137 return errors.Errorf("The provided passphrase for file store encryption with ID:%q must be 32-bits length", s.id) 138 } 139 s.encryptor, err = encryption.NewEncryptor(hex.EncodeToString([]byte(secretKey))) 140 return err 141 } 142 143 // prepareFileLock returns an existing file lock or creates a new one 144 func (s *fileStore) prepareFileLock(filePath string) *sync.RWMutex { 145 s.locksLock.Lock() 146 lock, found := s.fileLocks[filePath] 147 if !found { 148 lock = new(sync.RWMutex) 149 s.fileLocks[filePath] = lock 150 } 151 s.locksLock.Unlock() 152 return lock 153 } 154 155 func (s *fileStore) buildFilePath(k string, withExtension bool) string { 156 filePath := k 157 if withExtension && s.filenameExtension != "" { 158 filePath += "." + s.filenameExtension 159 } 160 return filepath.Join(s.directory, filePath) 161 } 162 163 func (s *fileStore) extractKeyFromFilePath(filePath string, withExtension bool) string { 164 key := strings.TrimPrefix(filePath, s.directory+string(os.PathSeparator)) 165 if !withExtension { 166 return key 167 } 168 key = strings.TrimSuffix(key, "."+s.filenameExtension) 169 return key 170 } 171 172 func (s *fileStore) Set(ctx context.Context, k string, v interface{}) error { 173 if err := utils.CheckKeyAndValue(k, v); err != nil { 174 return err 175 } 176 177 data, err := s.codec.Marshal(v) 178 if err != nil { 179 return errors.Wrapf(err, "failed to marshal value %+v due to error:%+v", v, err) 180 } 181 182 // Prepare file lock. 183 filePath := s.buildFilePath(k, true) 184 lock := s.prepareFileLock(filePath) 185 186 // File lock and file handling. 187 lock.Lock() 188 defer lock.Unlock() 189 190 err = os.MkdirAll(path.Dir(filePath), 0700) 191 if err != nil { 192 return err 193 } 194 195 // Copy to cache if necessary 196 if s.withCache { 197 s.cache.Set(k, data, 1) 198 } 199 200 // encrypt if necessary 201 if s.withEncryption { 202 data, err = s.encryptor.Encrypt(data) 203 if err != nil { 204 return err 205 } 206 } 207 return ioutil.WriteFile(filePath, data, 0600) 208 } 209 210 func (s *fileStore) SetCollection(ctx context.Context, keyValues []store.KeyValueIn) error { 211 if keyValues == nil { 212 return nil 213 } 214 errGroup, ctx := errgroup.WithContext(ctx) 215 sem := make(chan struct{}, s.concurrencyLimit) 216 for _, kv := range keyValues { 217 sem <- struct{}{} 218 kvItem := kv 219 errGroup.Go(func() error { 220 defer func() { 221 <-sem 222 }() 223 return s.Set(ctx, kvItem.Key, kvItem.Value) 224 }) 225 } 226 227 return errGroup.Wait() 228 } 229 230 func (s *fileStore) Get(k string, v interface{}) (bool, error) { 231 if err := utils.CheckKeyAndValue(k, v); err != nil { 232 return false, err 233 } 234 235 // Check cache first 236 if s.withCache { 237 value, has := s.cache.Get(k) 238 if has { 239 data, ok := value.([]byte) 240 if ok { 241 return true, errors.Wrapf(s.codec.Unmarshal(data, v), "failed to unmarshal data:%q", string(data)) 242 } 243 log.Printf("[WARNING] Failed to cast retrieved value from cache to bytes array for key:%q. Data will be retrieved from store.", k) 244 } 245 } 246 247 filePath := s.buildFilePath(k, true) 248 exist, _, err := s.getValueFromFile(filePath, v) 249 return exist, err 250 } 251 252 func (s *fileStore) getValueFromFile(filePath string, v interface{}) (bool, []byte, error) { 253 // Prepare file lock. 254 lock := s.prepareFileLock(filePath) 255 256 // File lock and file handling. 257 lock.RLock() 258 // Deferring the unlocking would lead to the unmarshalling being done during the lock, which is bad for performance. 259 data, err := ioutil.ReadFile(filePath) 260 lock.RUnlock() 261 if err != nil { 262 if os.IsNotExist(err) { 263 return false, nil, nil 264 } 265 return false, nil, err 266 } 267 268 // decrypt if necessary 269 if s.withEncryption { 270 data, err = s.encryptor.Decrypt(data) 271 if err != nil { 272 return false, nil, err 273 } 274 } 275 276 return true, data, errors.Wrapf(s.codec.Unmarshal(data, v), "failed to unmarshal data:%q", string(data)) 277 } 278 279 func (s *fileStore) Exist(k string) (bool, error) { 280 filePath := s.buildFilePath(k, true) 281 282 _, err := os.Stat(filePath) 283 if err != nil { 284 if os.IsNotExist(err) { 285 return false, nil 286 } 287 return false, err 288 } 289 290 return true, nil 291 } 292 293 func (s *fileStore) Keys(k string) ([]string, error) { 294 filePath := s.buildFilePath(k, false) 295 296 files, err := ioutil.ReadDir(filePath) 297 if err != nil { 298 if os.IsNotExist(err) { 299 return nil, nil 300 } 301 return nil, err 302 } 303 304 result := make([]string, 0) 305 for _, file := range files { 306 fileName := file.Name() 307 // return the whole key path without specific extension 308 result = append(result, path.Join(k, strings.TrimSuffix(fileName, "."+s.filenameExtension))) 309 } 310 311 return collections.RemoveDuplicates(result), nil 312 } 313 314 func (s *fileStore) Delete(ctx context.Context, k string, recursive bool) error { 315 if err := utils.CheckKey(k); err != nil { 316 return err 317 } 318 319 // Handle cache 320 if s.withCache { 321 if err := s.clearCache(ctx, k, recursive); err != nil { 322 return err 323 } 324 } 325 326 var err error 327 328 // Try to delete a single file in all cases 329 filePath := s.buildFilePath(k, true) 330 // Prepare file lock. 331 lock := s.prepareFileLock(filePath) 332 333 // File lock and file handling. 334 lock.Lock() 335 defer lock.Unlock() 336 337 err = os.Remove(filePath) 338 339 // Try to delete a directory if recursive is true 340 if recursive { 341 // Remove the whole directory 342 err = os.RemoveAll(s.buildFilePath(k, false)) 343 } 344 345 if os.IsNotExist(err) { 346 return nil 347 } 348 return err 349 } 350 351 func (s *fileStore) clearCache(ctx context.Context, k string, recursive bool) error { 352 s.cache.Del(k) 353 354 if !recursive { 355 return nil 356 } 357 358 // Delete sub-keys 359 keys, err := s.Keys(k) 360 if err != nil { 361 return err 362 } 363 errGroup, ctx := errgroup.WithContext(ctx) 364 for _, key := range keys { 365 key := key 366 errGroup.Go(func() error { 367 return s.clearCache(ctx, key, true) 368 }) 369 } 370 371 return errGroup.Wait() 372 } 373 374 func (s *fileStore) GetLastModifyIndex(k string) (uint64, error) { 375 // key can be directory or file, ie with or without extension 376 // let's try first without extension as it can be the most current case 377 rootPath := s.buildFilePath(k, false) 378 fInfo, err := os.Stat(rootPath) 379 380 if err != nil { 381 if !os.IsNotExist(err) { 382 return 0, errors.Wrapf(err, "failed to get last index for key:%q", k) 383 } 384 // not a directory, let's try a file with extension 385 fp := s.buildFilePath(k, true) 386 fInfo, err = os.Stat(fp) 387 if err != nil { 388 if !os.IsNotExist(err) { 389 return 0, errors.Wrapf(err, "failed to get last index for key:%q", k) 390 } 391 // File not found : the key doesn't exist 392 return indexFileNotExist, nil 393 } 394 return uint64(fInfo.ModTime().UnixNano()), nil 395 } 396 397 // For a directory, lastIndex is determined by the max modify index of sub-directories 398 var index, lastIndex uint64 399 err = filepath.Walk(rootPath, func(pathFile string, info os.FileInfo, err error) error { 400 if err != nil { 401 return err 402 } 403 if info.IsDir() { 404 index = uint64(info.ModTime().UnixNano()) 405 if index > lastIndex { 406 lastIndex = index 407 } 408 } 409 return nil 410 }) 411 return lastIndex, err 412 } 413 414 func (s *fileStore) List(ctx context.Context, k string, waitIndex uint64, timeout time.Duration) ([]store.KeyValueOut, uint64, error) { 415 if waitIndex == 0 { 416 index, err := s.GetLastModifyIndex(k) 417 if err != nil { 418 return nil, index, err 419 } 420 return s.list(ctx, k, waitIndex, index) 421 } 422 423 // Default timeout to 5 minutes if not set as param or as config property 424 if timeout == 0 { 425 timeout = s.properties.GetDuration("blocking_query_default_timeout") 426 if timeout == 0 { 427 timeout = 5 * time.Minute 428 } 429 } 430 // last index Lookup time interval 431 timeAfter := time.After(timeout) 432 ticker := time.NewTicker(100 * time.Millisecond) 433 var stop bool 434 index := uint64(0) 435 var err error 436 for index <= waitIndex && !stop { 437 select { 438 case <-ctx.Done(): 439 log.Debugf("Cancel signal has been received: the store List query is stopped") 440 ticker.Stop() 441 stop = true 442 case <-timeAfter: 443 log.Debugf("Timeout has been reached: the store List query is stopped") 444 ticker.Stop() 445 stop = true 446 case <-ticker.C: 447 index, err = s.GetLastModifyIndex(k) 448 if err != nil { 449 return nil, index, err 450 } 451 } 452 } 453 454 return s.list(ctx, k, waitIndex, index) 455 } 456 457 func (s *fileStore) listFirstLevelTree(rootPath string, waitIndex, lastIndex uint64) ([]string, []store.KeyValueOut, error) { 458 // Retrieve all sub-directories to list keys in concurrency at first level 459 var subPaths []string 460 infos, err := ioutil.ReadDir(rootPath) 461 if err != nil { 462 return subPaths, nil, err 463 } 464 465 kvs := make([]store.KeyValueOut, 0) 466 for _, info := range infos { 467 pathFile := path.Join(rootPath, info.Name()) 468 if info.IsDir() { 469 subPaths = append(subPaths, pathFile) 470 } else { 471 kv, err := s.addKeyValueToList(info, pathFile, waitIndex, lastIndex) 472 if err != nil { 473 return subPaths, nil, err 474 } 475 if kv != nil { 476 kvs = append(kvs, *kv) 477 } 478 } 479 } 480 return subPaths, kvs, err 481 } 482 483 func (s *fileStore) list(ctx context.Context, k string, waitIndex, lastIndex uint64) ([]store.KeyValueOut, uint64, error) { 484 rootPath := s.buildFilePath(k, false) 485 fInfo, err := os.Stat(rootPath) 486 if err != nil { 487 if os.IsNotExist(err) { 488 return nil, indexFileNotExist, nil 489 } 490 return nil, 0, err 491 } 492 // Not a directory so nothing to do 493 if !fInfo.IsDir() { 494 return nil, 0, nil 495 } 496 497 // List first-level tree directories in order to walk them concurrently 498 subPaths, kvs, err := s.listFirstLevelTree(rootPath, waitIndex, lastIndex) 499 500 errGroup, ctx := errgroup.WithContext(ctx) 501 sem := make(chan struct{}, s.concurrencyLimit) 502 503 // Use a channel to provide kvs concurrently safe 504 c := make(chan store.KeyValueOut) 505 for _, subPath := range subPaths { 506 pathItem := subPath 507 sem <- struct{}{} 508 errGroup.Go(func() error { 509 defer func() { 510 <-sem 511 }() 512 return s.walk(c, pathItem, k, waitIndex, lastIndex) 513 }) 514 } 515 516 go func() { 517 errGroup.Wait() 518 close(c) 519 }() 520 521 // Fill the kvs from channel once all goroutines are finished 522 for r := range c { 523 kvs = append(kvs, r) 524 } 525 526 return kvs, lastIndex, errGroup.Wait() 527 } 528 529 func (s *fileStore) walk(c chan store.KeyValueOut, pathItem, k string, waitIndex, lastIndex uint64) error { 530 err := filepath.Walk(pathItem, func(pathFile string, info os.FileInfo, err error) error { 531 if err != nil { 532 return err 533 } 534 // Add kv for a file 535 if !info.IsDir() { 536 kv, err := s.addKeyValueToList(info, pathFile, waitIndex, lastIndex) 537 if err != nil { 538 return err 539 } 540 if kv != nil { 541 c <- *kv 542 } 543 } 544 return nil 545 }) 546 return err 547 } 548 549 func (s *fileStore) addKeyValueToList(info os.FileInfo, pathFile string, waitIndex, lastIndex uint64) (*store.KeyValueOut, error) { 550 index := uint64(info.ModTime().UnixNano()) 551 // Retrieve only files before last modification Index 552 if index > waitIndex && (lastIndex == 0 || index <= lastIndex) { 553 var value map[string]interface{} 554 _, raw, err := s.getValueFromFile(pathFile, &value) 555 if err != nil { 556 return nil, err 557 } 558 559 return &store.KeyValueOut{ 560 Key: s.extractKeyFromFilePath(pathFile, true), 561 LastModifyIndex: index, 562 Value: value, 563 RawValue: raw, 564 }, nil 565 } 566 return nil, nil 567 }