github.com/muhammadn/cortex@v1.9.1-0.20220510110439-46bb7000d03d/pkg/ingester/active_series.go (about) 1 package ingester 2 3 import ( 4 "hash" 5 "math" 6 "sync" 7 "time" 8 9 "github.com/cespare/xxhash" 10 "github.com/prometheus/common/model" 11 "github.com/prometheus/prometheus/pkg/labels" 12 "go.uber.org/atomic" 13 14 "github.com/cortexproject/cortex/pkg/util" 15 ) 16 17 const ( 18 numActiveSeriesStripes = 512 19 ) 20 21 // ActiveSeries is keeping track of recently active series for a single tenant. 22 type ActiveSeries struct { 23 stripes [numActiveSeriesStripes]activeSeriesStripe 24 } 25 26 // activeSeriesStripe holds a subset of the series timestamps for a single tenant. 27 type activeSeriesStripe struct { 28 // Unix nanoseconds. Only used by purge. Zero = unknown. 29 // Updated in purge and when old timestamp is used when updating series (in this case, oldestEntryTs is updated 30 // without holding the lock -- hence the atomic). 31 oldestEntryTs atomic.Int64 32 33 mu sync.RWMutex 34 refs map[uint64][]activeSeriesEntry 35 active int // Number of active entries in this stripe. Only decreased during purge or clear. 36 } 37 38 // activeSeriesEntry holds a timestamp for single series. 39 type activeSeriesEntry struct { 40 lbs labels.Labels 41 nanos *atomic.Int64 // Unix timestamp in nanoseconds. Needs to be a pointer because we don't store pointers to entries in the stripe. 42 } 43 44 func NewActiveSeries() *ActiveSeries { 45 c := &ActiveSeries{} 46 47 // Stripes are pre-allocated so that we only read on them and no lock is required. 48 for i := 0; i < numActiveSeriesStripes; i++ { 49 c.stripes[i].refs = map[uint64][]activeSeriesEntry{} 50 } 51 52 return c 53 } 54 55 // Updates series timestamp to 'now'. Function is called to make a copy of labels if entry doesn't exist yet. 56 func (c *ActiveSeries) UpdateSeries(series labels.Labels, now time.Time, labelsCopy func(labels.Labels) labels.Labels) { 57 fp := fingerprint(series) 58 stripeID := fp % numActiveSeriesStripes 59 60 c.stripes[stripeID].updateSeriesTimestamp(now, series, fp, labelsCopy) 61 } 62 63 var sep = []byte{model.SeparatorByte} 64 65 var hashPool = sync.Pool{New: func() interface{} { return xxhash.New() }} 66 67 func fingerprint(series labels.Labels) uint64 { 68 sum := hashPool.Get().(hash.Hash64) 69 defer hashPool.Put(sum) 70 71 sum.Reset() 72 for _, label := range series { 73 _, _ = sum.Write(util.YoloBuf(label.Name)) 74 _, _ = sum.Write(sep) 75 _, _ = sum.Write(util.YoloBuf(label.Value)) 76 _, _ = sum.Write(sep) 77 } 78 79 return sum.Sum64() 80 } 81 82 // Purge removes expired entries from the cache. This function should be called 83 // periodically to avoid memory leaks. 84 func (c *ActiveSeries) Purge(keepUntil time.Time) { 85 for s := 0; s < numActiveSeriesStripes; s++ { 86 c.stripes[s].purge(keepUntil) 87 } 88 } 89 90 //nolint // Linter reports that this method is unused, but it is. 91 func (c *ActiveSeries) clear() { 92 for s := 0; s < numActiveSeriesStripes; s++ { 93 c.stripes[s].clear() 94 } 95 } 96 97 func (c *ActiveSeries) Active() int { 98 total := 0 99 for s := 0; s < numActiveSeriesStripes; s++ { 100 total += c.stripes[s].getActive() 101 } 102 return total 103 } 104 105 func (s *activeSeriesStripe) updateSeriesTimestamp(now time.Time, series labels.Labels, fingerprint uint64, labelsCopy func(labels.Labels) labels.Labels) { 106 nowNanos := now.UnixNano() 107 108 e := s.findEntryForSeries(fingerprint, series) 109 entryTimeSet := false 110 if e == nil { 111 e, entryTimeSet = s.findOrCreateEntryForSeries(fingerprint, series, nowNanos, labelsCopy) 112 } 113 114 if !entryTimeSet { 115 if prev := e.Load(); nowNanos > prev { 116 entryTimeSet = e.CAS(prev, nowNanos) 117 } 118 } 119 120 if entryTimeSet { 121 for prevOldest := s.oldestEntryTs.Load(); nowNanos < prevOldest; { 122 // If recent purge already removed entries older than "oldest entry timestamp", setting this to 0 will make 123 // sure that next purge doesn't take the shortcut route. 124 if s.oldestEntryTs.CAS(prevOldest, 0) { 125 break 126 } 127 } 128 } 129 } 130 131 func (s *activeSeriesStripe) findEntryForSeries(fingerprint uint64, series labels.Labels) *atomic.Int64 { 132 s.mu.RLock() 133 defer s.mu.RUnlock() 134 135 // Check if already exists within the entries. 136 for ix, entry := range s.refs[fingerprint] { 137 if labels.Equal(entry.lbs, series) { 138 return s.refs[fingerprint][ix].nanos 139 } 140 } 141 142 return nil 143 } 144 145 func (s *activeSeriesStripe) findOrCreateEntryForSeries(fingerprint uint64, series labels.Labels, nowNanos int64, labelsCopy func(labels.Labels) labels.Labels) (*atomic.Int64, bool) { 146 s.mu.Lock() 147 defer s.mu.Unlock() 148 149 // Check if already exists within the entries. 150 for ix, entry := range s.refs[fingerprint] { 151 if labels.Equal(entry.lbs, series) { 152 return s.refs[fingerprint][ix].nanos, false 153 } 154 } 155 156 s.active++ 157 e := activeSeriesEntry{ 158 lbs: labelsCopy(series), 159 nanos: atomic.NewInt64(nowNanos), 160 } 161 162 s.refs[fingerprint] = append(s.refs[fingerprint], e) 163 164 return e.nanos, true 165 } 166 167 //nolint // Linter reports that this method is unused, but it is. 168 func (s *activeSeriesStripe) clear() { 169 s.mu.Lock() 170 defer s.mu.Unlock() 171 172 s.oldestEntryTs.Store(0) 173 s.refs = map[uint64][]activeSeriesEntry{} 174 s.active = 0 175 } 176 177 func (s *activeSeriesStripe) purge(keepUntil time.Time) { 178 keepUntilNanos := keepUntil.UnixNano() 179 if oldest := s.oldestEntryTs.Load(); oldest > 0 && keepUntilNanos <= oldest { 180 // Nothing to do. 181 return 182 } 183 184 s.mu.Lock() 185 defer s.mu.Unlock() 186 187 active := 0 188 189 oldest := int64(math.MaxInt64) 190 for fp, entries := range s.refs { 191 // Since we do expect very few fingerprint collisions, we 192 // have an optimized implementation for the common case. 193 if len(entries) == 1 { 194 ts := entries[0].nanos.Load() 195 if ts < keepUntilNanos { 196 delete(s.refs, fp) 197 continue 198 } 199 200 active++ 201 if ts < oldest { 202 oldest = ts 203 } 204 continue 205 } 206 207 // We have more entries, which means there's a collision, 208 // so we have to iterate over the entries. 209 for i := 0; i < len(entries); { 210 ts := entries[i].nanos.Load() 211 if ts < keepUntilNanos { 212 entries = append(entries[:i], entries[i+1:]...) 213 } else { 214 if ts < oldest { 215 oldest = ts 216 } 217 218 i++ 219 } 220 } 221 222 // Either update or delete the entries in the map 223 if cnt := len(entries); cnt == 0 { 224 delete(s.refs, fp) 225 } else { 226 active += cnt 227 s.refs[fp] = entries 228 } 229 } 230 231 if oldest == math.MaxInt64 { 232 s.oldestEntryTs.Store(0) 233 } else { 234 s.oldestEntryTs.Store(oldest) 235 } 236 s.active = active 237 } 238 239 func (s *activeSeriesStripe) getActive() int { 240 s.mu.RLock() 241 defer s.mu.RUnlock() 242 243 return s.active 244 }