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 }