github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/pkg/graveler/settings/manager.go (about)

     1  package settings
     2  
     3  import (
     4  	"context"
     5  	"crypto/sha256"
     6  	"encoding/hex"
     7  	"errors"
     8  	"time"
     9  
    10  	"github.com/go-openapi/swag"
    11  	"github.com/treeverse/lakefs/pkg/cache"
    12  	"github.com/treeverse/lakefs/pkg/graveler"
    13  	"github.com/treeverse/lakefs/pkg/kv"
    14  	"github.com/treeverse/lakefs/pkg/logging"
    15  	"google.golang.org/protobuf/encoding/protojson"
    16  	"google.golang.org/protobuf/proto"
    17  )
    18  
    19  const (
    20  	cacheSize          = 100_000
    21  	defaultCacheExpiry = 3 * time.Second
    22  )
    23  
    24  type cacheKey struct {
    25  	RepositoryID graveler.RepositoryID
    26  	Key          string
    27  }
    28  
    29  // Manager is a key-value store for Graveler repository-level settings.
    30  // Each setting is stored under a key, and can be any proto.Message.
    31  // Fetched settings are cached using cache.Cache with a default expiry time of 1 second. Hence, the store is eventually consistent.
    32  type Manager struct {
    33  	store      kv.Store
    34  	refManager graveler.RefManager
    35  	cache      cache.Cache
    36  }
    37  
    38  type ManagerOption func(m *Manager)
    39  
    40  func WithCache(cache cache.Cache) ManagerOption {
    41  	return func(m *Manager) {
    42  		m.WithCache(cache)
    43  	}
    44  }
    45  
    46  func (m *Manager) WithCache(cache cache.Cache) {
    47  	m.cache = cache
    48  }
    49  
    50  func NewManager(refManager graveler.RefManager, store kv.Store, options ...ManagerOption) *Manager {
    51  	m := &Manager{
    52  		refManager: refManager,
    53  		store:      store,
    54  		cache:      cache.NewCache(cacheSize, defaultCacheExpiry, cache.NewJitterFn(defaultCacheExpiry)),
    55  	}
    56  	for _, o := range options {
    57  		o(m)
    58  	}
    59  	return m
    60  }
    61  
    62  // Save persists the given setting under the given repository and key. Overrides settings key in KV Store.
    63  // The setting is persisted only if the current version of the setting matches the given checksum.
    64  // If lastKnownChecksum is the empty string, the setting is persisted only if it does not exist.
    65  // If lastKnownChecksum is nil, the setting is persisted unconditionally.
    66  func (m *Manager) Save(ctx context.Context, repository *graveler.RepositoryRecord, key string, setting proto.Message, lastKnownChecksum *string) error {
    67  	logSetting(logging.FromContext(ctx), repository.RepositoryID, key, setting, "saving repository-level setting")
    68  	repoPartition := graveler.RepoPartition(repository)
    69  	keyPath := []byte(graveler.SettingsPath(key))
    70  	if lastKnownChecksum == nil {
    71  		return kv.SetMsg(ctx, m.store, repoPartition, keyPath, setting)
    72  	}
    73  	if *lastKnownChecksum == "" {
    74  		err := kv.SetMsgIf(ctx, m.store, repoPartition, keyPath, setting, nil)
    75  		if errors.Is(err, kv.ErrPredicateFailed) {
    76  			return graveler.ErrPreconditionFailed
    77  		}
    78  		return err
    79  	}
    80  	valueWithPredicate, err := m.store.Get(ctx, []byte(repoPartition), keyPath)
    81  	if errors.Is(err, kv.ErrNotFound) {
    82  		return graveler.ErrPreconditionFailed
    83  	}
    84  	if err != nil {
    85  		return err
    86  	}
    87  	currentChecksum, err := computeChecksum(valueWithPredicate.Value)
    88  	if err != nil {
    89  		return err
    90  	}
    91  	if *currentChecksum != *lastKnownChecksum {
    92  		return graveler.ErrPreconditionFailed
    93  	}
    94  	err = kv.SetMsgIf(ctx, m.store, repoPartition, keyPath, setting, valueWithPredicate.Predicate)
    95  	if errors.Is(err, kv.ErrPredicateFailed) {
    96  		return graveler.ErrPreconditionFailed
    97  	}
    98  	return err
    99  }
   100  
   101  func computeChecksum(value []byte) (*string, error) {
   102  	if len(value) == 0 {
   103  		// empty value checksum is the empty string
   104  		return swag.String(""), nil
   105  	}
   106  	h := sha256.New()
   107  	_, err := h.Write(value)
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  	return swag.String(hex.EncodeToString(h.Sum(nil))), nil
   112  }
   113  
   114  // GetLatest loads the latest setting into dst.
   115  // It returns a checksum representing the version of the setting, which can be passed to SaveIf for conditional updates.
   116  // The checksum of a non-existent setting is the empty string.
   117  func (m *Manager) GetLatest(ctx context.Context, repository *graveler.RepositoryRecord, key string, dst proto.Message) (*string, error) {
   118  	settings, err := m.store.Get(ctx, []byte(graveler.RepoPartition(repository)), []byte(graveler.SettingsPath(key)))
   119  	if errors.Is(err, kv.ErrNotFound) {
   120  		// return empty list and do not consider this an error
   121  		proto.Reset(dst)
   122  		return swag.String(""), nil
   123  	}
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  	checksum, err := computeChecksum(settings.Value)
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  	err = proto.Unmarshal(settings.Value, dst)
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  	logSetting(logging.FromContext(ctx), repository.RepositoryID, key, dst, "got repository-level setting")
   136  	return checksum, nil
   137  }
   138  
   139  // Get fetches the setting under the given repository and key, and loads it into dst.
   140  // The result is eventually consistent: it is not guaranteed to be the most up-to-date setting. The cache expiry period is 1 second.
   141  func (m *Manager) Get(ctx context.Context, repository *graveler.RepositoryRecord, key string, dst proto.Message) error {
   142  	k := cacheKey{
   143  		RepositoryID: repository.RepositoryID,
   144  		Key:          key,
   145  	}
   146  	tmp := proto.Clone(dst)
   147  	setting, err := m.cache.GetOrSet(k, func() (v interface{}, err error) {
   148  		_, err = m.GetLatest(ctx, repository, key, tmp)
   149  		if errors.Is(err, graveler.ErrNotFound) {
   150  			// do not return this error here, so that empty settings are cached
   151  			return tmp, nil
   152  		}
   153  		return tmp, err
   154  	})
   155  	if err != nil {
   156  		return err
   157  	}
   158  	if setting == nil {
   159  		return graveler.ErrNotFound
   160  	}
   161  	proto.Merge(dst, setting.(proto.Message))
   162  	return nil
   163  }
   164  
   165  func logSetting(logger logging.Logger, repositoryID graveler.RepositoryID, key string, setting proto.Message, logMsg string) {
   166  	if logger.IsTracing() {
   167  		logger.
   168  			WithField("repo", repositoryID).
   169  			WithField("key", key).
   170  			WithField("setting", protojson.Format(setting)).
   171  			Trace(logMsg)
   172  	}
   173  }