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  }