github.com/grafana/pyroscope@v1.18.0/pkg/settings/store/store.go (about) 1 package store 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "path/filepath" 10 "strconv" 11 "sync" 12 13 "github.com/go-kit/log" 14 "github.com/go-kit/log/level" 15 "github.com/thanos-io/objstore" 16 ) 17 18 type Key struct { 19 TenantID string 20 } 21 22 var ErrElementNotFound = errors.New("element not found") 23 24 type ErrConflictGeneration struct { 25 ObservedGeneration int64 26 StoreGeneration int64 27 } 28 29 func (e ErrConflictGeneration) Error() string { 30 return fmt.Sprintf("conflicting update, please try again: observed_generation=%d, store_generation=%d", e.ObservedGeneration, e.StoreGeneration) 31 } 32 33 type StoreHelper[T any] interface { 34 ID(T) string 35 GetGeneration(T) int64 36 SetGeneration(T, int64) 37 FromStore(json.RawMessage) (T, error) 38 ToStore(T) (json.RawMessage, error) 39 TypePath() string 40 } 41 42 type Collection[T any] struct { 43 Generation int64 44 Elements []T 45 } 46 47 type GenericStore[T any, H StoreHelper[T]] struct { 48 logger log.Logger 49 bucket objstore.Bucket 50 helper H 51 path string 52 53 cacheLock sync.RWMutex 54 cache *Collection[T] 55 } 56 57 func New[T any, H StoreHelper[T]]( 58 logger log.Logger, bucket objstore.Bucket, key Key, helper H, 59 ) *GenericStore[T, H] { 60 return &GenericStore[T, H]{ 61 logger: logger, 62 bucket: bucket, 63 helper: helper, 64 path: filepath.Join(key.TenantID, helper.TypePath()) + ".json", 65 } 66 } 67 68 // ReadTxn is a transaction that runs under the read lock of the cache. The Collection should not be mutated at all. 69 type ReadTxn[T any] func(context.Context, *Collection[T]) error 70 71 func (s *GenericStore[T, H]) Read(ctx context.Context, txn ReadTxn[T]) error { 72 // serve from cache if available 73 s.cacheLock.RLock() 74 if s.cache != nil { 75 defer s.cacheLock.RUnlock() 76 return txn(ctx, s.cache) 77 } 78 s.cacheLock.RUnlock() 79 80 // get write lock and fetch from bucket 81 s.cacheLock.Lock() 82 defer s.cacheLock.Unlock() 83 84 // check again if cache is available in the meantime 85 if s.cache != nil { 86 return txn(ctx, s.cache) 87 } 88 89 // load from bucket 90 if err := s.unsafeLoadCache(ctx); err != nil { 91 return err 92 } 93 94 return txn(ctx, s.cache) 95 } 96 97 func (s *GenericStore[T, H]) Get(ctx context.Context) (*Collection[T], error) { 98 var result *Collection[T] 99 err := s.Read(ctx, func(ctx context.Context, coll *Collection[T]) error { 100 result = coll 101 return nil 102 }) 103 if err != nil { 104 return nil, err 105 } 106 return result, nil 107 108 } 109 110 func (s *GenericStore[T, H]) Delete(ctx context.Context, id string) error { 111 return s.Update(ctx, func(_ context.Context, coll *Collection[T]) error { 112 // iterate over the rules to find the rule 113 for idx, e := range coll.Elements { 114 if s.helper.ID(e) == id { 115 // delete the rule 116 coll.Elements = append(coll.Elements[:idx], coll.Elements[idx+1:]...) 117 118 // return early and save the ruleset 119 return nil 120 } 121 } 122 return ErrElementNotFound 123 }) 124 } 125 126 func (s *GenericStore[T, H]) Upsert(ctx context.Context, elem T, observedGeneration *int64) error { 127 return s.Update(ctx, func(_ context.Context, coll *Collection[T]) error { 128 // iterate over the store list to find the element with the same idx 129 pos := -1 130 for idx, e := range coll.Elements { 131 if s.helper.ID(e) == s.helper.ID(elem) { 132 pos = idx 133 } 134 } 135 136 // new element required 137 if pos == -1 { 138 // create a new rule 139 coll.Elements = append(coll.Elements, elem) 140 141 // by definition, the generation of a new element is 1 142 s.helper.SetGeneration(elem, 1) 143 144 return nil 145 } 146 147 // check if there had been a conflicted updated 148 storedElem := coll.Elements[pos] 149 storedGeneration := s.helper.GetGeneration(storedElem) 150 if observedGeneration != nil && *observedGeneration != storedGeneration { 151 level.Warn(s.logger).Log( 152 "msg", "conflicting update, please try again", 153 "observed_generation", observedGeneration, 154 "stored_generation", storedGeneration, 155 ) 156 157 return &ErrConflictGeneration{ 158 ObservedGeneration: *observedGeneration, 159 StoreGeneration: storedGeneration, 160 } 161 } 162 163 s.helper.SetGeneration(elem, storedGeneration+1) 164 coll.Elements[pos] = elem 165 166 return nil 167 }) 168 } 169 170 type UpdateTxn[T any] func(context.Context, *Collection[T]) error 171 172 // Update will under write lock, call a transaction the Collection. If there is an error returned, the update will be cancelled. 173 func (s *GenericStore[T, H]) Update( 174 ctx context.Context, 175 txn UpdateTxn[T], 176 ) error { 177 // get write lock and fetch from bucket 178 s.cacheLock.Lock() 179 defer s.cacheLock.Unlock() 180 181 // ensure we have the latest data 182 data, err := s.getFromBucket(ctx) 183 if err != nil { 184 return err 185 } 186 187 // call callback 188 if err := txn(ctx, data); err != nil { 189 return err 190 } 191 192 // save the changes 193 return s.unsafeFlush(ctx, data) 194 } 195 196 type storeStruct struct { 197 Generation string `json:"generation"` 198 Elements []json.RawMessage `json:"elements,omitempty"` 199 ElementsCompat []json.RawMessage `json:"rules,omitempty"` 200 } 201 202 func (s *GenericStore[T, H]) getFromBucket(ctx context.Context) (*Collection[T], error) { 203 // fetch from bucket 204 r, err := s.bucket.Get(ctx, s.path) 205 if s.bucket.IsObjNotFoundErr(err) { 206 return &Collection[T]{ 207 Elements: make([]T, 0), 208 }, nil 209 } 210 if err != nil { 211 return nil, err 212 } 213 defer func() { 214 _ = r.Close() 215 }() 216 217 var storeStruct storeStruct 218 if err := json.NewDecoder(r).Decode(&storeStruct); err != nil { 219 return nil, err 220 } 221 222 // handle compatibility with old model 223 if len(storeStruct.Elements) == 0 { 224 storeStruct.Elements = storeStruct.ElementsCompat 225 } 226 227 var ( 228 result = make([]T, len(storeStruct.Elements)) 229 ) 230 for idx, element := range storeStruct.Elements { 231 result[idx], err = s.helper.FromStore(element) 232 if err != nil { 233 return nil, err 234 } 235 } 236 237 generation, err := strconv.ParseInt(storeStruct.Generation, 10, 64) 238 if err != nil { 239 return nil, fmt.Errorf("invalid generation: %s", storeStruct.Generation) 240 } 241 242 return &Collection[T]{ 243 Generation: generation, 244 Elements: result, 245 }, nil 246 } 247 248 // unsafeLoad reads from bucket into the cache, only call with write lock held 249 func (s *GenericStore[T, H]) unsafeLoadCache(ctx context.Context) error { 250 // fetch from bucket 251 data, err := s.getFromBucket(ctx) 252 if err != nil { 253 return err 254 } 255 256 s.cache = data 257 return nil 258 } 259 260 // unsafeFlush writes from arguments into the bucket and then reset cache. Only call with write lock held 261 func (s *GenericStore[T, H]) unsafeFlush(ctx context.Context, coll *Collection[T]) error { 262 var ( 263 data = storeStruct{ 264 Elements: make([]json.RawMessage, len(coll.Elements)), 265 Generation: strconv.FormatInt(coll.Generation+1, 10), 266 } 267 err error 268 ) 269 for idx, element := range coll.Elements { 270 data.Elements[idx], err = s.helper.ToStore(element) 271 if err != nil { 272 return err 273 } 274 } 275 276 dataJson, err := json.Marshal(data) 277 if err != nil { 278 return err 279 } 280 281 // reset cache 282 s.cache = nil 283 284 // write to bucket 285 return s.bucket.Upload(ctx, s.path, bytes.NewReader(dataJson)) 286 }