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 }