github.com/insolar/vanilla@v0.0.0-20201023172447-248fdf805322/cachekit/core.go (about) 1 // Copyright 2020 Insolar Network Ltd. 2 // All rights reserved. 3 // This material is licensed under the Insolar License version 1.0, 4 // available at https://github.com/insolar/assured-ledger/blob/master/LICENSE.md. 5 6 package cachekit 7 8 import ( 9 "github.com/insolar/vanilla/throw" 10 ) 11 12 type Index = int 13 type Age = int64 14 type GenNo = int 15 16 // Strategy defines behavior of cache Core 17 type Strategy interface { 18 // TrimOnEachAddition is called once on Core creation. When result is true, then CanTrimEntries will be invoked on every addition. 19 // When this value is false, cache capacity can be exceeded upto an average size of a generation page. 20 TrimOnEachAddition() bool 21 22 // CurrentAge should provide time marks when this Strategy needs to use time-based retention. 23 CurrentAge() Age 24 25 // AllocationPageSize is called once on Core creation to provide a size of entry pages (# of items). 26 // It is recommended for cache implementation to use same size for paged storage. 27 AllocationPageSize() int 28 29 // NextGenerationCapacity is called on creation of every generation page. 30 // Parameters are length and capacity of the previous generation page, or (-1, -1) for a first page, 31 // It returns a capacity for a new page and a flag when fencing must be applied. A new generation page will be created when capacity is exhausted. 32 // Fence - is a per-generation map to detect and ignore multiple touches for the same entry. It reduced cost to track frequency to once per generations 33 // and is relevant for heavy load scenario only. 34 NextGenerationCapacity(prevLen int, prevCap int) (pageSize int, useFence bool) 35 // InitGenerationCapacity is an analogue of NextGenerationCapacity but when a first page is created. 36 InitGenerationCapacity() (pageSize int, useFence bool) 37 38 // CanAdvanceGeneration is intended to provide custom logic to switch to a new generation page. 39 // This logic can consider a number of hits and age of a current generation. 40 // This function is called on a first update being added to a new generation page, and will be called again when provided limits are exhausted. 41 // When (createGeneration) is (true) - a new generation page will be created and the given limits (hitCount) and (ageLimit) will be applied. 42 // When (createGeneration) is (false) - the given limits (hitCount) and (ageLimit) will be applied to the current generation page. 43 // NB! A new generation page will always be created when capacity is exhausted. See NextGenerationCapacity. 44 CanAdvanceGeneration(curLen int, curCap int, hitRemains uint64, start, end Age) (createGeneration bool, hitLimit uint64, ageLimit Age) 45 46 // InitialAdvanceLimits is an analogue of CanAdvanceGeneration applied when a generation is created. 47 // Age given as (start) is the age of the first record to be added. 48 InitialAdvanceLimits(curCap int, start Age) (hitLimit uint64, ageLimit Age) 49 50 // CanTrimGenerations should return a number of LFU generation pages to be trimmed. This trim does NOT free cache entries, but compacts 51 // generation pages by converting LFU into LRU entries. 52 // It receives a total number of entries, a total number of LFU generation pages, and ages of the recent generation, 53 // of the least-frequent generation (rarest) and of the oldest LRU generation. 54 // Zero or negative result will skip trimming. 55 CanTrimGenerations(totalCount, freqGenCount int, recent, rarest, oldest Age) int 56 57 // CanTrimEntries should return a number entries to be cleaned up from the cache. 58 // It receives a total number of entries, and ages of the recent and of the the oldest LRU generation. 59 // Zero or negative result will skip trimming. 60 // Trimming, initiated by positive result of CanTrimEntries may prevent CanTrimGenerations to be called. 61 CanTrimEntries(totalCount int, recent, oldest Age) int 62 } 63 64 type TrimFunc = func(trimmed []uint32) // these values are []Index 65 66 // NewCore creates a new core instance for the given Strategy and provides behavior that combines LFU and LRU strategies: 67 // * recent and the most frequently used entries are handled by LRU strategy. Accuracy of LRU logic depends on number*size of generation pages. 68 // * other entries are handled by LRU strategy. 69 // Cache implementation must provide a trim callback. 70 // The trim call back can be called during Add, Touch and Update operations. 71 // Provided instance can be copied, but only one copy can be used. 72 // Core uses entry pages to track entries (all pages of the same size) and generation pages (size can vary). 73 // Every cached entry gets a unique index, that can be reused after deletion / expiration of entries. 74 func NewCore(s Strategy, trimFn TrimFunc) Core { 75 switch { 76 case s == nil: 77 panic(throw.IllegalValue()) 78 case trimFn == nil: 79 panic(throw.IllegalValue()) 80 } 81 82 return Core{ 83 alloc: newAllocationTracker(s.AllocationPageSize()), 84 strat: s, 85 trimFn: trimFn, 86 trimEach: s.TrimOnEachAddition(), 87 } 88 } 89 90 // Core provides all data management functions for cache implementations. 91 // This implementation is focused to minimize a number of links and memory overheads per entry. 92 // Behavior is regulated by provided Strategy. 93 // Core functions are thread-unsafe and must be protected by cache's implementation. 94 type Core struct { 95 alloc allocationTracker 96 strat Strategy 97 trimFn TrimFunc 98 trimEach bool 99 100 hitLimit uint64 101 ageLimit Age 102 103 recent *generation 104 rarest *generation 105 oldest *generation 106 } 107 108 func (p *Core) Allocated() int { 109 return p.alloc.AllocatedCount() 110 } 111 112 func (p *Core) Occupied() int { 113 return p.alloc.Count() 114 } 115 116 func (p *Core) Add() (Index, GenNo) { 117 n := p.alloc.Add(2) // +1 is for tracking oldest 118 return n, p.addToGen(n, true, false) 119 } 120 121 // Delete can ONLY be called once per index, otherwise counting will be broken 122 func (p *Core) Delete(idx Index) { 123 // this will remove +1 for oldest tracking 124 // so the entry will be removed at the "rarest" generation 125 p.alloc.Dec(idx) 126 } 127 128 func (p *Core) Touch(index Index) GenNo { 129 _, overflow, ok := p.alloc.Inc(index) 130 if !ok { 131 return -1 132 } 133 return p.addToGen(index, false, overflow) 134 } 135 136 func (p *Core) addToGen(index Index, addedEntry, overflow bool) GenNo { 137 age := p.strat.CurrentAge() 138 if p.recent == nil { 139 p.recent = &generation{ 140 start: age, 141 end: age, 142 } 143 p.recent.init(p.strat.InitGenerationCapacity()) 144 145 p.rarest = p.recent 146 p.oldest = p.recent 147 genNo, _ := p.recent.Add(index, age) // first entry will always be added 148 return genNo 149 } 150 151 p.useOrAdvanceGen(age, addedEntry) 152 153 genNo, addedEvent := p.recent.Add(index, age) 154 switch { 155 case addedEvent: 156 case addedEntry: 157 panic(throw.Impossible()) 158 case !overflow: 159 _, _ = p.alloc.Dec(index) 160 } 161 return genNo 162 } 163 164 func (p *Core) useOrAdvanceGen(age Age, added bool) { 165 trimmedEntries, trimmedGens := false, false 166 167 if p.recent == nil || p.rarest == nil || p.oldest == nil { 168 panic(throw.Impossible()) 169 } 170 171 if p.trimEach && added { 172 trimCount := p.strat.CanTrimEntries(p.alloc.Count(), // is already allocated 173 p.recent.end, p.oldest.start) 174 175 trimmedEntries = trimCount > 0 176 trimmedGens = p.trimEntriesAndGenerations(trimCount) 177 } 178 179 if p.hitLimit > 0 { 180 p.hitLimit-- 181 } 182 curLen, curCap := len(p.recent.access), cap(p.recent.access) 183 184 switch { 185 case curLen == curCap: 186 case p.hitLimit == 0 || p.ageLimit <= age: 187 createGen := false 188 createGen, p.hitLimit, p.ageLimit = p.strat.CanAdvanceGeneration(curLen, curCap, p.hitLimit, p.recent.start, p.recent.end) 189 if createGen { 190 break 191 } 192 fallthrough 193 default: 194 return 195 } 196 197 newGen := &generation{ 198 genNo: p.recent.genNo + 1, 199 start: age, 200 end: age, 201 } 202 newGen.init(p.strat.NextGenerationCapacity(curLen, curCap)) 203 204 p.recent.next = newGen 205 p.recent = newGen 206 207 p.hitLimit, p.ageLimit = p.strat.InitialAdvanceLimits(cap(newGen.access), age) 208 209 if !trimmedEntries { 210 trimCount := p.strat.CanTrimEntries(p.alloc.Count(), p.recent.end, p.oldest.start) 211 trimmedGens = p.trimEntriesAndGenerations(trimCount) 212 } 213 214 if !trimmedGens { 215 trimGenCount := p.strat.CanTrimGenerations(p.alloc.Count(), p.recent.genNo-p.rarest.genNo+1, 216 p.recent.end, p.rarest.start, p.oldest.start) 217 p.trimGenerations(trimGenCount) 218 } 219 } 220 221 func (p *Core) trimEntriesAndGenerations(count int) bool { 222 for ; count > 0 && p.oldest != p.rarest; p.oldest = p.oldest.next { 223 count = p.oldest.trimOldest(p, count) 224 } 225 226 if count <= 0 { 227 return false 228 } 229 230 for ; count > 0 && p.recent != p.rarest; p.rarest = p.rarest.next { 231 count = p.rarest.trimRarest(p, count) 232 if len(p.rarest.access) != 0 { 233 break 234 } 235 p.oldest = p.rarest 236 } 237 238 if count <= 0 { 239 return true 240 } 241 242 count = p.recent.trimRarest(p, count) 243 244 if count > 0 && p.alloc.Count() > 0 { 245 panic(throw.Impossible()) 246 } 247 248 return true 249 } 250 251 func (p *Core) trimGenerations(count int) { 252 prev := p.oldest 253 if prev == p.rarest || prev.next != p.rarest { 254 prev = nil 255 } 256 257 for ; count > 0 && p.recent != p.rarest; count-- { 258 p.rarest.trimGeneration(p, prev) 259 260 if len(p.rarest.access) == 0 { 261 p.rarest = p.rarest.next 262 if prev != nil { 263 prev.next = p.rarest 264 } 265 } else { 266 prev = p.rarest 267 p.rarest = p.rarest.next 268 } 269 } 270 } 271 272 /*******************************************/ 273 274 type generation struct { 275 access []uint32 // Index 276 fence map[uint32]struct{} 277 start Age 278 end Age 279 next *generation 280 genNo GenNo 281 } 282 283 func (p *generation) init(capacity int, loadFence bool) { 284 p.access = make([]uint32, 0, capacity) 285 if loadFence { 286 p.fence = make(map[uint32]struct{}, capacity) 287 } 288 } 289 290 func (p *generation) Add(index Index, age Age) (GenNo, bool) { 291 idx := uint32(index) 292 n := len(p.access) 293 294 switch { 295 case n == 0: 296 p.access = append(p.access, idx) 297 if p.fence != nil { 298 p.fence[idx] = struct{}{} 299 } 300 return p.genNo, true 301 case p.end < age: 302 p.end = age 303 } 304 305 switch { 306 case p.fence != nil: 307 if _, ok := p.fence[idx]; !ok { 308 p.fence[idx] = struct{}{} 309 p.access = append(p.access, idx) 310 return p.genNo, true 311 } 312 case p.access[n-1] != idx: 313 p.access = append(p.access, idx) 314 return p.genNo, true 315 } 316 317 return p.genNo, false 318 } 319 320 func (p *generation) trimOldest(c *Core, count int) int { 321 indices := p.access 322 i, j := 0, 0 323 for _, idx := range p.access { 324 i++ 325 switch v, ok := c.alloc.Dec(Index(idx)); { 326 case !ok: 327 // was removed 328 continue 329 case v > 0: 330 // it seems to be revived 331 // so it will be back in a while 332 // restore the counter 333 c.alloc.Inc(Index(idx)) 334 continue 335 } 336 c.alloc.Delete(Index(idx)) 337 338 indices[j] = idx 339 j++ 340 341 count-- 342 if count == 0 { 343 break 344 } 345 } 346 347 p.access = p.access[i:] 348 if j > 0 { 349 c.trimFn(indices[:j:j]) 350 } 351 352 return count 353 } 354 355 func (p *generation) trimRarest(c *Core, count int) int { 356 indices := p.access 357 i, j := 0, 0 358 for _, idx := range p.access { 359 i++ 360 switch v, ok := c.alloc.Dec(Index(idx)); { 361 case !ok: 362 // was removed 363 continue 364 case v > 1: 365 // it has other events 366 // so it will be back in a while 367 // restore the counter 368 c.alloc.Inc(Index(idx)) 369 continue 370 } 371 c.alloc.Delete(Index(idx)) 372 373 indices[j] = idx 374 j++ 375 count-- 376 if count == 0 { 377 break 378 } 379 } 380 381 p.access = p.access[i:] 382 if j > 0 { 383 c.trimFn(indices[:j:j]) 384 } 385 386 return count 387 } 388 389 func (p *generation) trimGeneration(c *Core, prev *generation) { 390 indices := p.access 391 j := 0 392 for _, idx := range p.access { 393 switch v, ok := c.alloc.Dec(Index(idx)); { 394 case !ok: 395 // was removed 396 continue 397 case v > 1: 398 // not yet rare 399 continue 400 case v == 0: 401 // it was explicitly removed 402 // so do not pass it further 403 c.alloc.Delete(Index(idx)) 404 continue 405 } 406 // v == 1 - leave for FIFO removal 407 indices[j] = idx 408 j++ 409 } 410 411 if j == 0 { 412 p.access = nil 413 return 414 } 415 p.access = p.access[:j] 416 if prev == nil { 417 return 418 } 419 420 switch prevN := len(prev.access); { 421 case prevN == 0: 422 return 423 case prevN+j <= cap(p.access): 424 prev.access = append(p.access, prev.access...) 425 case prevN+j <= cap(prev.access): 426 combined := prev.access[:prevN+j] // expand within capacity 427 copy(combined[j:], prev.access) // move older records to the end 428 copy(combined, p.access[:j]) // put newer records to the front 429 } 430 431 prev.start = p.start 432 p.access = nil // enable this generation to be deleted 433 }