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  }