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  }