github.com/grafana/pyroscope@v1.18.0/pkg/metrics/ruler.go (about)

     1  package metrics
     2  
     3  import (
     4  	"sync"
     5  	"time"
     6  
     7  	"github.com/go-kit/log"
     8  	"github.com/go-kit/log/level"
     9  
    10  	"github.com/grafana/pyroscope/pkg/model"
    11  	"github.com/grafana/pyroscope/pkg/validation"
    12  )
    13  
    14  const (
    15  	rulesExpiryTime = time.Minute
    16  )
    17  
    18  type StaticRuler struct {
    19  	overrides *validation.Overrides
    20  }
    21  
    22  func NewStaticRulerFromOverrides(overrides *validation.Overrides) Ruler {
    23  	return &StaticRuler{
    24  		overrides: overrides,
    25  	}
    26  }
    27  
    28  func (ruler StaticRuler) RecordingRules(tenant string) []*model.RecordingRule {
    29  	rules := ruler.overrides.RecordingRules(tenant)
    30  	rs := make([]*model.RecordingRule, 0, len(rules))
    31  	for _, rule := range rules {
    32  		// should never fail, overrides already validated
    33  		r, _ := model.NewRecordingRule(rule)
    34  		rs = append(rs, r)
    35  	}
    36  	return rs
    37  }
    38  
    39  // CachedRemoteRuler is a thread-safe ruler that retrieves rules from an external service.
    40  // It has a per-tenant cache: rulesPerTenant
    41  type CachedRemoteRuler struct {
    42  	rulesPerTenant map[string]*tenantCache
    43  	mu             sync.RWMutex
    44  
    45  	client RecordingRulesClient
    46  
    47  	logger log.Logger
    48  }
    49  
    50  type RecordingRulesClient interface {
    51  	RecordingRules(tenant string) ([]*model.RecordingRule, error)
    52  }
    53  
    54  func NewCachedRemoteRuler(client RecordingRulesClient, logger log.Logger) (Ruler, error) {
    55  	return &CachedRemoteRuler{
    56  		rulesPerTenant: make(map[string]*tenantCache),
    57  		client:         client,
    58  		logger:         logger,
    59  	}, nil
    60  }
    61  
    62  func (r *CachedRemoteRuler) RecordingRules(tenant string) []*model.RecordingRule {
    63  	// get the per-tenant cache
    64  	r.mu.RLock()
    65  	cache, ok := r.rulesPerTenant[tenant]
    66  	r.mu.RUnlock()
    67  
    68  	// There's no cache for given tenant: init it
    69  	if !ok {
    70  		r.mu.Lock()
    71  		defer r.mu.Unlock()
    72  
    73  		// only race-winner will initialize the per-tenant cache
    74  		cache, ok = r.rulesPerTenant[tenant]
    75  		if !ok {
    76  			cache = &tenantCache{
    77  				initFunc: func() ([]*model.RecordingRule, error) {
    78  					return r.client.RecordingRules(tenant)
    79  				},
    80  				logger: r.logger,
    81  			}
    82  			r.rulesPerTenant[tenant] = cache
    83  		}
    84  	}
    85  
    86  	// get data from cache:
    87  	return cache.get()
    88  }
    89  
    90  // tenantCache is a thread-safe cache that holds an expirable array of rules.
    91  type tenantCache struct {
    92  	value    []*model.RecordingRule
    93  	ttl      time.Time
    94  	initFunc func() ([]*model.RecordingRule, error)
    95  	mu       sync.RWMutex
    96  	logger   log.Logger
    97  }
    98  
    99  // get returns the stored value if present and not expired.
   100  // Otherwise, a single call to initFunc will be performed to retrieve the value and hold it for future calls within
   101  // the ttl.
   102  func (c *tenantCache) get() []*model.RecordingRule {
   103  	c.mu.RLock()
   104  	if c.value != nil && time.Now().Before(c.ttl) {
   105  		defer c.mu.RUnlock()
   106  		// value exists and didn't expired
   107  		return c.value
   108  	}
   109  	c.mu.RUnlock()
   110  
   111  	c.mu.Lock()
   112  	defer c.mu.Unlock()
   113  
   114  	// only race-winner will fetch the data
   115  	if c.value == nil || time.Now().After(c.ttl) {
   116  		value, err := c.initFunc()
   117  		if err != nil {
   118  			// keep old value and ttl, just log an error
   119  			level.Error(c.logger).Log("msg", "failed to fetch recording rules", "err", err)
   120  		} else {
   121  			c.value = value
   122  			c.ttl = time.Now().Add(rulesExpiryTime)
   123  		}
   124  	}
   125  	return c.value
   126  }