├── go.mod ├── traces ├── report.png ├── dl-address.sh ├── dl-wikipedia.sh ├── dl-storage.sh ├── combine.sh ├── combine-png.sh ├── dl-cache2k.sh ├── dl-youtube.sh ├── report.sh ├── zipf.go ├── visualize-request.sh ├── visualize-size.sh ├── cache2k.go ├── address_test.go ├── wikipedia_test.go ├── youtube_test.go ├── youtube.go ├── wikipedia.go ├── report_test.go ├── storage.go ├── address.go ├── README.md ├── zipf_test.go ├── storage_test.go ├── report.go ├── files.go └── cache2k_test.go ├── synthetic ├── synthetic.go ├── counter.go ├── exponential.go ├── uniform.go ├── zipf.go └── hotspot.go ├── .github └── workflows │ └── go.yml ├── policy_test.go ├── filter_test.go ├── sketch_test.go ├── example └── main.go ├── stats_test.go ├── LICENSE ├── README.md ├── hash_test.go ├── filter.go ├── benchmark_test.go ├── sketch.go ├── tinylfu.go ├── hash.go ├── cache.go ├── tinylfu_test.go ├── stats.go ├── lru.go ├── policy.go ├── lru_test.go ├── local_test.go └── local.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Comcast/goburrow-cache 2 | 3 | go 1.23 4 | -------------------------------------------------------------------------------- /traces/report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comcast/goburrow-cache/master/traces/report.png -------------------------------------------------------------------------------- /traces/dl-address.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | FILE="proj1-traces.tar.gz" 3 | curl -O "http://cseweb.ucsd.edu/classes/fa07/cse240a/$FILE" 4 | tar xvzf "$FILE" 5 | -------------------------------------------------------------------------------- /synthetic/synthetic.go: -------------------------------------------------------------------------------- 1 | package synthetic 2 | 3 | // Generator is a pseudo-random numbers generator. 4 | type Generator interface { 5 | Int() int 6 | } 7 | -------------------------------------------------------------------------------- /traces/dl-wikipedia.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | FILES="wiki.1191201596.gz" 4 | for F in $FILES; do 5 | if [ ! -f "$F" ]; then 6 | curl -O "http://www.wikibench.eu/wiki/2007-10/$F" 7 | fi 8 | done 9 | -------------------------------------------------------------------------------- /traces/dl-storage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | FILES="WebSearch1.spc.bz2 Financial2.spc.bz2" 4 | for F in $FILES; do 5 | if [ ! -f "$F" ]; then 6 | curl -O "http://skuld.cs.umass.edu/traces/storage/$F" 7 | fi 8 | done 9 | -------------------------------------------------------------------------------- /traces/combine.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | if [ -z "$FORMAT" ]; then 5 | FORMAT="png" 6 | else 7 | FORMAT="${FORMAT%% *}" 8 | fi 9 | 10 | FILES=$(ls *-requests.$FORMAT *-cachesize.$FORMAT | sort) 11 | gm montage -mode concatenate -tile 4x $FILES "report.$FORMAT" 12 | -------------------------------------------------------------------------------- /traces/combine-png.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | NAMES="address wikipedia youtube zipf" 4 | FORMAT="png" 5 | FILES="" 6 | for N in $NAMES; do 7 | FILES="$FILES $N-requests.$FORMAT $N-cachesize.$FORMAT" 8 | done 9 | gm montage -mode concatenate -tile 4x $FILES "report.$FORMAT" 10 | -------------------------------------------------------------------------------- /traces/dl-cache2k.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | FILES="trace-cpp.trc.bin.gz trace-glimpse.trc.bin.gz trace-mt-db-20160419-busy.trc.bin.bz2 trace-multi2.trc.bin.gz trace-oltp.trc.bin.gz trace-sprite.trc.bin.gz" 5 | for F in $FILES; do 6 | if [ ! -f "$F" ]; then 7 | curl -L -O "https://github.com/cache2k/cache2k-benchmark/raw/master/traces/src/main/resources/org/cache2k/benchmark/traces/$F" 8 | fi 9 | done 10 | -------------------------------------------------------------------------------- /synthetic/counter.go: -------------------------------------------------------------------------------- 1 | package synthetic 2 | 3 | import ( 4 | "sync/atomic" 5 | ) 6 | 7 | type counterGenerator struct { 8 | i int64 9 | } 10 | 11 | func (g *counterGenerator) Int() int { 12 | return int(atomic.AddInt64(&g.i, 1)) 13 | } 14 | 15 | // Counter returns a Generator giving a sequence of unique integers. 16 | func Counter(start int) Generator { 17 | return &counterGenerator{ 18 | i: int64(start), 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /traces/dl-youtube.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | FILE="youtube_traces.tgz" 4 | if [ ! -f "$FILE" ]; then 5 | curl -O "http://skuld.cs.umass.edu/traces/network/$FILE" 6 | fi 7 | tar xzf "$FILE" 8 | 9 | rm youtube.parsed.*.24.dat 10 | rm youtube.parsed.*.S1.dat 11 | 12 | for FILE in youtube.parsed.*.dat; do 13 | # YYMMDD 14 | NAME="$(echo "$FILE" | sed -e 's/\([0-9]\{2\}\)\([0-9]\{2\}\)\([0-9]\{2\}\)\(\.dat\)/\3\1\2\4/')" 15 | mv "$FILE" "$NAME" 16 | done 17 | -------------------------------------------------------------------------------- /synthetic/exponential.go: -------------------------------------------------------------------------------- 1 | package synthetic 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | type exponentialGenerator struct { 9 | r *rand.Rand 10 | mean float64 11 | } 12 | 13 | func (g *exponentialGenerator) Int() int { 14 | return int(g.r.ExpFloat64() * g.mean) 15 | } 16 | 17 | // Exponential returns a Generator resembling an exponential distribution. 18 | func Exponential(mean float64) Generator { 19 | return &exponentialGenerator{ 20 | r: rand.New(rand.NewSource(time.Now().UnixNano())), 21 | mean: mean, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | steps: 14 | 15 | - name: Set up Go 1.x 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: ^1.13 19 | id: go 20 | 21 | - name: Check out 22 | uses: actions/checkout@v2 23 | 24 | - name: Build 25 | run: go build -v 26 | 27 | - name: Test 28 | run: | 29 | go test -v -bench . -race 30 | GOARCH=386 go test -v 31 | -------------------------------------------------------------------------------- /traces/report.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | report() { 5 | NAME="$1" 6 | go test -v -run "$NAME" 7 | 8 | NAME=$(echo "$NAME" | tr '[:upper:]' '[:lower:]') 9 | ./visualize-request.sh request_$NAME-*.txt 10 | for OUTPUT in out.*; do 11 | mv -v "$OUTPUT" "$NAME-requests.${OUTPUT#*.}" 12 | done 13 | ./visualize-size.sh size_$NAME-*.txt 14 | for OUTPUT in out.*; do 15 | mv -v "$OUTPUT" "$NAME-cachesize.${OUTPUT#*.}" 16 | done 17 | } 18 | 19 | TRACES="Address CPP Multi2 ORMBusy Glimpse OLTP Sprite Financial WebSearch Wikipedia YouTube Zipf" 20 | for TRACE in $TRACES; do 21 | report $TRACE 22 | done 23 | -------------------------------------------------------------------------------- /policy_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "sync/atomic" 5 | "testing" 6 | ) 7 | 8 | func BenchmarkCacheSegment(b *testing.B) { 9 | c := cache{} 10 | const count = 1 << 10 11 | entries := make([]*entry, count) 12 | for i := range entries { 13 | entries[i] = newEntry(i, i, uint64(i)) 14 | } 15 | var n int32 16 | b.ReportAllocs() 17 | b.ResetTimer() 18 | b.RunParallel(func(pb *testing.PB) { 19 | for pb.Next() { 20 | i := atomic.AddInt32(&n, 1) 21 | c.getOrSet(entries[i&(count-1)]) 22 | if i > 0 && i&0xf == 0 { 23 | c.delete(entries[(i-1)&(count-1)]) 24 | } 25 | } 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /synthetic/uniform.go: -------------------------------------------------------------------------------- 1 | package synthetic 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | type uniformGenerator struct { 9 | r *rand.Rand 10 | n int 11 | min int 12 | } 13 | 14 | func (g *uniformGenerator) Int() int { 15 | return g.min + g.r.Intn(g.n) 16 | } 17 | 18 | // Uniform returns a Generator resembling a uniform distribution. 19 | func Uniform(min, max int) Generator { 20 | if max <= min { 21 | panic("synthetic: invalid uniform range") 22 | } 23 | return &uniformGenerator{ 24 | r: rand.New(rand.NewSource(time.Now().UnixNano())), 25 | min: min, 26 | n: max - min, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /synthetic/zipf.go: -------------------------------------------------------------------------------- 1 | package synthetic 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | type zipfGenerator struct { 9 | r *rand.Zipf 10 | min int 11 | } 12 | 13 | func (g *zipfGenerator) Int() int { 14 | return g.min + int(g.r.Uint64()) 15 | } 16 | 17 | // Zipf returns a Generator resembling Zipf distribution. 18 | func Zipf(min, max int, exp float64) Generator { 19 | if max <= min { 20 | panic("synthetic: invalid zipf range") 21 | } 22 | if exp <= 1.0 { 23 | panic("synthetic: invalid zipf exponent") 24 | } 25 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 26 | return &zipfGenerator{ 27 | r: rand.NewZipf(r, exp, 1.0, uint64(max-min)), 28 | min: min, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /traces/zipf.go: -------------------------------------------------------------------------------- 1 | package traces 2 | 3 | import ( 4 | "context" 5 | "math/rand" 6 | ) 7 | 8 | type zipfProvider struct { 9 | r *rand.Zipf 10 | n int 11 | } 12 | 13 | func NewZipfProvider(s float64, num int) Provider { 14 | if s <= 1.0 || num <= 0 { 15 | panic("invalid zipf parameters") 16 | } 17 | r := rand.New(rand.NewSource(1)) 18 | return &zipfProvider{ 19 | r: rand.NewZipf(r, s, 1.0, 1<<16-1), 20 | n: num, 21 | } 22 | } 23 | 24 | func (p *zipfProvider) Provide(ctx context.Context, keys chan<- interface{}) { 25 | defer close(keys) 26 | for i := 0; i < p.n; i++ { 27 | v := p.r.Uint64() 28 | select { 29 | case <-ctx.Done(): 30 | return 31 | case keys <- v: 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /traces/visualize-request.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ -z "$FORMAT" ]; then 3 | #FORMAT='svg size 400,300 font "Helvetica,10"' 4 | FORMAT='png size 220,180 small noenhanced' 5 | fi 6 | OUTPUT="out.${FORMAT%% *}" 7 | PLOTARG="" 8 | 9 | for f in "$@"; do 10 | if [ ! -z "$PLOTARG" ]; then 11 | PLOTARG="$PLOTARG," 12 | fi 13 | NAME="$(basename "$f")" 14 | NAME="${NAME%.*}" 15 | NAME="${NAME#*_}" 16 | PLOTARG="$PLOTARG '$f' every ::1 using 1:3 with lines title '$NAME'" 17 | done 18 | 19 | ARG="set datafile separator ',';\ 20 | set xlabel 'Requests';\ 21 | set xtics rotate by 45 right;\ 22 | set ylabel 'Hit Rate' offset 2;\ 23 | set yrange [0:];\ 24 | set key bottom right;\ 25 | set colors classic;\ 26 | set terminal $FORMAT;\ 27 | set output '$OUTPUT';\ 28 | plot $PLOTARG" 29 | 30 | gnuplot -e "$ARG" 31 | -------------------------------------------------------------------------------- /traces/visualize-size.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ -z "$FORMAT" ]; then 3 | #FORMAT='svg size 400,300 font "Helvetica,10"' 4 | FORMAT='png size 220,180 small noenhanced' 5 | fi 6 | OUTPUT="out.${FORMAT%% *}" 7 | PLOTARG="" 8 | 9 | for f in "$@"; do 10 | if [ ! -z "$PLOTARG" ]; then 11 | PLOTARG="$PLOTARG," 12 | fi 13 | NAME="$(basename "$f")" 14 | NAME="${NAME%.*}" 15 | NAME="${NAME#*_}" 16 | PLOTARG="$PLOTARG '$f' every ::1 using 5:3:xtic(5) with lines title '$NAME'" 17 | done 18 | 19 | ARG="set datafile separator ',';\ 20 | set xlabel 'Cache Size';\ 21 | set xtics rotate by 45 right;\ 22 | set ylabel 'Hit Rate' offset 2;\ 23 | set yrange [0:];\ 24 | set key bottom right;\ 25 | set colors classic;\ 26 | set terminal $FORMAT;\ 27 | set output '$OUTPUT';\ 28 | plot $PLOTARG" 29 | 30 | gnuplot -e "$ARG" 31 | -------------------------------------------------------------------------------- /traces/cache2k.go: -------------------------------------------------------------------------------- 1 | package traces 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "encoding/binary" 7 | "io" 8 | ) 9 | 10 | type cache2kProvider struct { 11 | r *bufio.Reader 12 | } 13 | 14 | // NewCache2kProvider returns a Provider which items are from traces 15 | // in Cache2k repository (https://github.com/cache2k/cache2k-benchmark). 16 | func NewCache2kProvider(r io.Reader) Provider { 17 | return &cache2kProvider{ 18 | r: bufio.NewReader(r), 19 | } 20 | } 21 | 22 | func (p *cache2kProvider) Provide(ctx context.Context, keys chan<- interface{}) { 23 | defer close(keys) 24 | 25 | v := make([]byte, 4) 26 | for { 27 | _, err := p.r.Read(v) 28 | if err != nil { 29 | return 30 | } 31 | k := binary.LittleEndian.Uint32(v) 32 | select { 33 | case <-ctx.Done(): 34 | return 35 | case keys <- k: 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /traces/address_test.go: -------------------------------------------------------------------------------- 1 | package traces 2 | 3 | import "testing" 4 | 5 | func TestRequestAddress(t *testing.T) { 6 | for _, p := range policies { 7 | p := p 8 | t.Run(p, func(t *testing.T) { 9 | t.Parallel() 10 | opt := options{ 11 | policy: p, 12 | cacheSize: 128, 13 | reportInterval: 5000, 14 | maxItems: 500000, 15 | } 16 | testRequest(t, NewAddressProvider, opt, 17 | "traces/gcc.trace", "request_address-"+p+".txt") 18 | }) 19 | } 20 | } 21 | 22 | func TestSizeAddress(t *testing.T) { 23 | for _, p := range policies { 24 | p := p 25 | t.Run(p, func(t *testing.T) { 26 | t.Parallel() 27 | opt := options{ 28 | policy: p, 29 | cacheSize: 25, 30 | maxItems: 100000, 31 | } 32 | testSize(t, NewAddressProvider, opt, 33 | "traces/gcc.trace", "size_address-"+p+".txt") 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /traces/wikipedia_test.go: -------------------------------------------------------------------------------- 1 | package traces 2 | 3 | import "testing" 4 | 5 | func TestRequestWikipedia(t *testing.T) { 6 | for _, p := range policies { 7 | p := p 8 | t.Run(p, func(t *testing.T) { 9 | t.Parallel() 10 | opt := options{ 11 | policy: p, 12 | cacheSize: 512, 13 | reportInterval: 10000, 14 | maxItems: 1000000, 15 | } 16 | testRequest(t, NewWikipediaProvider, opt, 17 | "wiki.*.gz", "request_wikipedia-"+p+".txt") 18 | }) 19 | } 20 | } 21 | 22 | func TestSizeWikipedia(t *testing.T) { 23 | for _, p := range policies { 24 | p := p 25 | t.Run(p, func(t *testing.T) { 26 | t.Parallel() 27 | opt := options{ 28 | policy: p, 29 | cacheSize: 250, 30 | maxItems: 100000, 31 | } 32 | testSize(t, NewWikipediaProvider, opt, 33 | "wiki.*.gz", "size_wikipedia-"+p+".txt") 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /filter_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "testing" 4 | 5 | func TestBloomFilter(t *testing.T) { 6 | const numIns = 100000 7 | f := bloomFilter{} 8 | f.init(numIns, 0.01) 9 | 10 | var i uint64 11 | for i = 0; i < numIns; i += 2 { 12 | existed := f.put(i) 13 | if existed { 14 | t.Fatalf("unexpected put(%d): %v, want: false", i, existed) 15 | } 16 | } 17 | for i = 0; i < numIns; i += 2 { 18 | existed := f.contains(i) 19 | if !existed { 20 | t.Fatalf("unexpected contains(%d): %v, want: true", i, existed) 21 | } 22 | } 23 | for i = 1; i < numIns; i += 2 { 24 | existed := f.contains(i) 25 | if existed { 26 | t.Fatalf("unexpected contains(%d): %v, want: false", i, existed) 27 | } 28 | } 29 | for i = 0; i < numIns; i += 2 { 30 | existed := f.put(i) 31 | if !existed { 32 | t.Fatalf("unexpected put(%d): %v, want: true", i, existed) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /traces/youtube_test.go: -------------------------------------------------------------------------------- 1 | package traces 2 | 3 | import "testing" 4 | 5 | func TestRequestYouTube(t *testing.T) { 6 | for _, p := range policies { 7 | p := p 8 | t.Run(p, func(t *testing.T) { 9 | t.Parallel() 10 | opt := options{ 11 | policy: p, 12 | cacheSize: 512, 13 | reportInterval: 2000, 14 | maxItems: 200000, 15 | } 16 | testRequest(t, NewYoutubeProvider, opt, 17 | "youtube.parsed.0803*.dat", "request_youtube-"+p+".txt") 18 | }) 19 | } 20 | } 21 | 22 | func TestSizeYouTube(t *testing.T) { 23 | for _, p := range policies { 24 | p := p 25 | t.Run(p, func(t *testing.T) { 26 | t.Parallel() 27 | opt := options{ 28 | policy: p, 29 | cacheSize: 250, 30 | maxItems: 100000, 31 | } 32 | testSize(t, NewYoutubeProvider, opt, 33 | "youtube.parsed.0803*.dat", "size_youtube-"+p+".txt") 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /traces/youtube.go: -------------------------------------------------------------------------------- 1 | package traces 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "io" 8 | ) 9 | 10 | type youtubeProvider struct { 11 | r *bufio.Reader 12 | } 13 | 14 | func NewYoutubeProvider(r io.Reader) Provider { 15 | return &youtubeProvider{ 16 | r: bufio.NewReader(r), 17 | } 18 | } 19 | 20 | func (p *youtubeProvider) Provide(ctx context.Context, keys chan<- interface{}) { 21 | defer close(keys) 22 | for { 23 | b, err := p.r.ReadBytes('\n') 24 | if err != nil { 25 | return 26 | } 27 | v := p.parse(b) 28 | if v != "" { 29 | select { 30 | case <-ctx.Done(): 31 | return 32 | case keys <- v: 33 | } 34 | } 35 | } 36 | } 37 | 38 | func (p *youtubeProvider) parse(b []byte) string { 39 | // Get video id 40 | idx := bytes.Index(b, []byte("GETVIDEO ")) 41 | if idx < 0 { 42 | return "" 43 | } 44 | b = b[idx+len("GETVIDEO "):] 45 | idx = bytes.IndexAny(b, "& ") 46 | if idx > 0 { 47 | b = b[:idx] 48 | } 49 | return string(b) 50 | } 51 | -------------------------------------------------------------------------------- /traces/wikipedia.go: -------------------------------------------------------------------------------- 1 | package traces 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "io" 8 | ) 9 | 10 | type wikipediaProvider struct { 11 | r *bufio.Reader 12 | } 13 | 14 | func NewWikipediaProvider(r io.Reader) Provider { 15 | return &wikipediaProvider{ 16 | r: bufio.NewReader(r), 17 | } 18 | } 19 | 20 | func (p *wikipediaProvider) Provide(ctx context.Context, keys chan<- interface{}) { 21 | defer close(keys) 22 | for { 23 | b, err := p.r.ReadBytes('\n') 24 | if err != nil { 25 | return 26 | } 27 | v := p.parse(b) 28 | if v != "" { 29 | select { 30 | case <-ctx.Done(): 31 | return 32 | case keys <- v: 33 | } 34 | } 35 | } 36 | } 37 | 38 | func (p *wikipediaProvider) parse(b []byte) string { 39 | // Get url 40 | idx := bytes.Index(b, []byte("http://")) 41 | if idx < 0 { 42 | return "" 43 | } 44 | b = b[idx+len("http://"):] 45 | // Get path 46 | idx = bytes.IndexByte(b, '/') 47 | if idx > 0 { 48 | b = b[idx:] 49 | } 50 | // Skip params 51 | idx = bytes.IndexAny(b, "? ") 52 | if idx > 0 { 53 | b = b[:idx] 54 | } 55 | return string(b) 56 | } 57 | -------------------------------------------------------------------------------- /traces/report_test.go: -------------------------------------------------------------------------------- 1 | package traces 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func testRequest(t *testing.T, newProvider func(io.Reader) Provider, opt options, traceFiles string, reportFile string) { 10 | r, err := openFilesGlob(traceFiles) 11 | if err != nil { 12 | t.Skip(err) 13 | } 14 | defer r.Close() 15 | provider := newProvider(r) 16 | 17 | w, err := os.Create(reportFile) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | defer w.Close() 22 | reporter := NewReporter(w) 23 | benchmarkCache(provider, reporter, opt) 24 | } 25 | 26 | func testSize(t *testing.T, newProvider func(io.Reader) Provider, opt options, traceFiles, reportFile string) { 27 | r, err := openFilesGlob(traceFiles) 28 | if err != nil { 29 | t.Skip(err) 30 | } 31 | defer r.Close() 32 | w, err := os.Create(reportFile) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | defer w.Close() 37 | reporter := NewReporter(w) 38 | for i := 0; i < 5; i++ { 39 | provider := newProvider(r) 40 | benchmarkCache(provider, reporter, opt) 41 | err = r.Reset() 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | opt.cacheSize += opt.cacheSize 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /synthetic/hotspot.go: -------------------------------------------------------------------------------- 1 | package synthetic 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | type hotspotGenerator struct { 9 | r *rand.Rand 10 | min int 11 | 12 | hotFrac float64 13 | hotN int 14 | coldN int 15 | } 16 | 17 | func (g *hotspotGenerator) Int() int { 18 | v := g.min 19 | r := g.r.Float64() 20 | if r > g.hotFrac { 21 | // Hotset 22 | v += g.r.Intn(g.hotN) 23 | } else { 24 | // Coldset 25 | v += g.hotN + g.r.Intn(g.coldN) 26 | } 27 | return v 28 | } 29 | 30 | // Hotspot returns a Generator resembling a hotspot distribution. 31 | // hotFrac is the fraction of total items which have a proportion (1.0-hotFrac). 32 | func Hotspot(min, max int, hotFrac float64) Generator { 33 | if max <= min { 34 | panic("synthetic: invalid hotspot range") 35 | } 36 | if hotFrac < 0.0 || hotFrac > 1.0 { 37 | panic("synthetic: invalid hotspot fraction") 38 | } 39 | n := max - min 40 | hotN := int(hotFrac * float64(n)) 41 | coldN := n - hotN 42 | 43 | return &hotspotGenerator{ 44 | r: rand.New(rand.NewSource(time.Now().UnixNano())), 45 | min: min, 46 | hotFrac: hotFrac, 47 | hotN: hotN, 48 | coldN: coldN, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /traces/storage.go: -------------------------------------------------------------------------------- 1 | package traces 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "io" 8 | "strconv" 9 | ) 10 | 11 | type storageProvider struct { 12 | r *bufio.Reader 13 | } 14 | 15 | // NewStorageProvider returns a Provider with items are from 16 | // Storage traces by the University of Massachusetts 17 | // (http://traces.cs.umass.edu/index.php/Storage/Storage). 18 | func NewStorageProvider(r io.Reader) Provider { 19 | return &storageProvider{ 20 | r: bufio.NewReader(r), 21 | } 22 | } 23 | 24 | func (p *storageProvider) Provide(ctx context.Context, keys chan<- interface{}) { 25 | defer close(keys) 26 | for { 27 | b, err := p.r.ReadBytes('\n') 28 | if err != nil { 29 | return 30 | } 31 | k := p.parse(b) 32 | if k > 0 { 33 | select { 34 | case <-ctx.Done(): 35 | return 36 | case keys <- k: 37 | } 38 | } 39 | } 40 | } 41 | 42 | func (p *storageProvider) parse(b []byte) uint64 { 43 | idx := bytes.IndexByte(b, ',') 44 | if idx < 0 { 45 | return 0 46 | } 47 | b = b[idx+1:] 48 | idx = bytes.IndexByte(b, ',') 49 | if idx < 0 { 50 | return 0 51 | } 52 | k, err := strconv.ParseUint(string(b[:idx]), 10, 64) 53 | if err != nil { 54 | return 0 55 | } 56 | return k 57 | } 58 | -------------------------------------------------------------------------------- /traces/address.go: -------------------------------------------------------------------------------- 1 | package traces 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "context" 7 | "io" 8 | "strconv" 9 | ) 10 | 11 | type addressProvider struct { 12 | r *bufio.Reader 13 | } 14 | 15 | // NewAddressProvider returns a Provider with items are from 16 | // application traces by the University of California, San Diego 17 | // (http://cseweb.ucsd.edu/classes/fa07/cse240a/project1.html). 18 | func NewAddressProvider(r io.Reader) Provider { 19 | return &addressProvider{ 20 | r: bufio.NewReader(r), 21 | } 22 | } 23 | 24 | func (p *addressProvider) Provide(ctx context.Context, keys chan<- interface{}) { 25 | defer close(keys) 26 | for { 27 | b, err := p.r.ReadBytes('\n') 28 | if err != nil { 29 | return 30 | } 31 | v := p.parse(b) 32 | if v > 0 { 33 | select { 34 | case <-ctx.Done(): 35 | return 36 | case keys <- v: 37 | } 38 | } 39 | } 40 | } 41 | 42 | func (p *addressProvider) parse(b []byte) uint64 { 43 | idx := bytes.IndexByte(b, ' ') 44 | if idx < 0 { 45 | return 0 46 | } 47 | b = b[idx+1:] 48 | idx = bytes.IndexByte(b, ' ') 49 | if idx < 0 { 50 | return 0 51 | } 52 | b = b[:idx] 53 | 54 | val, err := strconv.ParseUint(string(b), 0, 0) 55 | if err != nil { 56 | return 0 57 | } 58 | return val 59 | } 60 | -------------------------------------------------------------------------------- /sketch_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "testing" 4 | 5 | func TestCountMinSketch(t *testing.T) { 6 | const max = 15 7 | cm := &countMinSketch{} 8 | cm.init(max) 9 | for i := 0; i < max; i++ { 10 | // Increase value at i j times 11 | for j := i; j > 0; j-- { 12 | cm.add(uint64(i)) 13 | } 14 | } 15 | for i := 0; i < max; i++ { 16 | n := cm.estimate(uint64(i)) 17 | if int(n) != i { 18 | t.Fatalf("unexpected estimate(%d): %d, want: %d", i, n, i) 19 | } 20 | } 21 | cm.reset() 22 | for i := 0; i < max; i++ { 23 | n := cm.estimate(uint64(i)) 24 | if int(n) != i/2 { 25 | t.Fatalf("unexpected estimate(%d): %d, want: %d", i, n, i/2) 26 | } 27 | } 28 | cm.reset() 29 | for i := 0; i < max; i++ { 30 | n := cm.estimate(uint64(i)) 31 | if int(n) != i/4 { 32 | t.Fatalf("unexpected estimate(%d): %d, want: %d", i, n, i/4) 33 | } 34 | } 35 | for i := 0; i < 100; i++ { 36 | cm.add(1) 37 | } 38 | n := cm.estimate(1) 39 | if n != 15 { 40 | t.Fatalf("unexpected estimate(%d): %d, want: %d", 1, n, 15) 41 | } 42 | } 43 | 44 | func BenchmarkCountMinSketchReset(b *testing.B) { 45 | cm := &countMinSketch{} 46 | cm.init(1<<15 - 1) 47 | b.ResetTimer() 48 | b.ReportAllocs() 49 | for i := 0; i < b.N; i++ { 50 | cm.add(0xCAFECAFECAFECAFE) 51 | cm.reset() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "time" 7 | 8 | "github.com/Comcast/goburrow-cache" 9 | ) 10 | 11 | func main() { 12 | load := func(k cache.Key) (cache.Value, error) { 13 | fmt.Printf("loading %v\n", k) 14 | time.Sleep(500 * time.Millisecond) 15 | return fmt.Sprintf("%d-%d", k, time.Now().Unix()), nil 16 | } 17 | remove := func(k cache.Key, v cache.Value) { 18 | fmt.Printf("removed %v (%v)\n", k, v) 19 | } 20 | // Create a new cache 21 | c := cache.NewLoadingCache(load, 22 | cache.WithMaximumSize(1000), 23 | cache.WithExpireAfterAccess(30*time.Second), 24 | cache.WithRefreshAfterWrite(20*time.Second), 25 | cache.WithRemovalListener(remove), 26 | ) 27 | 28 | getTicker := time.Tick(2 * time.Second) 29 | reportTicker := time.Tick(30 * time.Second) 30 | for { 31 | select { 32 | case <-getTicker: 33 | k := rand.Intn(100) 34 | v, _ := c.Get(k) 35 | fmt.Printf("get %v: %v\n", k, v) 36 | case <-reportTicker: 37 | st := cache.Stats{} 38 | c.Stats(&st) 39 | fmt.Printf("total: %d, hits: %d (%.2f%%), misses: %d (%.2f%%), evictions: %d, load: %s (%s)\n", 40 | st.RequestCount(), st.HitCount, st.HitRate()*100.0, st.MissCount, st.MissRate()*100.0, 41 | st.EvictionCount, st.TotalLoadTime, st.AverageLoadPenalty()) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /traces/README.md: -------------------------------------------------------------------------------- 1 | # Cache performance report 2 | 3 | Run all tests 4 | ``` 5 | ./report.sh 6 | ``` 7 | 8 | Run individual test 9 | ``` 10 | go test -v -run Wikipedia 11 | ./visualize-request.sh request_wikipedia-*.txt 12 | ./visualize-size.sh size_wikipedia-*.txt 13 | open out.png 14 | ``` 15 | 16 | ## Traces 17 | 18 | Name | Source 19 | ------------ | ------ 20 | Address | [University of California, San Diego](http://cseweb.ucsd.edu/classes/fa07/cse240a/project1.html) 21 | CPP | Authors of the LIRS algorithm - retrieved from [Cache2k](https://github.com/cache2k/cache2k-benchmark) 22 | Glimpse | Authors of the LIRS algorithm - retrieved from [Cache2k](https://github.com/cache2k/cache2k-benchmark) 23 | Multi2 | Authors of the LIRS algorithm - retrieved from [Cache2k](https://github.com/cache2k/cache2k-benchmark) 24 | OLTP | Authors of the ARC algorithm - retrieved from [Cache2k](https://github.com/cache2k/cache2k-benchmark) 25 | ORMBusy | GmbH - retrieved from [Cache2k](https://github.com/cache2k/cache2k-benchmark) 26 | Sprite | Authors of the LIRS algorithm - retrieved from [Cache2k](https://github.com/cache2k/cache2k-benchmark) 27 | Wikipedia | [WikiBench](http://www.wikibench.eu/) 28 | YouTube | [University of Massachusetts](http://traces.cs.umass.edu/index.php/Network/Network) 29 | WebSearch | [University of Massachusetts](http://traces.cs.umass.edu/index.php/Storage/Storage) 30 | -------------------------------------------------------------------------------- /stats_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestStatsCounter(t *testing.T) { 9 | c := statsCounter{} 10 | c.RecordHits(3) 11 | c.RecordMisses(2) 12 | c.RecordLoadSuccess(2 * time.Second) 13 | c.RecordLoadError(1 * time.Second) 14 | c.RecordEviction() 15 | 16 | var st Stats 17 | c.Snapshot(&st) 18 | 19 | if st.HitCount != 3 { 20 | t.Fatalf("unexpected hit count: %v", st) 21 | } 22 | if st.MissCount != 2 { 23 | t.Fatalf("unexpected miss count: %v", st) 24 | } 25 | if st.LoadSuccessCount != 1 { 26 | t.Fatalf("unexpected success count: %v", st) 27 | } 28 | if st.LoadErrorCount != 1 { 29 | t.Fatalf("unexpected error count: %v", st) 30 | } 31 | if st.TotalLoadTime != 3*time.Second { 32 | t.Fatalf("unexpected load time: %v", st) 33 | } 34 | if st.EvictionCount != 1 { 35 | t.Fatalf("unexpected eviction count: %v", st) 36 | } 37 | 38 | if st.RequestCount() != 5 { 39 | t.Fatalf("unexpected request count: %v", st.RequestCount()) 40 | } 41 | if st.HitRate() != 0.6 { 42 | t.Fatalf("unexpected hit rate: %v", st.HitRate()) 43 | } 44 | if st.MissRate() != 0.4 { 45 | t.Fatalf("unexpected miss rate: %v", st.MissRate()) 46 | } 47 | if st.LoadErrorRate() != 0.5 { 48 | t.Fatalf("unexpected error rate: %v", st.LoadErrorRate()) 49 | } 50 | if st.AverageLoadPenalty() != (1500 * time.Millisecond) { 51 | t.Fatalf("unexpected load penalty: %v", st.AverageLoadPenalty()) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /traces/zipf_test.go: -------------------------------------------------------------------------------- 1 | package traces 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestRequestZipf(t *testing.T) { 9 | for _, p := range policies { 10 | p := p 11 | t.Run(p, func(t *testing.T) { 12 | t.Parallel() 13 | testRequestZipf(t, p, "request_zipf-"+p+".txt") 14 | }) 15 | } 16 | } 17 | 18 | func testRequestZipf(t *testing.T, policy, reportFile string) { 19 | opt := options{ 20 | policy: policy, 21 | cacheSize: 512, 22 | reportInterval: 1000, 23 | maxItems: 100000, 24 | } 25 | 26 | provider := NewZipfProvider(1.01, opt.maxItems) 27 | 28 | w, err := os.Create(reportFile) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | defer w.Close() 33 | reporter := NewReporter(w) 34 | benchmarkCache(provider, reporter, opt) 35 | } 36 | 37 | func TestSizeZipf(t *testing.T) { 38 | for _, p := range policies { 39 | p := p 40 | t.Run(p, func(t *testing.T) { 41 | t.Parallel() 42 | testSizeZipf(t, p, "size_zipf-"+p+".txt") 43 | }) 44 | } 45 | } 46 | 47 | func testSizeZipf(t *testing.T, policy, reportFile string) { 48 | opt := options{ 49 | cacheSize: 250, 50 | policy: policy, 51 | maxItems: 100000, 52 | } 53 | 54 | w, err := os.Create(reportFile) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | defer w.Close() 59 | reporter := NewReporter(w) 60 | for i := 0; i < 5; i++ { 61 | provider := NewZipfProvider(1.01, opt.maxItems) 62 | benchmarkCache(provider, reporter, opt) 63 | opt.cacheSize += opt.cacheSize 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Quoc-Viet Nguyen. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | 3. Neither the names of the copyright holders nor the names of its contributors 14 | may be used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE 21 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /traces/storage_test.go: -------------------------------------------------------------------------------- 1 | package traces 2 | 3 | import "testing" 4 | 5 | func TestRequestWebSearch(t *testing.T) { 6 | for _, p := range policies { 7 | p := p 8 | t.Run(p, func(t *testing.T) { 9 | t.Parallel() 10 | opt := options{ 11 | policy: p, 12 | cacheSize: 256000, 13 | reportInterval: 10000, 14 | maxItems: 1000000, 15 | } 16 | testRequest(t, NewStorageProvider, opt, 17 | "WebSearch*.spc.bz2", "request_websearch-"+p+".txt") 18 | }) 19 | } 20 | } 21 | 22 | func TestRequestFinancial(t *testing.T) { 23 | for _, p := range policies { 24 | p := p 25 | t.Run(p, func(t *testing.T) { 26 | t.Parallel() 27 | opt := options{ 28 | policy: p, 29 | cacheSize: 512, 30 | reportInterval: 30000, 31 | maxItems: 3000000, 32 | } 33 | testRequest(t, NewStorageProvider, opt, 34 | "Financial*.spc.bz2", "request_financial-"+p+".txt") 35 | }) 36 | } 37 | } 38 | 39 | func TestSizeWebSearch(t *testing.T) { 40 | for _, p := range policies { 41 | p := p 42 | opt := options{ 43 | policy: p, 44 | cacheSize: 25000, 45 | maxItems: 1000000, 46 | } 47 | t.Run(p, func(t *testing.T) { 48 | t.Parallel() 49 | testSize(t, NewStorageProvider, opt, 50 | "WebSearch*.spc.bz2", "size_websearch-"+p+".txt") 51 | }) 52 | } 53 | } 54 | 55 | func TestSizeFinancial(t *testing.T) { 56 | for _, p := range policies { 57 | p := p 58 | opt := options{ 59 | policy: p, 60 | cacheSize: 250, 61 | maxItems: 1000000, 62 | } 63 | t.Run(p, func(t *testing.T) { 64 | t.Parallel() 65 | testSize(t, NewStorageProvider, opt, 66 | "Financial*.spc.bz2", "size_financial-"+p+".txt") 67 | }) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mango Cache 2 | [![GoDoc](https://godoc.org/github.com/goburrow/cache?status.svg)](https://godoc.org/github.com/goburrow/cache) 3 | ![Go](https://github.com/goburrow/cache/workflows/Go/badge.svg) 4 | 5 | Partial implementations of [Guava Cache](https://github.com/google/guava) in Go. 6 | 7 | Supported cache replacement policies: 8 | 9 | - LRU 10 | - Segmented LRU (default) 11 | - TinyLFU (experimental) 12 | 13 | The TinyLFU implementation is inspired by 14 | [Caffeine](https://github.com/ben-manes/caffeine) by Ben Manes and 15 | [go-tinylfu](https://github.com/dgryski/go-tinylfu) by Damian Gryski. 16 | 17 | ## Download 18 | 19 | ``` 20 | go get -u github.com/goburrow/cache 21 | ``` 22 | 23 | ## Example 24 | 25 | ```go 26 | package main 27 | 28 | import ( 29 | "fmt" 30 | "math/rand" 31 | "time" 32 | 33 | "github.com/goburrow/cache" 34 | ) 35 | 36 | func main() { 37 | load := func(k cache.Key) (cache.Value, error) { 38 | time.Sleep(100 * time.Millisecond) // Slow task 39 | return fmt.Sprintf("%d", k), nil 40 | } 41 | // Create a loading cache 42 | c := cache.NewLoadingCache(load, 43 | cache.WithMaximumSize(100), // Limit number of entries in the cache. 44 | cache.WithExpireAfterAccess(1*time.Minute), // Expire entries after 1 minute since last accessed. 45 | cache.WithRefreshAfterWrite(2*time.Minute), // Expire entries after 2 minutes since last created. 46 | ) 47 | 48 | getTicker := time.Tick(100 * time.Millisecond) 49 | reportTicker := time.Tick(5 * time.Second) 50 | for { 51 | select { 52 | case <-getTicker: 53 | _, _ = c.Get(rand.Intn(200)) 54 | case <-reportTicker: 55 | st := cache.Stats{} 56 | c.Stats(&st) 57 | fmt.Printf("%+v\n", st) 58 | } 59 | } 60 | } 61 | ``` 62 | 63 | ## Performance 64 | 65 | See [traces](traces/) and [benchmark](https://github.com/goburrow/cache/wiki/Benchmark) 66 | 67 | ![report](traces/report.png) 68 | -------------------------------------------------------------------------------- /traces/report.go: -------------------------------------------------------------------------------- 1 | package traces 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/Comcast/goburrow-cache" 9 | ) 10 | 11 | type Reporter interface { 12 | Report(cache.Stats, options) 13 | } 14 | 15 | type Provider interface { 16 | Provide(ctx context.Context, keys chan<- interface{}) 17 | } 18 | 19 | type reporter struct { 20 | w io.Writer 21 | headerPrinted bool 22 | } 23 | 24 | func NewReporter(w io.Writer) Reporter { 25 | return &reporter{w: w} 26 | } 27 | 28 | func (r *reporter) Report(st cache.Stats, opt options) { 29 | if !r.headerPrinted { 30 | fmt.Fprintf(r.w, "Requets,Hits,HitRate,Evictions,CacheSize\n") 31 | r.headerPrinted = true 32 | } 33 | fmt.Fprintf(r.w, "%d,%d,%.04f,%d,%d\n", 34 | st.RequestCount(), st.HitCount, st.HitRate(), st.EvictionCount, 35 | opt.cacheSize) 36 | } 37 | 38 | type options struct { 39 | policy string 40 | cacheSize int 41 | reportInterval int 42 | maxItems int 43 | } 44 | 45 | var policies = []string{ 46 | "lru", 47 | "slru", 48 | "tinylfu", 49 | } 50 | 51 | func benchmarkCache(p Provider, r Reporter, opt options) { 52 | c := cache.New(cache.WithMaximumSize(opt.cacheSize), cache.WithPolicy(opt.policy)) 53 | defer c.Close() 54 | 55 | keys := make(chan interface{}, 100) 56 | ctx, cancel := context.WithCancel(context.Background()) 57 | defer cancel() 58 | 59 | go p.Provide(ctx, keys) 60 | stats := cache.Stats{} 61 | i := 0 62 | for { 63 | if opt.maxItems > 0 && i >= opt.maxItems { 64 | break 65 | } 66 | k, ok := <-keys 67 | if !ok { 68 | break 69 | } 70 | _, ok = c.GetIfPresent(k) 71 | if !ok { 72 | c.Put(k, k) 73 | } 74 | i++ 75 | if opt.reportInterval > 0 && i%opt.reportInterval == 0 { 76 | c.Stats(&stats) 77 | r.Report(stats, opt) 78 | } 79 | } 80 | if opt.reportInterval == 0 { 81 | c.Stats(&stats) 82 | r.Report(stats, opt) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /hash_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "encoding/binary" 5 | "hash/fnv" 6 | "testing" 7 | "unsafe" 8 | ) 9 | 10 | func sumFNV(data []byte) uint64 { 11 | h := fnv.New64a() 12 | h.Write(data) 13 | return h.Sum64() 14 | } 15 | 16 | func sumFNVu64(v uint64) uint64 { 17 | b := make([]byte, 8) 18 | binary.LittleEndian.PutUint64(b, v) 19 | return sumFNV(b) 20 | } 21 | 22 | func sumFNVu32(v uint32) uint64 { 23 | b := make([]byte, 4) 24 | binary.LittleEndian.PutUint32(b, v) 25 | return sumFNV(b) 26 | } 27 | 28 | func TestSum(t *testing.T) { 29 | var tests = []struct { 30 | k interface{} 31 | h uint64 32 | }{ 33 | {int(-1), sumFNVu64(^uint64(1) + 1)}, 34 | {int8(-8), sumFNVu32(^uint32(8) + 1)}, 35 | {int16(-16), sumFNVu32(^uint32(16) + 1)}, 36 | {int32(-32), sumFNVu32(^uint32(32) + 1)}, 37 | {int64(-64), sumFNVu64(^uint64(64) + 1)}, 38 | {uint(1), sumFNVu64(1)}, 39 | {uint8(8), sumFNVu32(8)}, 40 | {uint16(16), sumFNVu32(16)}, 41 | {uint32(32), sumFNVu32(32)}, 42 | {uint64(64), sumFNVu64(64)}, 43 | {byte(255), sumFNVu32(255)}, 44 | {rune(1024), sumFNVu32(1024)}, 45 | {true, 1}, 46 | {false, 0}, 47 | {float32(2.5), sumFNVu32(0x40200000)}, 48 | {float64(2.5), sumFNVu64(0x4004000000000000)}, 49 | {uintptr(unsafe.Pointer(t)), sumFNVu64(uint64(uintptr(unsafe.Pointer(t))))}, 50 | {"", sumFNV(nil)}, 51 | {"string", sumFNV([]byte("string"))}, 52 | {t, sumFNVu64(uint64(uintptr(unsafe.Pointer(t))))}, 53 | {(*testing.T)(nil), sumFNVu64(0)}, 54 | } 55 | 56 | for _, tt := range tests { 57 | h := sum(tt.k) 58 | if h != tt.h { 59 | t.Errorf("unexpected hash: %v (0x%x), key: %+v (%T), want: %v", 60 | h, h, tt.k, tt.k, tt.h) 61 | } 62 | } 63 | } 64 | 65 | func BenchmarkSumInt(b *testing.B) { 66 | b.ReportAllocs() 67 | b.RunParallel(func(pb *testing.PB) { 68 | for pb.Next() { 69 | sum(0x0105) 70 | } 71 | }) 72 | } 73 | 74 | func BenchmarkSumString(b *testing.B) { 75 | b.ReportAllocs() 76 | b.RunParallel(func(pb *testing.PB) { 77 | for pb.Next() { 78 | sum("09130105060103210913010506010321091301050601032109130105060103210913010506010321") 79 | } 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "math" 4 | 5 | // bloomFilter is Bloom Filter implementation used as a cache admission policy. 6 | // See http://billmill.org/bloomfilter-tutorial/ 7 | type bloomFilter struct { 8 | numHashes uint32 // number of hashes per element 9 | bitsMask uint32 // size of bit vector 10 | bits []uint64 // filter bit vector 11 | } 12 | 13 | // init initializes bloomFilter with the given expected insertions ins and 14 | // false positive probability fpp. 15 | func (f *bloomFilter) init(ins int, fpp float64) { 16 | ln2 := math.Log(2.0) 17 | factor := -math.Log(fpp) / (ln2 * ln2) 18 | 19 | numBits := nextPowerOfTwo(uint32(float64(ins) * factor)) 20 | if numBits == 0 { 21 | numBits = 1 22 | } 23 | f.bitsMask = numBits - 1 24 | 25 | if ins == 0 { 26 | f.numHashes = 1 27 | } else { 28 | f.numHashes = uint32(ln2 * float64(numBits) / float64(ins)) 29 | } 30 | 31 | size := int(numBits+63) / 64 32 | if len(f.bits) != size { 33 | f.bits = make([]uint64, size) 34 | } else { 35 | f.reset() 36 | } 37 | } 38 | 39 | // put inserts a hash value into the bloom filter. 40 | // It returns true if the value may already in the filter. 41 | func (f *bloomFilter) put(h uint64) bool { 42 | h1, h2 := uint32(h), uint32(h>>32) 43 | var o uint = 1 44 | for i := uint32(0); i < f.numHashes; i++ { 45 | o &= f.set((h1 + (i * h2)) & f.bitsMask) 46 | } 47 | return o == 1 48 | } 49 | 50 | // contains returns true if the given hash is may be in the filter. 51 | func (f *bloomFilter) contains(h uint64) bool { 52 | h1, h2 := uint32(h), uint32(h>>32) 53 | var o uint = 1 54 | for i := uint32(0); i < f.numHashes; i++ { 55 | o &= f.get((h1 + (i * h2)) & f.bitsMask) 56 | } 57 | return o == 1 58 | } 59 | 60 | // set sets bit at index i and returns previous value. 61 | func (f *bloomFilter) set(i uint32) uint { 62 | idx, shift := i/64, i%64 63 | val := f.bits[idx] 64 | mask := uint64(1) << shift 65 | f.bits[idx] |= mask 66 | return uint((val & mask) >> shift) 67 | } 68 | 69 | // get returns bit set at index i. 70 | func (f *bloomFilter) get(i uint32) uint { 71 | idx, shift := i/64, i%64 72 | val := f.bits[idx] 73 | mask := uint64(1) << shift 74 | return uint((val & mask) >> shift) 75 | } 76 | 77 | // reset clears the bloom filter. 78 | func (f *bloomFilter) reset() { 79 | for i := range f.bits { 80 | f.bits[i] = 0 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/Comcast/goburrow-cache/synthetic" 8 | ) 9 | 10 | const ( 11 | testMaxSize = 512 12 | benchmarkThreshold = 100 13 | ) 14 | 15 | type sameGenerator int 16 | 17 | func (g sameGenerator) Int() int { 18 | return int(g) 19 | } 20 | 21 | func BenchmarkSame(b *testing.B) { 22 | g := sameGenerator(1) 23 | benchmarkCache(b, g) 24 | } 25 | 26 | func BenchmarkUniform(b *testing.B) { 27 | distintKeys := testMaxSize * 2 28 | g := synthetic.Uniform(0, distintKeys) 29 | benchmarkCache(b, g) 30 | } 31 | 32 | func BenchmarkUniformLess(b *testing.B) { 33 | distintKeys := testMaxSize 34 | g := synthetic.Uniform(0, distintKeys) 35 | benchmarkCache(b, g) 36 | } 37 | 38 | func BenchmarkCounter(b *testing.B) { 39 | g := synthetic.Counter(0) 40 | benchmarkCache(b, g) 41 | } 42 | 43 | func BenchmarkExponential(b *testing.B) { 44 | g := synthetic.Exponential(1.0) 45 | benchmarkCache(b, g) 46 | } 47 | 48 | func BenchmarkZipf(b *testing.B) { 49 | items := testMaxSize * 10 50 | g := synthetic.Zipf(0, items, 1.01) 51 | benchmarkCache(b, g) 52 | } 53 | 54 | func BenchmarkHotspot(b *testing.B) { 55 | items := testMaxSize * 2 56 | g := synthetic.Hotspot(0, items, 0.25) 57 | benchmarkCache(b, g) 58 | } 59 | 60 | func benchmarkCache(b *testing.B, g synthetic.Generator) { 61 | c := New(WithMaximumSize(testMaxSize)) 62 | defer c.Close() 63 | 64 | intCh := make(chan int, 100) 65 | go func(n int) { 66 | for i := 0; i < n; i++ { 67 | intCh <- g.Int() 68 | } 69 | }(b.N) 70 | defer close(intCh) 71 | 72 | if b.N > benchmarkThreshold { 73 | defer printStats(b, c, time.Now()) 74 | } 75 | 76 | b.ResetTimer() 77 | b.ReportAllocs() 78 | b.RunParallel(func(pb *testing.PB) { 79 | for pb.Next() { 80 | k := <-intCh 81 | _, ok := c.GetIfPresent(k) 82 | if !ok { 83 | c.Put(k, k) 84 | } 85 | } 86 | }) 87 | } 88 | 89 | func printStats(b *testing.B, c Cache, start time.Time) { 90 | dur := time.Since(start) 91 | 92 | var st Stats 93 | c.Stats(&st) 94 | 95 | b.Logf("total: %d (%s), hits: %d (%.2f%%), misses: %d (%.2f%%), evictions: %d\n", 96 | st.RequestCount(), dur, 97 | st.HitCount, st.HitRate()*100.0, 98 | st.MissCount, st.MissRate()*100.0, 99 | st.EvictionCount) 100 | } 101 | -------------------------------------------------------------------------------- /sketch.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | const sketchDepth = 4 4 | 5 | // countMinSketch is an implementation of count-min sketch with 4-bit counters. 6 | // See http://dimacs.rutgers.edu/~graham/pubs/papers/cmsoft.pdf 7 | type countMinSketch struct { 8 | counters []uint64 9 | mask uint32 10 | } 11 | 12 | // init initialize count-min sketch with the given width. 13 | func (c *countMinSketch) init(width int) { 14 | // Need (width x 4 x 4) bits = width/4 x uint64 15 | size := nextPowerOfTwo(uint32(width)) >> 2 16 | if size < 1 { 17 | size = 1 18 | } 19 | c.mask = size - 1 20 | if len(c.counters) == int(size) { 21 | c.clear() 22 | } else { 23 | c.counters = make([]uint64, size) 24 | } 25 | } 26 | 27 | // add increases counters associated with the given hash. 28 | func (c *countMinSketch) add(h uint64) { 29 | h1, h2 := uint32(h), uint32(h>>32) 30 | 31 | for i := uint32(0); i < sketchDepth; i++ { 32 | idx, off := c.position(h1 + i*h2) 33 | c.inc(idx, (16*i)+off) 34 | } 35 | } 36 | 37 | // estimate returns minimum value of counters associated with the given hash. 38 | func (c *countMinSketch) estimate(h uint64) uint8 { 39 | h1, h2 := uint32(h), uint32(h>>32) 40 | 41 | var min uint8 = 0xFF 42 | for i := uint32(0); i < sketchDepth; i++ { 43 | idx, off := c.position(h1 + i*h2) 44 | count := c.val(idx, (16*i)+off) 45 | if count < min { 46 | min = count 47 | } 48 | } 49 | return min 50 | } 51 | 52 | // reset divides all counters by two. 53 | func (c *countMinSketch) reset() { 54 | for i, v := range c.counters { 55 | if v != 0 { 56 | c.counters[i] = (v >> 1) & 0x7777777777777777 57 | } 58 | } 59 | } 60 | 61 | func (c *countMinSketch) position(h uint32) (idx uint32, off uint32) { 62 | idx = (h >> 2) & c.mask 63 | off = (h & 3) << 2 64 | return 65 | } 66 | 67 | // inc increases value at index idx. 68 | func (c *countMinSketch) inc(idx, off uint32) { 69 | v := c.counters[idx] 70 | count := uint8(v>>off) & 0x0F 71 | if count < 15 { 72 | c.counters[idx] = v + (1 << off) 73 | } 74 | } 75 | 76 | // val returns value at index idx. 77 | func (c *countMinSketch) val(idx, off uint32) uint8 { 78 | v := c.counters[idx] 79 | return uint8(v>>off) & 0x0F 80 | } 81 | 82 | func (c *countMinSketch) clear() { 83 | for i := range c.counters { 84 | c.counters[i] = 0 85 | } 86 | } 87 | 88 | // nextPowerOfTwo returns the smallest power of two which is greater than or equal to i. 89 | func nextPowerOfTwo(i uint32) uint32 { 90 | n := i - 1 91 | n |= n >> 1 92 | n |= n >> 2 93 | n |= n >> 4 94 | n |= n >> 8 95 | n |= n >> 16 96 | n++ 97 | return n 98 | } 99 | -------------------------------------------------------------------------------- /tinylfu.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | const ( 4 | samplesMultiplier = 8 5 | insertionsMultiplier = 2 6 | countersMultiplier = 1 7 | falsePositiveProbability = 0.1 8 | admissionRatio = 0.01 9 | ) 10 | 11 | // tinyLFU is an implementation of TinyLFU. It utilizing 4bit Count Min Sketch 12 | // and Bloom Filter as a Doorkeeper and Segmented LRU for long term retention. 13 | // See https://arxiv.org/pdf/1512.00727v2.pdf 14 | type tinyLFU struct { 15 | filter bloomFilter // 1bit counter 16 | counter countMinSketch // 4bit counter 17 | 18 | additions int 19 | samples int 20 | 21 | lru lruCache 22 | slru slruCache 23 | } 24 | 25 | func (l *tinyLFU) init(c *cache, cap int) { 26 | if cap > 0 { 27 | // Only enable doorkeeper when capacity is finite. 28 | l.samples = samplesMultiplier * cap 29 | l.filter.init(insertionsMultiplier*cap, falsePositiveProbability) 30 | l.counter.init(countersMultiplier * cap) 31 | } 32 | lruCap := int(float64(cap) * admissionRatio) 33 | l.lru.init(c, lruCap) 34 | l.slru.init(c, cap-lruCap) 35 | } 36 | 37 | func (l *tinyLFU) write(en *entry) *entry { 38 | if l.lru.cap <= 0 { 39 | return l.slru.write(en) 40 | } 41 | l.increase(en.hash) 42 | candidate := l.lru.write(en) 43 | if candidate == nil { 44 | return nil 45 | } 46 | victim := l.slru.victim() 47 | if victim == nil { 48 | return l.slru.write(candidate) 49 | } 50 | // Determine one going to be evicted 51 | candidateFreq := l.estimate(candidate.hash) 52 | victimFreq := l.estimate(victim.hash) 53 | if candidateFreq > victimFreq { 54 | return l.slru.write(candidate) 55 | } 56 | return candidate 57 | } 58 | 59 | func (l *tinyLFU) access(en *entry) { 60 | l.increase(en.hash) 61 | if en.listID == admissionWindow { 62 | l.lru.access(en) 63 | } else { 64 | l.slru.access(en) 65 | } 66 | } 67 | 68 | func (l *tinyLFU) remove(en *entry) *entry { 69 | if en.listID == admissionWindow { 70 | return l.lru.remove(en) 71 | } 72 | return l.slru.remove(en) 73 | } 74 | 75 | // increase adds the given hash to the filter and counter. 76 | func (l *tinyLFU) increase(h uint64) { 77 | if l.samples <= 0 { 78 | return 79 | } 80 | l.additions++ 81 | if l.additions >= l.samples { 82 | l.filter.reset() 83 | l.counter.reset() 84 | l.additions = 0 85 | } 86 | if l.filter.put(h) { 87 | l.counter.add(h) 88 | } 89 | } 90 | 91 | // estimate estimates frequency of the given hash value. 92 | func (l *tinyLFU) estimate(h uint64) uint8 { 93 | freq := l.counter.estimate(h) 94 | if l.filter.contains(h) { 95 | freq++ 96 | } 97 | return freq 98 | } 99 | 100 | // iterate walks through all lists by access time. 101 | func (l *tinyLFU) iterate(fn func(en *entry) bool) { 102 | l.slru.iterate(fn) 103 | l.lru.iterate(fn) 104 | } 105 | -------------------------------------------------------------------------------- /hash.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "math" 5 | "reflect" 6 | ) 7 | 8 | // Hash is an interface implemented by cache keys to 9 | // override default hash function. 10 | type Hash interface { 11 | Sum64() uint64 12 | } 13 | 14 | // sum calculates hash value of the given key. 15 | func sum(k interface{}) uint64 { 16 | switch h := k.(type) { 17 | case Hash: 18 | return h.Sum64() 19 | case int: 20 | return hashU64(uint64(h)) 21 | case int8: 22 | return hashU32(uint32(h)) 23 | case int16: 24 | return hashU32(uint32(h)) 25 | case int32: 26 | return hashU32(uint32(h)) 27 | case int64: 28 | return hashU64(uint64(h)) 29 | case uint: 30 | return hashU64(uint64(h)) 31 | case uint8: 32 | return hashU32(uint32(h)) 33 | case uint16: 34 | return hashU32(uint32(h)) 35 | case uint32: 36 | return hashU32(h) 37 | case uint64: 38 | return hashU64(h) 39 | case uintptr: 40 | return hashU64(uint64(h)) 41 | case float32: 42 | return hashU32(math.Float32bits(h)) 43 | case float64: 44 | return hashU64(math.Float64bits(h)) 45 | case bool: 46 | if h { 47 | return 1 48 | } 49 | return 0 50 | case string: 51 | return hashString(h) 52 | } 53 | // TODO: complex64 and complex128 54 | if h, ok := hashPointer(k); ok { 55 | return h 56 | } 57 | // TODO: use gob to encode k to bytes then hash. 58 | return 0 59 | } 60 | 61 | const ( 62 | fnvOffset uint64 = 14695981039346656037 63 | fnvPrime uint64 = 1099511628211 64 | ) 65 | 66 | func hashU64(v uint64) uint64 { 67 | // Inline code from hash/fnv to reduce memory allocations 68 | h := fnvOffset 69 | // for i := uint(0); i < 64; i += 8 { 70 | // h ^= (v >> i) & 0xFF 71 | // h *= fnvPrime 72 | // } 73 | h ^= (v >> 0) & 0xFF 74 | h *= fnvPrime 75 | h ^= (v >> 8) & 0xFF 76 | h *= fnvPrime 77 | h ^= (v >> 16) & 0xFF 78 | h *= fnvPrime 79 | h ^= (v >> 24) & 0xFF 80 | h *= fnvPrime 81 | h ^= (v >> 32) & 0xFF 82 | h *= fnvPrime 83 | h ^= (v >> 40) & 0xFF 84 | h *= fnvPrime 85 | h ^= (v >> 48) & 0xFF 86 | h *= fnvPrime 87 | h ^= (v >> 56) & 0xFF 88 | h *= fnvPrime 89 | return h 90 | } 91 | 92 | func hashU32(v uint32) uint64 { 93 | h := fnvOffset 94 | h ^= uint64(v>>0) & 0xFF 95 | h *= fnvPrime 96 | h ^= uint64(v>>8) & 0xFF 97 | h *= fnvPrime 98 | h ^= uint64(v>>16) & 0xFF 99 | h *= fnvPrime 100 | h ^= uint64(v>>24) & 0xFF 101 | h *= fnvPrime 102 | return h 103 | } 104 | 105 | // hashString calculates hash value using FNV-1a algorithm. 106 | func hashString(data string) uint64 { 107 | // Inline code from hash/fnv to reduce memory allocations 108 | h := fnvOffset 109 | for _, b := range data { 110 | h ^= uint64(b) 111 | h *= fnvPrime 112 | } 113 | return h 114 | } 115 | 116 | func hashPointer(k interface{}) (uint64, bool) { 117 | v := reflect.ValueOf(k) 118 | switch v.Kind() { 119 | case reflect.Ptr, reflect.UnsafePointer, reflect.Func, reflect.Slice, reflect.Map, reflect.Chan: 120 | return hashU64(uint64(v.Pointer())), true 121 | default: 122 | return 0, false 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | // Package cache provides partial implementations of Guava Cache, 2 | // including support for LRU, Segmented LRU and TinyLFU. 3 | package cache 4 | 5 | // Key is any value which is comparable. 6 | // See http://golang.org/ref/spec#Comparison_operators for details. 7 | type Key interface{} 8 | 9 | // Value is any value. 10 | type Value interface{} 11 | 12 | // Cache is a key-value cache which entries are added and stayed in the 13 | // cache until either are evicted or manually invalidated. 14 | type Cache interface { 15 | // GetIfPresent returns value associated with Key or (nil, false) 16 | // if there is no cached value for Key. 17 | GetIfPresent(Key) (Value, bool) 18 | 19 | // Get all keys. 20 | GetAllKeys() []interface{} 21 | 22 | // Get all values. 23 | GetAllValues() []interface{} 24 | 25 | // Get all entries. 26 | GetAll() map[interface{}]interface{} 27 | 28 | // Put associates value with Key. If a value is already associated 29 | // with Key, the old one will be replaced with Value. 30 | Put(Key, Value) 31 | 32 | // Invalidate discards cached value of the given Key. 33 | Invalidate(Key) 34 | 35 | // InvalidateAll discards all entries. 36 | InvalidateAll() 37 | 38 | // Size of cache 39 | Size() int 40 | 41 | // Stats copies cache statistics to given Stats pointer. 42 | Stats(*Stats) 43 | 44 | // Close implements io.Closer for cleaning up all resources. 45 | // Users must ensure the cache is not being used before closing or 46 | // after closed. 47 | Close() error 48 | } 49 | 50 | // Func is a generic callback for entry events in the cache. 51 | type Func func(Key, Value) 52 | 53 | // LoadingCache is a cache with values are loaded automatically and stored 54 | // in the cache until either evicted or manually invalidated. 55 | type LoadingCache interface { 56 | Cache 57 | 58 | // Get returns value associated with Key or call underlying LoaderFunc 59 | // to load value if it is not present. 60 | Get(Key) (Value, error) 61 | 62 | // Refresh loads new value for Key. If the Key already existed, the previous value 63 | // will continue to be returned by Get while the new value is loading. 64 | // If Key does not exist, this function will block until the value is loaded. 65 | Refresh(Key) 66 | 67 | // RefreshIfModifiedAfter asynchronously reloads value for Key if the provided timestamp 68 | // indicates the data was modified after the entry was last loaded. If the entry doesn't exist, 69 | // it will synchronously load and block until the value is loaded. 70 | RefreshIfModifiedAfter(Key, int64) 71 | } 72 | 73 | // LoaderFunc retrieves the value corresponding to given Key. 74 | type LoaderFunc func(Key) (Value, error) 75 | 76 | // Reloader specifies how cache loader is run to refresh value for the given Key. 77 | // If Reloader is not set, cache values are refreshed in a new go routine. 78 | type Reloader interface { 79 | // Reload should reload the value asynchronously. 80 | // Application must call setFunc to set new value or error. 81 | Reload(key Key, oldValue Value, setFunc func(Value, error)) 82 | // Close shuts down all running tasks. Currently, the error returned is not being used. 83 | Close() error 84 | } 85 | -------------------------------------------------------------------------------- /traces/files.go: -------------------------------------------------------------------------------- 1 | package traces 2 | 3 | import ( 4 | "compress/bzip2" 5 | "compress/gzip" 6 | "fmt" 7 | "io" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | ) 12 | 13 | type readSeekCloser interface { 14 | io.ReadCloser 15 | io.Seeker 16 | } 17 | 18 | type gzipFile struct { 19 | r *gzip.Reader 20 | f *os.File 21 | } 22 | 23 | func newGzipFile(f *os.File) *gzipFile { 24 | r, err := gzip.NewReader(f) 25 | if err != nil { 26 | panic(err) 27 | } 28 | return &gzipFile{ 29 | r: r, 30 | f: f, 31 | } 32 | } 33 | 34 | func (f *gzipFile) Read(p []byte) (int, error) { 35 | return f.r.Read(p) 36 | } 37 | 38 | func (f *gzipFile) Seek(offset int64, whence int) (int64, error) { 39 | n, err := f.f.Seek(offset, whence) 40 | if err != nil { 41 | return n, err 42 | } 43 | f.r.Reset(f.f) 44 | return n, nil 45 | } 46 | 47 | func (f *gzipFile) Close() error { 48 | err1 := f.r.Close() 49 | err2 := f.f.Close() 50 | if err2 != nil { 51 | return err2 52 | } 53 | return err1 54 | } 55 | 56 | type bzip2File struct { 57 | r io.Reader 58 | f *os.File 59 | } 60 | 61 | func newBzip2File(f *os.File) *bzip2File { 62 | return &bzip2File{ 63 | r: bzip2.NewReader(f), 64 | f: f, 65 | } 66 | } 67 | 68 | func (f *bzip2File) Read(p []byte) (int, error) { 69 | return f.r.Read(p) 70 | } 71 | 72 | func (f *bzip2File) Seek(offset int64, whence int) (int64, error) { 73 | n, err := f.f.Seek(offset, whence) 74 | if err != nil { 75 | return n, err 76 | } 77 | f.r = bzip2.NewReader(f.f) 78 | return n, nil 79 | } 80 | 81 | func (f *bzip2File) Close() error { 82 | return f.f.Close() 83 | } 84 | 85 | type filesReader struct { 86 | io.Reader 87 | files []readSeekCloser 88 | } 89 | 90 | func openFilesGlob(pattern string) (*filesReader, error) { 91 | files, err := filepath.Glob(pattern) 92 | if err != nil { 93 | return nil, err 94 | } 95 | if len(files) == 0 { 96 | return nil, fmt.Errorf("%s not found", pattern) 97 | } 98 | return openFiles(files...) 99 | } 100 | 101 | func openFiles(files ...string) (*filesReader, error) { 102 | r := &filesReader{} 103 | r.files = make([]readSeekCloser, 0, len(files)) 104 | readers := make([]io.Reader, 0, len(files)) 105 | for _, name := range files { 106 | f, err := os.Open(name) 107 | if err != nil { 108 | r.Close() 109 | return nil, err 110 | } 111 | var rs readSeekCloser 112 | if strings.HasSuffix(name, ".gz") { 113 | rs = newGzipFile(f) 114 | } else if strings.HasSuffix(name, ".bz2") { 115 | rs = newBzip2File(f) 116 | } else { 117 | rs = f 118 | } 119 | r.files = append(r.files, rs) 120 | readers = append(readers, rs) 121 | } 122 | r.Reader = io.MultiReader(readers...) 123 | return r, nil 124 | } 125 | 126 | func (r *filesReader) Close() error { 127 | var err error 128 | for _, f := range r.files { 129 | e := f.Close() 130 | if err != nil && e != nil { 131 | err = e 132 | } 133 | } 134 | return err 135 | } 136 | 137 | func (r *filesReader) Reset() error { 138 | readers := make([]io.Reader, 0, len(r.files)) 139 | for _, f := range r.files { 140 | _, err := f.Seek(0, 0) 141 | if err != nil { 142 | return err 143 | } 144 | readers = append(readers, f) 145 | } 146 | r.Reader = io.MultiReader(readers...) 147 | return nil 148 | } 149 | -------------------------------------------------------------------------------- /tinylfu_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | type tinyLFUTest struct { 9 | c cache 10 | lfu tinyLFU 11 | t *testing.T 12 | } 13 | 14 | func (t *tinyLFUTest) assertCap(n int) { 15 | if t.lfu.lru.cap+t.lfu.slru.protectedCap+t.lfu.slru.probationCap != n { 16 | t.t.Helper() 17 | t.t.Fatalf("unexpected lru.cap: %d, slru.cap: %d/%d", 18 | t.lfu.lru.cap, t.lfu.slru.protectedCap, t.lfu.slru.probationCap) 19 | } 20 | } 21 | 22 | func (t *tinyLFUTest) assertLen(admission, protected, probation int) { 23 | sz := cacheSize(&t.c) 24 | az := t.lfu.lru.ls.Len() 25 | tz := t.lfu.slru.protectedLs.Len() 26 | bz := t.lfu.slru.probationLs.Len() 27 | if sz != admission+protected+probation || az != admission || tz != protected || bz != probation { 28 | t.t.Helper() 29 | t.t.Fatalf("unexpected data length: cache=%d admission=%d protected=%d probation=%d, want: %d %d %d", 30 | sz, az, tz, bz, admission, protected, probation) 31 | } 32 | } 33 | 34 | func (t *tinyLFUTest) assertEntry(en *entry, k int, v string, id uint8) { 35 | ak := en.key.(int) 36 | av := en.getValue().(string) 37 | if ak != k || av != v || en.listID != id { 38 | t.t.Helper() 39 | t.t.Fatalf("unexpected entry: %+v, want: {key: %d, value: %s, listID: %d}", 40 | en, k, v, id) 41 | } 42 | } 43 | 44 | func (t *tinyLFUTest) assertLRUEntry(k int, id uint8) { 45 | en := t.c.get(k, sum(k)) 46 | if en == nil { 47 | t.t.Helper() 48 | t.t.Fatalf("entry not found in cache: key=%v", k) 49 | } 50 | ak := en.key.(int) 51 | av := en.getValue().(string) 52 | v := fmt.Sprintf("%d", k) 53 | if ak != k || av != v || en.listID != id { 54 | t.t.Helper() 55 | t.t.Fatalf("unexpected entry: %+v, want: {key: %d, value: %s, listID: %d}", 56 | en, k, v, id) 57 | } 58 | } 59 | 60 | func TestTinyLFU(t *testing.T) { 61 | s := tinyLFUTest{t: t} 62 | s.lfu.init(&s.c, 200) 63 | s.assertCap(200) 64 | s.lfu.slru.protectedCap = 2 65 | s.lfu.slru.probationCap = 1 66 | 67 | en := make([]*entry, 10) 68 | for i := range en { 69 | en[i] = newEntry(i, fmt.Sprintf("%d", i), sum(i)) 70 | } 71 | for i := 0; i < 5; i++ { 72 | remEn := s.lfu.write(en[i]) 73 | if remEn != nil { 74 | t.Fatalf("unexpected entry removed: %+v", remEn) 75 | } 76 | } 77 | // 4 3 | - | 2 1 0 78 | s.assertLen(2, 0, 3) 79 | s.assertLRUEntry(4, admissionWindow) 80 | s.assertLRUEntry(3, admissionWindow) 81 | s.assertLRUEntry(2, probationSegment) 82 | s.assertLRUEntry(1, probationSegment) 83 | s.assertLRUEntry(0, probationSegment) 84 | 85 | s.lfu.access(en[1]) 86 | s.lfu.access(en[2]) 87 | // 4 3 | 2 1 | 0 88 | s.assertLen(2, 2, 1) 89 | s.assertLRUEntry(2, protectedSegment) 90 | s.assertLRUEntry(1, protectedSegment) 91 | s.assertLRUEntry(0, probationSegment) 92 | 93 | remEn := s.lfu.write(en[5]) 94 | // 5 4 | 2 1 | 0 95 | if remEn == nil { 96 | t.Fatalf("expect an entry removed when adding %+v", en[5]) 97 | } 98 | s.assertEntry(remEn, 3, "3", admissionWindow) 99 | 100 | s.lfu.access(en[4]) 101 | s.lfu.access(en[5]) 102 | remEn = s.lfu.write(en[6]) 103 | // 6 5 | 2 1 | 4 104 | if remEn == nil { 105 | t.Fatalf("expect an entry removed when adding %+v", en[6]) 106 | } 107 | s.assertLen(2, 2, 1) 108 | s.assertEntry(remEn, 0, "0", probationSegment) 109 | n := s.lfu.estimate(en[1].hash) 110 | if n != 2 { 111 | t.Fatalf("unexpected estimate: %d %+v", n, en[1]) 112 | } 113 | s.lfu.access(en[2]) 114 | s.lfu.access(en[2]) 115 | n = s.lfu.estimate(en[2].hash) 116 | if n != 4 { 117 | t.Fatalf("unexpected estimate: %d %+v", n, en[2]) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /stats.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "fmt" 5 | "sync/atomic" 6 | "time" 7 | ) 8 | 9 | // Stats is statistics about performance of a cache. 10 | type Stats struct { 11 | HitCount uint64 12 | MissCount uint64 13 | LoadSuccessCount uint64 14 | LoadErrorCount uint64 15 | TotalLoadTime time.Duration 16 | EvictionCount uint64 17 | } 18 | 19 | // RequestCount returns a total of HitCount and MissCount. 20 | func (s *Stats) RequestCount() uint64 { 21 | return s.HitCount + s.MissCount 22 | } 23 | 24 | // HitRate returns the ratio of cache requests which were hits. 25 | func (s *Stats) HitRate() float64 { 26 | total := s.RequestCount() 27 | if total == 0 { 28 | return 1.0 29 | } 30 | return float64(s.HitCount) / float64(total) 31 | } 32 | 33 | // MissRate returns the ratio of cache requests which were misses. 34 | func (s *Stats) MissRate() float64 { 35 | total := s.RequestCount() 36 | if total == 0 { 37 | return 0.0 38 | } 39 | return float64(s.MissCount) / float64(total) 40 | } 41 | 42 | // LoadErrorRate returns the ratio of cache loading attempts which returned errors. 43 | func (s *Stats) LoadErrorRate() float64 { 44 | total := s.LoadSuccessCount + s.LoadErrorCount 45 | if total == 0 { 46 | return 0.0 47 | } 48 | return float64(s.LoadErrorCount) / float64(total) 49 | } 50 | 51 | // AverageLoadPenalty returns the average time spent loading new values. 52 | func (s *Stats) AverageLoadPenalty() time.Duration { 53 | total := s.LoadSuccessCount + s.LoadErrorCount 54 | if total == 0 { 55 | return 0.0 56 | } 57 | return s.TotalLoadTime / time.Duration(total) 58 | } 59 | 60 | // String returns a string representation of this statistics. 61 | func (s *Stats) String() string { 62 | return fmt.Sprintf("hits: %d, misses: %d, successes: %d, errors: %d, time: %s, evictions: %d", 63 | s.HitCount, s.MissCount, s.LoadSuccessCount, s.LoadErrorCount, s.TotalLoadTime, s.EvictionCount) 64 | } 65 | 66 | // StatsCounter accumulates statistics of a cache. 67 | type StatsCounter interface { 68 | // RecordHits records cache hits. 69 | RecordHits(count uint64) 70 | 71 | // RecordMisses records cache misses. 72 | RecordMisses(count uint64) 73 | 74 | // RecordLoadSuccess records successful load of a new entry. 75 | RecordLoadSuccess(loadTime time.Duration) 76 | 77 | // RecordLoadError records failed load of a new entry. 78 | RecordLoadError(loadTime time.Duration) 79 | 80 | // RecordEviction records eviction of an entry from the cache. 81 | RecordEviction() 82 | 83 | // Snapshot writes snapshot of this counter values to the given Stats pointer. 84 | Snapshot(*Stats) 85 | } 86 | 87 | // statsCounter is a simple implementation of StatsCounter. 88 | type statsCounter struct { 89 | Stats 90 | } 91 | 92 | // RecordHits increases HitCount atomically. 93 | func (s *statsCounter) RecordHits(count uint64) { 94 | atomic.AddUint64(&s.Stats.HitCount, count) 95 | } 96 | 97 | // RecordMisses increases MissCount atomically. 98 | func (s *statsCounter) RecordMisses(count uint64) { 99 | atomic.AddUint64(&s.Stats.MissCount, count) 100 | } 101 | 102 | // RecordLoadSuccess increases LoadSuccessCount atomically. 103 | func (s *statsCounter) RecordLoadSuccess(loadTime time.Duration) { 104 | atomic.AddUint64(&s.Stats.LoadSuccessCount, 1) 105 | atomic.AddInt64((*int64)(&s.Stats.TotalLoadTime), int64(loadTime)) 106 | } 107 | 108 | // RecordLoadError increases LoadErrorCount atomically. 109 | func (s *statsCounter) RecordLoadError(loadTime time.Duration) { 110 | atomic.AddUint64(&s.Stats.LoadErrorCount, 1) 111 | atomic.AddInt64((*int64)(&s.Stats.TotalLoadTime), int64(loadTime)) 112 | } 113 | 114 | // RecordEviction increases EvictionCount atomically. 115 | func (s *statsCounter) RecordEviction() { 116 | atomic.AddUint64(&s.Stats.EvictionCount, 1) 117 | } 118 | 119 | // Snapshot copies current stats to t. 120 | func (s *statsCounter) Snapshot(t *Stats) { 121 | t.HitCount = atomic.LoadUint64(&s.HitCount) 122 | t.MissCount = atomic.LoadUint64(&s.MissCount) 123 | t.LoadSuccessCount = atomic.LoadUint64(&s.LoadSuccessCount) 124 | t.LoadErrorCount = atomic.LoadUint64(&s.LoadErrorCount) 125 | t.TotalLoadTime = time.Duration(atomic.LoadInt64((*int64)(&s.TotalLoadTime))) 126 | t.EvictionCount = atomic.LoadUint64(&s.EvictionCount) 127 | } 128 | -------------------------------------------------------------------------------- /traces/cache2k_test.go: -------------------------------------------------------------------------------- 1 | package traces 2 | 3 | import "testing" 4 | 5 | func TestRequestORMBusy(t *testing.T) { 6 | for _, p := range policies { 7 | p := p 8 | t.Run(p, func(t *testing.T) { 9 | t.Parallel() 10 | opt := options{ 11 | policy: p, 12 | cacheSize: 512, 13 | reportInterval: 40000, 14 | maxItems: 4000000, 15 | } 16 | testRequest(t, NewCache2kProvider, opt, 17 | "trace-mt-db-*-busy.trc.bin.bz2", "request_ormbusy-"+p+".txt") 18 | }) 19 | } 20 | } 21 | 22 | func TestSizeORMBusy(t *testing.T) { 23 | for _, p := range policies { 24 | p := p 25 | t.Run(p, func(t *testing.T) { 26 | t.Parallel() 27 | opt := options{ 28 | policy: p, 29 | cacheSize: 250, 30 | maxItems: 1000000, 31 | } 32 | testSize(t, NewCache2kProvider, opt, 33 | "trace-mt-db-*-busy.trc.bin.bz2", "size_ormbusy-"+p+".txt") 34 | }) 35 | } 36 | } 37 | 38 | func TestRequestCPP(t *testing.T) { 39 | for _, p := range policies { 40 | p := p 41 | t.Run(p, func(t *testing.T) { 42 | t.Parallel() 43 | opt := options{ 44 | policy: p, 45 | cacheSize: 128, 46 | reportInterval: 100, 47 | maxItems: 9000, 48 | } 49 | testRequest(t, NewCache2kProvider, opt, 50 | "trace-cpp.trc.bin.gz", "request_cpp-"+p+".txt") 51 | }) 52 | } 53 | } 54 | 55 | func TestSizeCPP(t *testing.T) { 56 | for _, p := range policies { 57 | p := p 58 | t.Run(p, func(t *testing.T) { 59 | t.Parallel() 60 | opt := options{ 61 | policy: p, 62 | cacheSize: 25, 63 | maxItems: 9000, 64 | } 65 | testSize(t, NewCache2kProvider, opt, 66 | "trace-cpp.trc.bin.gz", "size_cpp-"+p+".txt") 67 | }) 68 | } 69 | } 70 | 71 | func TestRequestGlimpse(t *testing.T) { 72 | for _, p := range policies { 73 | p := p 74 | t.Run(p, func(t *testing.T) { 75 | t.Parallel() 76 | opt := options{ 77 | policy: p, 78 | cacheSize: 512, 79 | reportInterval: 100, 80 | maxItems: 6000, 81 | } 82 | testRequest(t, NewCache2kProvider, opt, 83 | "trace-glimpse.trc.bin.gz", "request_glimpse-"+p+".txt") 84 | }) 85 | } 86 | } 87 | 88 | func TestSizeGlimpse(t *testing.T) { 89 | for _, p := range policies { 90 | p := p 91 | t.Run(p, func(t *testing.T) { 92 | t.Parallel() 93 | opt := options{ 94 | policy: p, 95 | cacheSize: 125, 96 | maxItems: 6000, 97 | } 98 | testSize(t, NewCache2kProvider, opt, 99 | "trace-glimpse.trc.bin.gz", "size_glimpse-"+p+".txt") 100 | }) 101 | } 102 | } 103 | 104 | func TestRequestOLTP(t *testing.T) { 105 | for _, p := range policies { 106 | p := p 107 | t.Run(p, func(t *testing.T) { 108 | t.Parallel() 109 | opt := options{ 110 | policy: p, 111 | cacheSize: 512, 112 | reportInterval: 1000, 113 | maxItems: 900000, 114 | } 115 | testRequest(t, NewCache2kProvider, opt, 116 | "trace-oltp.trc.bin.gz", "request_oltp-"+p+".txt") 117 | }) 118 | } 119 | } 120 | 121 | func TestSizeOLTP(t *testing.T) { 122 | for _, p := range policies { 123 | p := p 124 | t.Run(p, func(t *testing.T) { 125 | t.Parallel() 126 | opt := options{ 127 | policy: p, 128 | cacheSize: 250, 129 | maxItems: 500000, 130 | } 131 | testSize(t, NewCache2kProvider, opt, 132 | "trace-oltp.trc.bin.gz", "size_oltp-"+p+".txt") 133 | }) 134 | } 135 | } 136 | 137 | func TestRequestSprite(t *testing.T) { 138 | for _, p := range policies { 139 | p := p 140 | t.Run(p, func(t *testing.T) { 141 | t.Parallel() 142 | opt := options{ 143 | policy: p, 144 | cacheSize: 512, 145 | reportInterval: 1000, 146 | maxItems: 120000, 147 | } 148 | testRequest(t, NewCache2kProvider, opt, 149 | "trace-sprite.trc.bin.gz", "request_sprite-"+p+".txt") 150 | }) 151 | } 152 | } 153 | 154 | func TestSizeSprite(t *testing.T) { 155 | for _, p := range policies { 156 | p := p 157 | t.Run(p, func(t *testing.T) { 158 | t.Parallel() 159 | opt := options{ 160 | policy: p, 161 | cacheSize: 25, 162 | maxItems: 120000, 163 | } 164 | testSize(t, NewCache2kProvider, opt, 165 | "trace-sprite.trc.bin.gz", "size_sprite-"+p+".txt") 166 | }) 167 | } 168 | } 169 | 170 | func TestRequestMulti2(t *testing.T) { 171 | for _, p := range policies { 172 | p := p 173 | t.Run(p, func(t *testing.T) { 174 | t.Parallel() 175 | opt := options{ 176 | policy: p, 177 | cacheSize: 512, 178 | reportInterval: 200, 179 | maxItems: 25000, 180 | } 181 | testRequest(t, NewCache2kProvider, opt, 182 | "trace-multi2.trc.bin.gz", "request_multi2-"+p+".txt") 183 | }) 184 | } 185 | } 186 | 187 | func TestSizeMulti2(t *testing.T) { 188 | for _, p := range policies { 189 | p := p 190 | t.Run(p, func(t *testing.T) { 191 | t.Parallel() 192 | opt := options{ 193 | policy: p, 194 | cacheSize: 250, 195 | maxItems: 25000, 196 | } 197 | testSize(t, NewCache2kProvider, opt, 198 | "trace-multi2.trc.bin.gz", "size_multi2-"+p+".txt") 199 | }) 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /lru.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "container/list" 5 | ) 6 | 7 | // lruCache is a LRU cache. 8 | type lruCache struct { 9 | cache *cache 10 | cap int 11 | ls list.List 12 | } 13 | 14 | // init initializes cache list. 15 | func (l *lruCache) init(c *cache, cap int) { 16 | l.cache = c 17 | l.cap = cap 18 | l.ls.Init() 19 | } 20 | 21 | // write adds new entry to the cache and returns evicted entry if necessary. 22 | func (l *lruCache) write(en *entry) *entry { 23 | // Fast path 24 | if en.accessList != nil { 25 | // Entry existed, update its status instead. 26 | l.markAccess(en) 27 | return nil 28 | } 29 | // Try to add new entry to the list 30 | cen := l.cache.getOrSet(en) 31 | if cen == nil { 32 | // Brand new entry, add to the LRU list. 33 | en.accessList = l.ls.PushFront(en) 34 | } else { 35 | // Entry has already been added, update its value instead. 36 | cen.setValue(en.getValue()) 37 | cen.setWriteTime(en.getWriteTime()) 38 | if cen.accessList == nil { 39 | // Entry is loaded to the cache but not yet registered. 40 | cen.accessList = l.ls.PushFront(cen) 41 | } else { 42 | l.markAccess(cen) 43 | } 44 | } 45 | if l.cap > 0 && l.ls.Len() > l.cap { 46 | // Remove the last element when capacity exceeded. 47 | en = getEntry(l.ls.Back()) 48 | return l.remove(en) 49 | } 50 | return nil 51 | } 52 | 53 | // access updates cache entry for a get. 54 | func (l *lruCache) access(en *entry) { 55 | if en.accessList != nil { 56 | l.markAccess(en) 57 | } 58 | } 59 | 60 | // markAccess marks the element has just been accessed. 61 | // en.accessList must not be null. 62 | func (l *lruCache) markAccess(en *entry) { 63 | l.ls.MoveToFront(en.accessList) 64 | } 65 | 66 | // remove removes an entry from the cache. 67 | func (l *lruCache) remove(en *entry) *entry { 68 | if en.accessList == nil { 69 | // Already deleted 70 | return nil 71 | } 72 | l.cache.delete(en) 73 | l.ls.Remove(en.accessList) 74 | en.accessList = nil 75 | return en 76 | } 77 | 78 | // iterate walks through all lists by access time. 79 | func (l *lruCache) iterate(fn func(en *entry) bool) { 80 | iterateListFromBack(&l.ls, fn) 81 | } 82 | 83 | const ( 84 | admissionWindow uint8 = iota 85 | probationSegment 86 | protectedSegment 87 | ) 88 | 89 | const ( 90 | protectedRatio = 0.8 91 | ) 92 | 93 | // slruCache is a segmented LRU. 94 | // See http://highscalability.com/blog/2016/1/25/design-of-a-modern-cache.html 95 | type slruCache struct { 96 | cache *cache 97 | 98 | probationCap int 99 | probationLs list.List 100 | 101 | protectedCap int 102 | protectedLs list.List 103 | } 104 | 105 | // init initializes the cache list. 106 | func (l *slruCache) init(c *cache, cap int) { 107 | l.cache = c 108 | l.protectedCap = int(float64(cap) * protectedRatio) 109 | l.probationCap = cap - l.protectedCap 110 | l.probationLs.Init() 111 | l.protectedLs.Init() 112 | } 113 | 114 | // length returns total number of entries in the cache. 115 | func (l *slruCache) length() int { 116 | return l.probationLs.Len() + l.protectedLs.Len() 117 | } 118 | 119 | // write adds new entry to the cache and returns evicted entry if necessary. 120 | func (l *slruCache) write(en *entry) *entry { 121 | // Fast path 122 | if en.accessList != nil { 123 | // Entry existed, update its value instead. 124 | l.markAccess(en) 125 | return nil 126 | } 127 | // Try to add new entry to the probation segment. 128 | cen := l.cache.getOrSet(en) 129 | if cen == nil { 130 | // Brand new entry, add to the probation segment. 131 | en.listID = probationSegment 132 | en.accessList = l.probationLs.PushFront(en) 133 | } else { 134 | // Entry has already been added, update its value instead. 135 | cen.setValue(en.getValue()) 136 | cen.setWriteTime(en.getWriteTime()) 137 | if cen.accessList == nil { 138 | // Entry is loaded to the cache but not yet registered. 139 | cen.listID = probationSegment 140 | cen.accessList = l.probationLs.PushFront(cen) 141 | } else { 142 | l.markAccess(cen) 143 | } 144 | } 145 | // The probation list can exceed its capacity if number of entries 146 | // is still under total allowed capacity. 147 | if l.probationCap > 0 && l.probationLs.Len() > l.probationCap && 148 | l.length() > (l.probationCap+l.protectedCap) { 149 | // Remove the last element when capacity exceeded. 150 | en = getEntry(l.probationLs.Back()) 151 | return l.remove(en) 152 | } 153 | return nil 154 | } 155 | 156 | // access updates cache entry for a get. 157 | func (l *slruCache) access(en *entry) { 158 | if en.accessList != nil { 159 | l.markAccess(en) 160 | } 161 | } 162 | 163 | // markAccess marks the element has just been accessed. 164 | // en.accessList must not be null. 165 | func (l *slruCache) markAccess(en *entry) { 166 | if en.listID == protectedSegment { 167 | // Already in the protected segment. 168 | l.protectedLs.MoveToFront(en.accessList) 169 | return 170 | } 171 | // The entry is currently in the probation segment, promote it to the protected segment. 172 | en.listID = protectedSegment 173 | l.probationLs.Remove(en.accessList) 174 | en.accessList = l.protectedLs.PushFront(en) 175 | 176 | if l.protectedCap > 0 && l.protectedLs.Len() > l.protectedCap { 177 | // Protected list capacity exceeded, move the last entry in the protected segment to 178 | // the probation segment. 179 | en = getEntry(l.protectedLs.Back()) 180 | en.listID = probationSegment 181 | l.protectedLs.Remove(en.accessList) 182 | en.accessList = l.probationLs.PushFront(en) 183 | } 184 | } 185 | 186 | // remove removes an entry from the cache and returns the removed entry or nil 187 | // if it is not found. 188 | func (l *slruCache) remove(en *entry) *entry { 189 | if en.accessList == nil { 190 | return nil 191 | } 192 | l.cache.delete(en) 193 | if en.listID == protectedSegment { 194 | l.protectedLs.Remove(en.accessList) 195 | } else { 196 | l.probationLs.Remove(en.accessList) 197 | } 198 | en.accessList = nil 199 | return en 200 | } 201 | 202 | // victim returns the last entry in probation list if total entries reached the limit. 203 | func (l *slruCache) victim() *entry { 204 | if l.probationCap <= 0 || l.length() < (l.probationCap+l.protectedCap) { 205 | return nil 206 | } 207 | el := l.probationLs.Back() 208 | if el == nil { 209 | return nil 210 | } 211 | return getEntry(el) 212 | } 213 | 214 | // iterate walks through all lists by access time. 215 | func (l *slruCache) iterate(fn func(en *entry) bool) { 216 | iterateListFromBack(&l.protectedLs, fn) 217 | iterateListFromBack(&l.probationLs, fn) 218 | } 219 | -------------------------------------------------------------------------------- /policy.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "container/list" 5 | "sync" 6 | "sync/atomic" 7 | ) 8 | 9 | const ( 10 | // Number of cache data store will be 2 ^ concurrencyLevel. 11 | concurrencyLevel = 2 12 | segmentCount = 1 << concurrencyLevel 13 | segmentMask = segmentCount - 1 14 | ) 15 | 16 | // entry stores cached entry key and value. 17 | type entry struct { 18 | // Structs with first field align to 64 bits will also be aligned to 64. 19 | // https://golang.org/pkg/sync/atomic/#pkg-note-BUG 20 | 21 | // hash is the hash value of this entry key 22 | hash uint64 23 | // accessTime is the last time this entry was accessed. 24 | accessTime int64 // Access atomically - must be aligned on 32-bit 25 | // writeTime is the last time this entry was updated. 26 | writeTime int64 // Access atomically - must be aligned on 32-bit 27 | 28 | // FIXME: More efficient way to store boolean flags 29 | invalidated int32 30 | loading int32 31 | 32 | key Key 33 | value atomic.Value // Store value 34 | 35 | // These properties are managed by only cache policy so do not need atomic access. 36 | 37 | // accessList is the list (ordered by access time) this entry is currently in. 38 | accessList *list.Element 39 | // writeList is the list (ordered by write time) this entry is currently in. 40 | writeList *list.Element 41 | // listID is ID of the list which this entry is currently in. 42 | listID uint8 43 | } 44 | 45 | func newEntry(k Key, v Value, h uint64) *entry { 46 | en := &entry{ 47 | key: k, 48 | hash: h, 49 | } 50 | en.setValue(v) 51 | return en 52 | } 53 | 54 | func (e *entry) getValue() Value { 55 | return e.value.Load().(Value) 56 | } 57 | 58 | func (e *entry) setValue(v Value) { 59 | e.value.Store(v) 60 | } 61 | 62 | func (e *entry) getAccessTime() int64 { 63 | return atomic.LoadInt64(&e.accessTime) 64 | } 65 | 66 | func (e *entry) setAccessTime(v int64) { 67 | atomic.StoreInt64(&e.accessTime, v) 68 | } 69 | 70 | func (e *entry) getWriteTime() int64 { 71 | return atomic.LoadInt64(&e.writeTime) 72 | } 73 | 74 | func (e *entry) setWriteTime(v int64) { 75 | atomic.StoreInt64(&e.writeTime, v) 76 | } 77 | 78 | func (e *entry) getLoading() bool { 79 | return atomic.LoadInt32(&e.loading) != 0 80 | } 81 | 82 | func (e *entry) setLoading(v bool) bool { 83 | if v { 84 | return atomic.CompareAndSwapInt32(&e.loading, 0, 1) 85 | } 86 | return atomic.CompareAndSwapInt32(&e.loading, 1, 0) 87 | } 88 | 89 | func (e *entry) getInvalidated() bool { 90 | return atomic.LoadInt32(&e.invalidated) != 0 91 | } 92 | 93 | func (e *entry) setInvalidated(v bool) { 94 | if v { 95 | atomic.StoreInt32(&e.invalidated, 1) 96 | } else { 97 | atomic.StoreInt32(&e.invalidated, 0) 98 | } 99 | } 100 | 101 | // getEntry returns the entry attached to the given list element. 102 | func getEntry(el *list.Element) *entry { 103 | return el.Value.(*entry) 104 | } 105 | 106 | // event is the cache event (add, hit or delete). 107 | type event uint8 108 | 109 | const ( 110 | eventWrite event = iota 111 | eventAccess 112 | eventDelete 113 | eventClose 114 | ) 115 | 116 | type entryEvent struct { 117 | entry *entry 118 | event event 119 | } 120 | 121 | // cache is a data structure for cache entries. 122 | type cache struct { 123 | size int64 // Access atomically - must be aligned on 32-bit 124 | segs [segmentCount]sync.Map // map[Key]*entry 125 | } 126 | 127 | func (c *cache) get(k Key, h uint64) *entry { 128 | seg := c.segment(h) 129 | v, ok := seg.Load(k) 130 | if ok { 131 | return v.(*entry) 132 | } 133 | return nil 134 | } 135 | 136 | func (c *cache) getOrSet(v *entry) *entry { 137 | seg := c.segment(v.hash) 138 | en, ok := seg.LoadOrStore(v.key, v) 139 | if ok { 140 | return en.(*entry) 141 | } 142 | atomic.AddInt64(&c.size, 1) 143 | return nil 144 | } 145 | 146 | func (c *cache) delete(v *entry) { 147 | seg := c.segment(v.hash) 148 | seg.Delete(v.key) 149 | atomic.AddInt64(&c.size, -1) 150 | } 151 | 152 | func (c *cache) len() int { 153 | return int(atomic.LoadInt64(&c.size)) 154 | } 155 | 156 | func (c *cache) walk(fn func(*entry)) { 157 | for i := range c.segs { 158 | c.segs[i].Range(func(k, v interface{}) bool { 159 | fn(v.(*entry)) 160 | return true 161 | }) 162 | } 163 | } 164 | 165 | func (c *cache) segment(h uint64) *sync.Map { 166 | return &c.segs[h&segmentMask] 167 | } 168 | 169 | // policy is a cache policy. 170 | type policy interface { 171 | // init initializes the policy. 172 | init(cache *cache, maximumSize int) 173 | // write handles Write event for the entry. 174 | // It adds new entry and returns evicted entry if needed. 175 | write(entry *entry) *entry 176 | // access handles Access event for the entry. 177 | // It marks then entry recently accessed. 178 | access(entry *entry) 179 | // remove removes the entry. 180 | remove(entry *entry) *entry 181 | // iterate iterates all entries by their access time. 182 | iterate(func(entry *entry) bool) 183 | } 184 | 185 | func newPolicy(name string) policy { 186 | switch name { 187 | case "", "slru": 188 | return &slruCache{} 189 | case "lru": 190 | return &lruCache{} 191 | case "tinylfu": 192 | return &tinyLFU{} 193 | default: 194 | panic("cache: unsupported policy " + name) 195 | } 196 | } 197 | 198 | // recencyQueue manages cache entries by write time. 199 | type recencyQueue struct { 200 | ls list.List 201 | } 202 | 203 | func (w *recencyQueue) init(cache *cache, maximumSize int) { 204 | w.ls.Init() 205 | } 206 | 207 | func (w *recencyQueue) write(en *entry) *entry { 208 | if en.writeList == nil { 209 | en.writeList = w.ls.PushFront(en) 210 | } else { 211 | w.ls.MoveToFront(en.writeList) 212 | } 213 | return nil 214 | } 215 | 216 | func (w *recencyQueue) access(en *entry) { 217 | } 218 | 219 | func (w *recencyQueue) remove(en *entry) *entry { 220 | if en.writeList == nil { 221 | return en 222 | } 223 | w.ls.Remove(en.writeList) 224 | en.writeList = nil 225 | return en 226 | } 227 | 228 | func (w *recencyQueue) iterate(fn func(en *entry) bool) { 229 | iterateListFromBack(&w.ls, fn) 230 | } 231 | 232 | type discardingQueue struct{} 233 | 234 | func (discardingQueue) init(cache *cache, maximumSize int) { 235 | } 236 | 237 | func (discardingQueue) write(en *entry) *entry { 238 | return nil 239 | } 240 | 241 | func (discardingQueue) access(en *entry) { 242 | } 243 | 244 | func (discardingQueue) remove(en *entry) *entry { 245 | return en 246 | } 247 | 248 | func (discardingQueue) iterate(fn func(en *entry) bool) { 249 | } 250 | 251 | func iterateListFromBack(ls *list.List, fn func(en *entry) bool) { 252 | for el := ls.Back(); el != nil; { 253 | en := getEntry(el) 254 | prev := el.Prev() // Get Prev as fn can delete the entry. 255 | if !fn(en) { 256 | return 257 | } 258 | el = prev 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /lru_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | type lruTest struct { 9 | c cache 10 | lru lruCache 11 | slru slruCache 12 | t *testing.T 13 | } 14 | 15 | func (t *lruTest) assertLRULen(n int) { 16 | sz := cacheSize(&t.c) 17 | lz := t.lru.ls.Len() 18 | if sz != n || lz != n { 19 | t.t.Helper() 20 | t.t.Fatalf("unexpected data length: cache=%d list=%d, want: %d", sz, lz, n) 21 | } 22 | } 23 | 24 | func (t *lruTest) assertSLRULen(protected, probation int) { 25 | sz := cacheSize(&t.c) 26 | tz := t.slru.protectedLs.Len() 27 | bz := t.slru.probationLs.Len() 28 | if sz != protected+probation || tz != protected || bz != probation { 29 | t.t.Helper() 30 | t.t.Fatalf("unexpected data length: cache=%d protected=%d probation=%d, want: %d %d", sz, tz, bz, protected, probation) 31 | } 32 | } 33 | 34 | func (t *lruTest) assertEntry(en *entry, k int, v string, id uint8) { 35 | if en == nil { 36 | t.t.Helper() 37 | t.t.Fatalf("unexpected entry: %v", en) 38 | } 39 | ak := en.key.(int) 40 | av := en.getValue().(string) 41 | if ak != k || av != v || en.listID != id { 42 | t.t.Helper() 43 | t.t.Fatalf("unexpected entry: %+v, want: {key: %d, value: %s, listID: %d}", 44 | en, k, v, id) 45 | } 46 | } 47 | 48 | func (t *lruTest) assertLRUEntry(k int) { 49 | en := t.c.get(k, 0) 50 | if en == nil { 51 | t.t.Helper() 52 | t.t.Fatalf("entry not found in cache: key=%v", k) 53 | } 54 | ak := en.key.(int) 55 | av := en.getValue().(string) 56 | v := fmt.Sprintf("%d", k) 57 | if ak != k || av != v || en.listID != 0 { 58 | t.t.Helper() 59 | t.t.Fatalf("unexpected entry: %+v, want: {key: %v, value: %v, listID: %v}", en, k, v, 0) 60 | } 61 | } 62 | 63 | func (t *lruTest) assertSLRUEntry(k int, id uint8) { 64 | en := t.c.get(k, 0) 65 | if en == nil { 66 | t.t.Helper() 67 | t.t.Fatalf("entry not found in cache: key=%v", k) 68 | } 69 | ak := en.key.(int) 70 | av := en.getValue().(string) 71 | v := fmt.Sprintf("%d", k) 72 | if ak != k || av != v || en.listID != id { 73 | t.t.Helper() 74 | t.t.Fatalf("unexpected entry: %+v, want: {key: %v, value: %v, listID: %v}", en, k, v, id) 75 | } 76 | } 77 | 78 | func TestLRU(t *testing.T) { 79 | s := lruTest{t: t} 80 | s.lru.init(&s.c, 3) 81 | 82 | en := createLRUEntries(4) 83 | remEn := s.lru.write(en[0]) 84 | // 0 85 | if remEn != nil { 86 | t.Fatalf("unexpected entry removed: %v", remEn) 87 | } 88 | s.assertLRULen(1) 89 | s.assertLRUEntry(0) 90 | remEn = s.lru.write(en[1]) 91 | // 1 0 92 | if remEn != nil { 93 | t.Fatalf("unexpected entry removed: %v", remEn) 94 | } 95 | s.assertLRULen(2) 96 | s.assertLRUEntry(1) 97 | s.assertLRUEntry(0) 98 | 99 | s.lru.access(en[0]) 100 | // 0 1 101 | 102 | remEn = s.lru.write(en[2]) 103 | // 2 0 1 104 | if remEn != nil { 105 | t.Fatalf("unexpected entry removed: %+v", remEn) 106 | } 107 | s.assertLRULen(3) 108 | 109 | remEn = s.lru.write(en[3]) 110 | // 3 2 0 111 | s.assertEntry(remEn, 1, "1", 0) 112 | s.assertLRULen(3) 113 | s.assertLRUEntry(3) 114 | s.assertLRUEntry(2) 115 | s.assertLRUEntry(0) 116 | 117 | remEn = s.lru.remove(en[2]) 118 | // 3 0 119 | s.assertEntry(remEn, 2, "2", 0) 120 | s.assertLRULen(2) 121 | s.assertLRUEntry(3) 122 | s.assertLRUEntry(0) 123 | } 124 | 125 | func TestLRUWalk(t *testing.T) { 126 | s := lruTest{t: t} 127 | s.lru.init(&s.c, 5) 128 | 129 | entries := createLRUEntries(6) 130 | for _, e := range entries { 131 | s.lru.write(e) 132 | } 133 | // 5 4 3 2 1 134 | found := "" 135 | s.lru.iterate(func(en *entry) bool { 136 | found += en.getValue().(string) + " " 137 | return true 138 | }) 139 | if found != "1 2 3 4 5 " { 140 | t.Fatalf("unexpected entries: %v", found) 141 | } 142 | s.lru.access(entries[1]) 143 | s.lru.access(entries[5]) 144 | s.lru.access(entries[3]) 145 | // 3 5 1 4 2 146 | found = "" 147 | s.lru.iterate(func(en *entry) bool { 148 | found += en.getValue().(string) + " " 149 | if en.key.(int)%2 == 0 { 150 | s.lru.remove(en) 151 | } 152 | return en.key.(int) != 5 153 | }) 154 | if found != "2 4 1 5 " { 155 | t.Fatalf("unexpected entries: %v", found) 156 | } 157 | s.assertLRULen(3) 158 | s.assertLRUEntry(3) 159 | s.assertLRUEntry(5) 160 | s.assertLRUEntry(1) 161 | } 162 | 163 | func TestSegmentedLRU(t *testing.T) { 164 | s := lruTest{t: t} 165 | s.slru.init(&s.c, 3) 166 | s.slru.probationCap = 1 167 | s.slru.protectedCap = 2 168 | 169 | en := createLRUEntries(5) 170 | 171 | remEn := s.slru.write(en[0]) 172 | // - | 0 173 | if remEn != nil { 174 | t.Fatalf("unexpected entry removed: %v", remEn) 175 | } 176 | s.assertSLRULen(0, 1) 177 | s.assertSLRUEntry(0, probationSegment) 178 | 179 | remEn = s.slru.write(en[1]) 180 | // - | 1 0 181 | if remEn != nil { 182 | t.Fatalf("unexpected entry removed: %v", remEn) 183 | } 184 | s.assertSLRULen(0, 2) 185 | s.assertSLRUEntry(1, probationSegment) 186 | 187 | s.slru.access(en[1]) 188 | // 1 | 0 189 | s.assertSLRULen(1, 1) 190 | s.assertSLRUEntry(1, protectedSegment) 191 | s.assertSLRUEntry(0, probationSegment) 192 | 193 | s.slru.access(en[0]) 194 | // 0 1 | - 195 | s.assertSLRULen(2, 0) 196 | s.assertSLRUEntry(0, protectedSegment) 197 | s.assertSLRUEntry(1, protectedSegment) 198 | 199 | remEn = s.slru.write(en[2]) 200 | // 0 1 | 2 201 | if remEn != nil { 202 | t.Fatalf("unexpected entry removed: %+v", remEn) 203 | } 204 | s.assertSLRULen(2, 1) 205 | s.assertSLRUEntry(2, probationSegment) 206 | 207 | remEn = s.slru.write(en[3]) 208 | // 0 1 | 3 209 | s.assertSLRULen(2, 1) 210 | s.assertEntry(remEn, 2, "2", probationSegment) 211 | s.assertSLRUEntry(3, probationSegment) 212 | 213 | s.slru.access(en[3]) 214 | // 3 0 | 1 215 | s.assertSLRULen(2, 1) 216 | s.assertSLRUEntry(3, protectedSegment) 217 | 218 | remEn = s.slru.write(en[4]) 219 | // 3 0 | 4 220 | s.assertSLRULen(2, 1) 221 | s.assertEntry(remEn, 1, "1", probationSegment) 222 | 223 | remEn = s.slru.remove(en[0]) 224 | // 3 | 4 225 | s.assertEntry(remEn, 0, "0", protectedSegment) 226 | s.assertSLRULen(1, 1) 227 | s.assertSLRUEntry(3, protectedSegment) 228 | s.assertSLRUEntry(4, probationSegment) 229 | } 230 | 231 | func TestSLRUWalk(t *testing.T) { 232 | s := lruTest{t: t} 233 | s.slru.init(&s.c, 6) 234 | 235 | entries := createLRUEntries(10) 236 | for _, e := range entries { 237 | s.slru.write(e) 238 | } 239 | // | 9 8 7 6 5 4 240 | found := "" 241 | s.slru.iterate(func(en *entry) bool { 242 | found += en.getValue().(string) + " " 243 | return true 244 | }) 245 | if found != "4 5 6 7 8 9 " { 246 | t.Fatalf("unexpected entries: %v", found) 247 | } 248 | s.slru.access(entries[7]) 249 | s.slru.access(entries[5]) 250 | s.slru.access(entries[8]) 251 | // 8 5 7 | 9 6 4 252 | found = "" 253 | s.slru.iterate(func(en *entry) bool { 254 | found += en.getValue().(string) + " " 255 | if en.key.(int)%2 == 0 { 256 | s.slru.remove(en) 257 | } 258 | return en.key.(int) != 6 259 | }) 260 | if found != "7 5 8 4 6 " { 261 | t.Fatalf("unexpected entries: %v", found) 262 | } 263 | s.assertSLRULen(2, 1) 264 | s.assertSLRUEntry(5, protectedSegment) 265 | s.assertSLRUEntry(7, protectedSegment) 266 | s.assertSLRUEntry(9, probationSegment) 267 | } 268 | 269 | func createLRUEntries(n int) []*entry { 270 | en := make([]*entry, n) 271 | for i := range en { 272 | en[i] = newEntry(i, fmt.Sprintf("%d", i), 0 /* unused */) 273 | } 274 | return en 275 | } 276 | -------------------------------------------------------------------------------- /local_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "errors" 5 | "math/rand" 6 | "sync" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestCache(t *testing.T) { 12 | data := map[string]int{"1": 1, "2": 2} 13 | 14 | wg := sync.WaitGroup{} 15 | c := New(withInsertionListener(func(Key, Value) { 16 | wg.Done() 17 | })) 18 | defer c.Close() 19 | 20 | wg.Add(len(data)) 21 | for k, v := range data { 22 | c.Put(k, v) 23 | } 24 | wg.Wait() 25 | 26 | for k, v := range data { 27 | val, ok := c.GetIfPresent(k) 28 | if !ok || val.(int) != v { 29 | t.Fatalf("unexpected value: %v (%v)", val, ok) 30 | } 31 | } 32 | 33 | keys := c.GetAllKeys() 34 | if len(keys) != len(data) { 35 | t.Fatalf("unexpected keys size: %v, want: %v", len(keys), len(data)) 36 | } else { 37 | for _, k := range keys { 38 | if _, ok := data[k.(string)]; !ok { 39 | t.Fatalf("unexpected key: %v", k) 40 | } 41 | } 42 | } 43 | 44 | values := c.GetAllValues() 45 | if len(values) != len(data) { 46 | t.Fatalf("unexpected values size: %v, want: %v", len(values), len(data)) 47 | } else { 48 | for _, v := range values { 49 | found := false 50 | for _, val := range data { 51 | if v.(int) == val { 52 | found = true 53 | break 54 | } 55 | } 56 | if !found { 57 | t.Fatalf("unexpected value: %v", v) 58 | } 59 | } 60 | } 61 | 62 | allValues := c.GetAll() 63 | if len(allValues) != len(data) { 64 | t.Fatalf("unexpected all entries size: %v, want: %v", len(allValues), len(data)) 65 | } else { 66 | for k, v := range allValues { 67 | if val, ok := data[k.(string)]; !ok { 68 | t.Fatalf("unexpected key: %v", k) 69 | } else if val != v.(int) { 70 | t.Fatalf("unexpected value: %v, want: %v", v, val) 71 | } 72 | } 73 | } 74 | 75 | size := c.Size() 76 | if size != len(data) { 77 | t.Fatalf("unexpected size: %v, want: %v", size, len(data)) 78 | } 79 | } 80 | 81 | func TestMaximumSize(t *testing.T) { 82 | max := 10 83 | wg := sync.WaitGroup{} 84 | insFunc := func(k Key, v Value) { 85 | wg.Done() 86 | } 87 | c := New(WithMaximumSize(max), withInsertionListener(insFunc)).(*localCache) 88 | defer c.Close() 89 | 90 | wg.Add(max) 91 | for i := 0; i < max; i++ { 92 | c.Put(i, i) 93 | } 94 | wg.Wait() 95 | n := cacheSize(&c.cache) 96 | if n != max { 97 | t.Fatalf("unexpected cache size: %v, want: %v", n, max) 98 | } 99 | c.onInsertion = nil 100 | for i := 0; i < 2*max; i++ { 101 | k := rand.Intn(2 * max) 102 | c.Put(k, k) 103 | time.Sleep(time.Duration(i+1) * time.Millisecond) 104 | n = cacheSize(&c.cache) 105 | if n != max { 106 | t.Fatalf("unexpected cache size: %v, want: %v", n, max) 107 | } 108 | } 109 | } 110 | 111 | func TestRemovalListener(t *testing.T) { 112 | removed := make(map[Key]int) 113 | wg := sync.WaitGroup{} 114 | remFunc := func(k Key, v Value) { 115 | removed[k] = v.(int) 116 | wg.Done() 117 | } 118 | insFunc := func(k Key, v Value) { 119 | wg.Done() 120 | } 121 | max := 3 122 | c := New(WithMaximumSize(max), WithRemovalListener(remFunc), 123 | withInsertionListener(insFunc)) 124 | defer c.Close() 125 | 126 | wg.Add(max + 2) 127 | for i := 1; i < max+2; i++ { 128 | c.Put(i, i) 129 | } 130 | wg.Wait() 131 | 132 | if len(removed) != 1 || removed[1] != 1 { 133 | t.Fatalf("unexpected removed entries: %+v", removed) 134 | } 135 | 136 | wg.Add(1) 137 | c.Invalidate(3) 138 | wg.Wait() 139 | if len(removed) != 2 || removed[3] != 3 { 140 | t.Fatalf("unexpected removed entries: %+v", removed) 141 | } 142 | wg.Add(2) 143 | c.InvalidateAll() 144 | wg.Wait() 145 | if len(removed) != 4 || removed[2] != 2 || removed[4] != 4 { 146 | t.Fatalf("unexpected removed entries: %+v", removed) 147 | } 148 | } 149 | 150 | func TestClose(t *testing.T) { 151 | removed := 0 152 | wg := sync.WaitGroup{} 153 | remFunc := func(Key, Value) { 154 | removed++ 155 | wg.Done() 156 | } 157 | insFunc := func(Key, Value) { 158 | wg.Done() 159 | } 160 | c := New(WithRemovalListener(remFunc), withInsertionListener(insFunc)) 161 | n := 10 162 | wg.Add(n) 163 | for i := 0; i < n; i++ { 164 | c.Put(i, i) 165 | } 166 | wg.Wait() 167 | wg.Add(n) 168 | c.Close() 169 | wg.Wait() 170 | if removed != n { 171 | t.Fatalf("unexpected removed: %d", removed) 172 | } 173 | } 174 | 175 | func TestLoadingCache(t *testing.T) { 176 | loadCount := 0 177 | loader := func(k Key) (Value, error) { 178 | loadCount++ 179 | if k.(int)%2 != 0 { 180 | return nil, errors.New("odd") 181 | } 182 | return k, nil 183 | } 184 | wg := sync.WaitGroup{} 185 | insFunc := func(Key, Value) { 186 | wg.Done() 187 | } 188 | c := NewLoadingCache(loader, withInsertionListener(insFunc)) 189 | defer c.Close() 190 | wg.Add(1) 191 | v, err := c.Get(2) 192 | if err != nil { 193 | t.Fatal(err) 194 | } 195 | if v.(int) != 2 { 196 | t.Fatalf("unexpected get: %v", v) 197 | } 198 | if loadCount != 1 { 199 | t.Fatalf("unexpected load count: %v", loadCount) 200 | } 201 | wg.Wait() 202 | v, err = c.Get(2) 203 | if err != nil { 204 | t.Fatal(err) 205 | } 206 | if v.(int) != 2 { 207 | t.Fatalf("unexpected get: %v", v) 208 | } 209 | if loadCount != 1 { 210 | t.Fatalf("unexpected load count: %v", loadCount) 211 | } 212 | v, err = c.Get(1) 213 | if err == nil || err.Error() != "odd" { 214 | t.Fatalf("expected error: %v", err) 215 | } 216 | // Should not insert 217 | wg.Wait() 218 | } 219 | 220 | func simpleLoader(k Key) (Value, error) { 221 | return k, nil 222 | } 223 | 224 | func TestCacheStats(t *testing.T) { 225 | wg := sync.WaitGroup{} 226 | insFunc := func(Key, Value) { 227 | wg.Done() 228 | } 229 | c := NewLoadingCache(simpleLoader, withInsertionListener(insFunc)) 230 | defer c.Close() 231 | 232 | wg.Add(1) 233 | _, err := c.Get("x") 234 | if err != nil { 235 | t.Fatal(err) 236 | } 237 | var st Stats 238 | c.Stats(&st) 239 | if st.MissCount != 1 || st.LoadSuccessCount != 1 || st.TotalLoadTime <= 0 { 240 | t.Fatalf("unexpected stats: %+v", st) 241 | } 242 | wg.Wait() 243 | _, err = c.Get("x") 244 | if err != nil { 245 | t.Fatal(err) 246 | } 247 | c.Stats(&st) 248 | if st.HitCount != 1 { 249 | t.Fatalf("unexpected stats: %+v", st) 250 | } 251 | } 252 | 253 | func TestExpireAfterAccess(t *testing.T) { 254 | wg := sync.WaitGroup{} 255 | fn := func(k Key, v Value) { 256 | wg.Done() 257 | } 258 | mockTime := newMockTime() 259 | currentTime = mockTime.now 260 | c := New(WithExpireAfterAccess(1*time.Second), WithRemovalListener(fn), 261 | withInsertionListener(fn)).(*localCache) 262 | defer c.Close() 263 | 264 | wg.Add(1) 265 | c.Put(1, 1) 266 | wg.Wait() 267 | 268 | mockTime.add(1 * time.Second) 269 | wg.Add(2) 270 | c.Put(2, 2) 271 | c.Put(3, 3) 272 | wg.Wait() 273 | n := cacheSize(&c.cache) 274 | if n != 3 { 275 | wg.Add(n) 276 | t.Fatalf("unexpected cache size: %d, want: %d", n, 3) 277 | } 278 | 279 | mockTime.add(1 * time.Nanosecond) 280 | wg.Add(2) 281 | c.Put(4, 4) 282 | wg.Wait() 283 | n = cacheSize(&c.cache) 284 | wg.Add(n) 285 | if n != 3 { 286 | t.Fatalf("unexpected cache size: %d, want: %d", n, 3) 287 | } 288 | _, ok := c.GetIfPresent(1) 289 | if ok { 290 | t.Fatalf("unexpected entry status: %v, want: %v", ok, false) 291 | } 292 | } 293 | 294 | func TestExpireAfterWrite(t *testing.T) { 295 | loadCount := 0 296 | loader := func(k Key) (Value, error) { 297 | loadCount++ 298 | return loadCount, nil 299 | } 300 | wg := sync.WaitGroup{} 301 | insFunc := func(Key, Value) { 302 | wg.Done() 303 | } 304 | mockTime := newMockTime() 305 | currentTime = mockTime.now 306 | c := NewLoadingCache(loader, WithExpireAfterWrite(1*time.Second), 307 | withInsertionListener(insFunc)) 308 | defer c.Close() 309 | // New value 310 | wg.Add(1) 311 | v, err := c.Get("refresh") 312 | if err != nil { 313 | t.Fatal(err) 314 | } 315 | wg.Wait() 316 | if v.(int) != 1 || loadCount != 1 { 317 | t.Fatalf("unexpected load count: %v value=%v, want: %v value=%v", loadCount, v, 1, 1) 318 | } 319 | // Within 1s, the value should not yet expired. 320 | mockTime.add(1 * time.Second) 321 | v, err = c.Get("refresh") 322 | if err != nil { 323 | t.Fatal(err) 324 | } 325 | if v.(int) != 1 || loadCount != 1 { 326 | t.Fatalf("unexpected load count: %v value=%v, want: %v value=%v", loadCount, v, 1, 1) 327 | } 328 | // After 1s, the value should be expired and refresh triggered. 329 | mockTime.add(1 * time.Nanosecond) 330 | wg.Add(1) 331 | v, err = c.Get("refresh") 332 | if err != nil { 333 | t.Fatal(err) 334 | } 335 | wg.Wait() 336 | if v.(int) != 1 || loadCount != 2 { 337 | t.Fatalf("unexpected load count: %v value=%v, want: %v value=%v", loadCount, v, 2, 1) 338 | } 339 | // New value is loaded. 340 | v, err = c.Get("refresh") 341 | if err != nil { 342 | t.Fatal(err) 343 | } 344 | if v.(int) != 2 || loadCount != 2 { 345 | t.Fatalf("unexpected load count: %v value=%v, want: %v value=%v", loadCount, v, 2, 2) 346 | } 347 | } 348 | 349 | func TestRefreshAterWrite(t *testing.T) { 350 | var mutex sync.Mutex 351 | loaded := make(map[int]int) 352 | loader := func(k Key) (Value, error) { 353 | mutex.Lock() 354 | n := loaded[k.(int)] 355 | n++ 356 | loaded[k.(int)] = n 357 | mutex.Unlock() 358 | return n, nil 359 | } 360 | wg := sync.WaitGroup{} 361 | insFunc := func(Key, Value) { 362 | wg.Done() 363 | } 364 | mockTime := newMockTime() 365 | currentTime = mockTime.now 366 | c := NewLoadingCache(loader, WithExpireAfterAccess(4*time.Second), WithRefreshAfterWrite(2*time.Second), 367 | WithReloader(&syncReloader{loader}), withInsertionListener(insFunc)) 368 | defer c.Close() 369 | 370 | wg.Add(3) 371 | v, err := c.Get(1) 372 | if err != nil || v.(int) != 1 { 373 | t.Fatalf("unexpected get: %v %v", v, err) 374 | } 375 | // 3s 376 | mockTime.add(3 * time.Second) 377 | v, err = c.Get(2) 378 | if err != nil || v.(int) != 1 { 379 | t.Fatalf("unexpected get: %v %v", v, err) 380 | } 381 | wg.Wait() 382 | if loaded[1] != 2 || loaded[2] != 1 { 383 | t.Fatalf("unexpected loaded: %v", loaded) 384 | } 385 | v, err = c.Get(1) 386 | if err != nil || v.(int) != 2 { 387 | t.Fatalf("unexpected get: %v %v", v, err) 388 | } 389 | // 8s 390 | mockTime.add(5 * time.Second) 391 | wg.Add(1) 392 | v, err = c.Get(1) 393 | if err != nil || v.(int) != 3 { 394 | t.Fatalf("unexpected get: %v %v", v, err) 395 | } 396 | } 397 | 398 | func TestRefreshIfModifiedAfter(t *testing.T) { 399 | loadCount := 0 400 | loader := func(k Key) (Value, error) { 401 | loadCount++ 402 | return loadCount, nil 403 | } 404 | wg := sync.WaitGroup{} 405 | insFunc := func(Key, Value) { 406 | wg.Done() 407 | } 408 | mockTime := newMockTime() 409 | currentTime = mockTime.now 410 | c := NewLoadingCache(loader, WithRefreshAfterWrite(1*time.Minute), 411 | WithReloader(&syncReloader{loader}), withInsertionListener(insFunc)) 412 | defer c.Close() 413 | 414 | key := "mykey" 415 | 416 | // 1. Entry does not exist, should load synchronously. 417 | wg.Add(1) 418 | c.RefreshIfModifiedAfter(key, mockTime.now().UnixNano()) 419 | wg.Wait() 420 | 421 | if loadCount != 1 { 422 | t.Fatalf("expected loadCount to be 1 after initial load, got %d", loadCount) 423 | } 424 | 425 | // 2. Entry exists, but modifiedTime is not after entry's write time. Should not refresh. 426 | c.RefreshIfModifiedAfter(key, mockTime.now().Add(-1*time.Millisecond).UnixNano()) // modified time is before write time 427 | // No wg.Add/Wait as it should be synchronous and do nothing. 428 | if loadCount != 1 { 429 | t.Fatalf("expected loadCount to be 1 when not modified, got %d", loadCount) 430 | } 431 | 432 | // 3. Entry exists, and modifiedTime is after entry's write time. Should refresh. 433 | wg.Add(1) 434 | c.RefreshIfModifiedAfter(key, mockTime.now().Add(1*time.Millisecond).UnixNano()) // modified time is after write time 435 | wg.Wait() 436 | if loadCount != 2 { 437 | t.Fatalf("expected loadCount to be 2 after refresh, got %d", loadCount) 438 | } 439 | 440 | // 4. Manually put new value, then refresh with old modifiedTime, should not refresh. 441 | key2 := "mykey2" 442 | 443 | wg.Add(1) 444 | c.Put(key2, 1) 445 | wg.Wait() 446 | v, err := c.Get(key2) 447 | if err != nil || v.(int) != 1 { 448 | t.Fatalf("unexpected get: %v %v", v, err) 449 | } 450 | 451 | c.RefreshIfModifiedAfter(key2, mockTime.now().UnixNano()) // modified time is the same as write time 452 | // No wg.Add/Wait as it should be synchronous and do nothing. 453 | if loadCount != 2 { 454 | t.Fatalf("expected loadCount to be 2 when not modified, got %d", loadCount) 455 | } 456 | 457 | wg.Add(1) 458 | c.RefreshIfModifiedAfter(key, mockTime.now().Add(1*time.Millisecond).UnixNano()) // modified time is after write time 459 | wg.Wait() 460 | if loadCount != 3 { 461 | t.Fatalf("expected loadCount to be 3 after refresh, got %d", loadCount) 462 | } 463 | } 464 | 465 | func TestGetIfPresentExpired(t *testing.T) { 466 | wg := sync.WaitGroup{} 467 | insFunc := func(Key, Value) { 468 | wg.Done() 469 | } 470 | c := New(WithExpireAfterWrite(1*time.Second), withInsertionListener(insFunc)) 471 | mockTime := newMockTime() 472 | currentTime = mockTime.now 473 | 474 | v, ok := c.GetIfPresent(0) 475 | if ok { 476 | t.Fatalf("expect not present, actual: %v %v", v, ok) 477 | } 478 | wg.Add(1) 479 | c.Put(0, "0") 480 | v, ok = c.GetIfPresent(0) 481 | if !ok || v.(string) != "0" { 482 | t.Fatalf("expect present, actual: %v %v", v, ok) 483 | } 484 | wg.Wait() 485 | mockTime.add(2 * time.Second) 486 | v, ok = c.GetIfPresent(0) 487 | if ok { 488 | t.Fatalf("expect not present, actual: %v %v", v, ok) 489 | } 490 | } 491 | 492 | func TestLoadingAsyncReload(t *testing.T) { 493 | var val Value 494 | loader := func(k Key) (Value, error) { 495 | if val == nil { 496 | return nil, errors.New("nil") 497 | } 498 | return val, nil 499 | } 500 | mockTime := newMockTime() 501 | currentTime = mockTime.now 502 | c := NewLoadingCache(loader, WithExpireAfterWrite(5*time.Millisecond), 503 | WithReloader(&syncReloader{loader})) 504 | val = "a" 505 | v, err := c.Get(1) 506 | if err != nil || v != val { 507 | t.Fatalf("unexpected get %v %v", v, err) 508 | } 509 | mockTime.add(50 * time.Millisecond) 510 | val = "b" 511 | v, err = c.Get(1) 512 | if err != nil || v != val { 513 | t.Fatalf("unexpected get %v %v", v, err) 514 | } 515 | val = nil 516 | v, err = c.Get(2) 517 | if v != nil || err == nil || err.Error() != "nil" { 518 | t.Fatalf("expect error: actual %v %v", v, err) 519 | } 520 | } 521 | 522 | func TestLoadingRefresh(t *testing.T) { 523 | count := 0 524 | c := NewLoadingCache(func(key Key) (Value, error) { 525 | count++ 526 | return count, nil 527 | }) 528 | for i := 10; i > 0; i-- { 529 | v, _ := c.Get(1) 530 | if v.(int) != 1 { 531 | t.Fatalf("expect value loaded, actual: %v", v) 532 | } 533 | v, ok := c.GetIfPresent(1) 534 | if !ok || v != 1 { 535 | t.Fatalf("expect value present, actual: %v %v", v, ok) 536 | } 537 | } 538 | c.Refresh(2) 539 | v, _ := c.Get(2) 540 | if v.(int) != 2 { 541 | t.Fatalf("expect new value loaded, actual: %v", v) 542 | } 543 | c.Put(2, 3) 544 | v, _ = c.Get(2) 545 | if v.(int) != 3 { 546 | t.Fatalf("expect new value, actual: %v", v) 547 | } 548 | } 549 | 550 | func TestCloseMultiple(t *testing.T) { 551 | c := New() 552 | start := make(chan bool) 553 | const n = 10 554 | var wg sync.WaitGroup 555 | wg.Add(n) 556 | for i := 0; i < n; i++ { 557 | go func() { 558 | defer wg.Done() 559 | <-start 560 | c.Close() 561 | }() 562 | } 563 | close(start) 564 | wg.Wait() 565 | // Should not panic 566 | c.GetIfPresent(0) 567 | c.Put(1, 1) 568 | c.Invalidate(0) 569 | c.InvalidateAll() 570 | c.Close() 571 | } 572 | 573 | func BenchmarkGetSame(b *testing.B) { 574 | c := New() 575 | c.Put("*", "*") 576 | b.ReportAllocs() 577 | b.ResetTimer() 578 | b.RunParallel(func(pb *testing.PB) { 579 | for pb.Next() { 580 | c.GetIfPresent("*") 581 | } 582 | }) 583 | } 584 | 585 | func cacheSize(c *cache) int { 586 | length := 0 587 | c.walk(func(*entry) { 588 | length++ 589 | }) 590 | return length 591 | } 592 | 593 | // mockTime is used for tests which required current system time. 594 | type mockTime struct { 595 | mu sync.RWMutex 596 | value time.Time 597 | } 598 | 599 | func newMockTime() *mockTime { 600 | return &mockTime{ 601 | value: time.Now(), 602 | } 603 | } 604 | 605 | func (t *mockTime) add(d time.Duration) { 606 | t.mu.Lock() 607 | defer t.mu.Unlock() 608 | t.value = t.value.Add(d) 609 | } 610 | 611 | func (t *mockTime) now() time.Time { 612 | t.mu.RLock() 613 | defer t.mu.RUnlock() 614 | return t.value 615 | } 616 | 617 | type syncReloader struct { 618 | loaderFn LoaderFunc 619 | } 620 | 621 | func (s *syncReloader) Reload(k Key, v Value, setFn func(Value, error)) { 622 | v, err := s.loaderFn(k) 623 | setFn(v, err) 624 | } 625 | 626 | func (s *syncReloader) Close() error { 627 | return nil 628 | } 629 | -------------------------------------------------------------------------------- /local.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "sync" 5 | "sync/atomic" 6 | "time" 7 | ) 8 | 9 | const ( 10 | // Default maximum number of cache entries. 11 | maximumCapacity = 1 << 30 12 | // Buffer size of entry channels 13 | chanBufSize = 64 14 | // Maximum number of entries to be drained in a single clean up. 15 | drainMax = 16 16 | // Number of cache access operations that will trigger clean up. 17 | drainThreshold = 64 18 | ) 19 | 20 | // currentTime is an alias for time.Now, used for testing. 21 | var currentTime = time.Now 22 | 23 | // localCache is an asynchronous LRU cache. 24 | type localCache struct { 25 | // internal data structure 26 | cache cache // Must be aligned on 32-bit 27 | 28 | // user configurations 29 | expireAfterAccess time.Duration 30 | expireAfterWrite time.Duration 31 | refreshAfterWrite time.Duration 32 | policyName string 33 | 34 | onInsertion Func 35 | onRemoval Func 36 | 37 | loader LoaderFunc 38 | reloader Reloader 39 | stats StatsCounter 40 | 41 | // cap is the cache capacity. 42 | cap int 43 | 44 | // accessQueue is the cache retention policy, which manages entries by access time. 45 | accessQueue policy 46 | // writeQueue is for managing entries by write time. 47 | // It is only fulfilled when expireAfterWrite or refreshAfterWrite is set. 48 | writeQueue policy 49 | // events is the cache event queue for processEntries 50 | events chan entryEvent 51 | 52 | // readCount is a counter of the number of reads since the last write. 53 | readCount int32 54 | 55 | // for closing routines created by this cache. 56 | closing int32 57 | closeWG sync.WaitGroup 58 | } 59 | 60 | // newLocalCache returns a default localCache. 61 | // init must be called before this cache can be used. 62 | func newLocalCache() *localCache { 63 | return &localCache{ 64 | cap: maximumCapacity, 65 | cache: cache{}, 66 | stats: &statsCounter{}, 67 | } 68 | } 69 | 70 | // init initializes cache replacement policy after all user configuration properties are set. 71 | func (c *localCache) init() { 72 | c.accessQueue = newPolicy(c.policyName) 73 | c.accessQueue.init(&c.cache, c.cap) 74 | if c.expireAfterWrite > 0 || c.refreshAfterWrite > 0 { 75 | c.writeQueue = &recencyQueue{} 76 | } else { 77 | c.writeQueue = discardingQueue{} 78 | } 79 | c.writeQueue.init(&c.cache, c.cap) 80 | c.events = make(chan entryEvent, chanBufSize) 81 | 82 | c.closeWG.Add(1) 83 | go c.processEntries() 84 | } 85 | 86 | // Close implements io.Closer and always returns a nil error. 87 | // Caller would ensure the cache is not being used (reading and writing) before closing. 88 | func (c *localCache) Close() error { 89 | if atomic.CompareAndSwapInt32(&c.closing, 0, 1) { 90 | // Do not close events channel to avoid panic when cache is still being used. 91 | c.events <- entryEvent{nil, eventClose} 92 | // Wait for the goroutine to close this channel 93 | c.closeWG.Wait() 94 | } 95 | return nil 96 | } 97 | 98 | // GetIfPresent gets cached value from entries list and updates 99 | // last access time for the entry if it is found. 100 | func (c *localCache) GetIfPresent(k Key) (Value, bool) { 101 | en := c.cache.get(k, sum(k)) 102 | if en == nil { 103 | c.stats.RecordMisses(1) 104 | return nil, false 105 | } 106 | now := currentTime() 107 | if c.isExpired(en, now) { 108 | c.sendEvent(eventDelete, en) 109 | c.stats.RecordMisses(1) 110 | return nil, false 111 | } 112 | c.setEntryAccessTime(en, now) 113 | c.sendEvent(eventAccess, en) 114 | c.stats.RecordHits(1) 115 | return en.getValue(), true 116 | } 117 | 118 | // Put adds new entry to entries list. 119 | func (c *localCache) Put(k Key, v Value) { 120 | h := sum(k) 121 | en := c.cache.get(k, h) 122 | now := currentTime() 123 | if en == nil { 124 | en = newEntry(k, v, h) 125 | c.setEntryWriteTime(en, now) 126 | c.setEntryAccessTime(en, now) 127 | // Add to the cache directly so the new value is available immediately. 128 | // However, only do this within the cache capacity (approximately). 129 | if c.cap == 0 || c.cache.len() < c.cap { 130 | cen := c.cache.getOrSet(en) 131 | if cen != nil { 132 | cen.setValue(v) 133 | c.setEntryWriteTime(cen, now) 134 | en = cen 135 | } 136 | } 137 | } else { 138 | // Update value and send notice 139 | en.setValue(v) 140 | c.setEntryWriteTime(en, now) 141 | } 142 | c.sendEvent(eventWrite, en) 143 | } 144 | 145 | // Invalidate removes the entry associated with key k. 146 | func (c *localCache) Invalidate(k Key) { 147 | en := c.cache.get(k, sum(k)) 148 | if en != nil { 149 | en.setInvalidated(true) 150 | c.sendEvent(eventDelete, en) 151 | } 152 | } 153 | 154 | // InvalidateAll resets entries list. 155 | func (c *localCache) InvalidateAll() { 156 | c.cache.walk(func(en *entry) { 157 | en.setInvalidated(true) 158 | }) 159 | c.sendEvent(eventDelete, nil) 160 | } 161 | 162 | // Get returns value associated with k or call underlying loader to retrieve value 163 | // if it is not in the cache. The returned value is only cached when loader returns 164 | // nil error. 165 | func (c *localCache) Get(k Key) (Value, error) { 166 | en := c.cache.get(k, sum(k)) 167 | if en == nil { 168 | c.stats.RecordMisses(1) 169 | return c.load(k) 170 | } 171 | // Check if this entry needs to be refreshed 172 | now := currentTime() 173 | if c.isExpired(en, now) { 174 | if c.loader == nil { 175 | c.sendEvent(eventDelete, en) 176 | } else { 177 | // For loading cache, we do not delete entry but leave it to 178 | // the eviction policy, so users still can get the old value. 179 | c.setEntryAccessTime(en, now) 180 | c.refreshAsync(en) 181 | } 182 | c.stats.RecordMisses(1) 183 | } else { 184 | c.setEntryAccessTime(en, now) 185 | c.sendEvent(eventAccess, en) 186 | c.stats.RecordHits(1) 187 | } 188 | return en.getValue(), nil 189 | } 190 | 191 | // GetAllKeys returns all keys. 192 | func (c *localCache) GetAllKeys() []interface{} { 193 | keys := make([]interface{}, 0, c.cache.len()) 194 | c.cache.walk(func(en *entry) { 195 | if !en.getInvalidated() { 196 | keys = append(keys, en.key) 197 | } 198 | }) 199 | return keys 200 | } 201 | 202 | // GetAllValues returns all values. 203 | func (c *localCache) GetAllValues() []interface{} { 204 | values := make([]interface{}, 0, c.cache.len()) 205 | c.cache.walk(func(en *entry) { 206 | if !en.getInvalidated() { 207 | values = append(values, en.getValue()) 208 | } 209 | }) 210 | return values 211 | } 212 | 213 | // GetAll returns all entries. 214 | func (c *localCache) GetAll() map[interface{}]interface{} { 215 | var values = make(map[interface{}]interface{}, c.cache.len()) 216 | c.cache.walk(func(en *entry) { 217 | if !en.getInvalidated() { 218 | values[en.key] = en.getValue() 219 | } 220 | }) 221 | return values 222 | } 223 | 224 | // Size returns the number of entries in the cache. 225 | func (c *localCache) Size() int { 226 | return c.cache.len() 227 | } 228 | 229 | // Refresh asynchronously reloads value for Key if it existed, otherwise 230 | // it will synchronously load and block until it value is loaded. 231 | func (c *localCache) Refresh(k Key) { 232 | if c.loader == nil { 233 | return 234 | } 235 | en := c.cache.get(k, sum(k)) 236 | if en == nil { 237 | c.load(k) 238 | } else { 239 | c.refreshAsync(en) 240 | } 241 | } 242 | 243 | // RefreshIfModifiedAfter asynchronously reloads value for Key if the provided timestamp 244 | // indicates the data was modified after the entry was last loaded. If the entry doesn't exist, 245 | // it will synchronously load and block until the value is loaded. 246 | func (c *localCache) RefreshIfModifiedAfter(k Key, modifiedTime int64) { 247 | if c.loader == nil { 248 | return 249 | } 250 | en := c.cache.get(k, sum(k)) 251 | if en == nil { 252 | c.load(k) 253 | } else { 254 | // Only refresh if the data was modified after the entry was last loaded 255 | if modifiedTime > en.getWriteTime() { 256 | c.refreshAsync(en) 257 | } 258 | } 259 | } 260 | 261 | // Stats copies cache stats to t. 262 | func (c *localCache) Stats(t *Stats) { 263 | c.stats.Snapshot(t) 264 | } 265 | 266 | func (c *localCache) processEntries() { 267 | defer c.closeWG.Done() 268 | for e := range c.events { 269 | switch e.event { 270 | case eventWrite: 271 | c.write(e.entry) 272 | c.postWriteCleanup() 273 | case eventAccess: 274 | c.access(e.entry) 275 | c.postReadCleanup() 276 | case eventDelete: 277 | if e.entry == nil { 278 | c.removeAll() 279 | } else { 280 | c.remove(e.entry) 281 | } 282 | c.postReadCleanup() 283 | case eventClose: 284 | if c.reloader != nil { 285 | // Stop all refresh tasks. 286 | c.reloader.Close() 287 | } 288 | c.removeAll() 289 | return 290 | } 291 | } 292 | } 293 | 294 | // sendEvent sends event only when the cache is not closing/closed. 295 | func (c *localCache) sendEvent(typ event, en *entry) { 296 | if atomic.LoadInt32(&c.closing) == 0 { 297 | c.events <- entryEvent{en, typ} 298 | } 299 | } 300 | 301 | // This function must only be called from processEntries goroutine. 302 | func (c *localCache) write(en *entry) { 303 | ren := c.accessQueue.write(en) 304 | c.writeQueue.write(en) 305 | if c.onInsertion != nil { 306 | c.onInsertion(en.key, en.getValue()) 307 | } 308 | if ren != nil { 309 | c.writeQueue.remove(ren) 310 | // An entry has been evicted 311 | c.stats.RecordEviction() 312 | if c.onRemoval != nil { 313 | c.onRemoval(ren.key, ren.getValue()) 314 | } 315 | } 316 | } 317 | 318 | // removeAll remove all entries in the cache. 319 | // This function must only be called from processEntries goroutine. 320 | func (c *localCache) removeAll() { 321 | c.accessQueue.iterate(func(en *entry) bool { 322 | c.remove(en) 323 | return true 324 | }) 325 | } 326 | 327 | // remove removes the given element from the cache and entries list. 328 | // It also calls onRemoval callback if it is set. 329 | func (c *localCache) remove(en *entry) { 330 | ren := c.accessQueue.remove(en) 331 | c.writeQueue.remove(en) 332 | if ren != nil && c.onRemoval != nil { 333 | c.onRemoval(ren.key, ren.getValue()) 334 | } 335 | } 336 | 337 | // access moves the given element to the top of the entries list. 338 | // This function must only be called from processEntries goroutine. 339 | func (c *localCache) access(en *entry) { 340 | c.accessQueue.access(en) 341 | } 342 | 343 | // load uses current loader to synchronously retrieve value for k and adds new 344 | // entry to the cache only if loader returns a nil error. 345 | func (c *localCache) load(k Key) (Value, error) { 346 | if c.loader == nil { 347 | panic("cache loader function must be set") 348 | } 349 | // TODO: Poll the value instead when the entry is loading. 350 | start := currentTime() 351 | v, err := c.loader(k) 352 | now := currentTime() 353 | loadTime := now.Sub(start) 354 | if err != nil { 355 | c.stats.RecordLoadError(loadTime) 356 | return nil, err 357 | } 358 | en := newEntry(k, v, sum(k)) 359 | c.setEntryWriteTime(en, now) 360 | c.setEntryAccessTime(en, now) 361 | if c.cap == 0 || c.cache.len() < c.cap { 362 | cen := c.cache.getOrSet(en) 363 | if cen != nil { 364 | cen.setValue(v) 365 | c.setEntryWriteTime(cen, now) 366 | en = cen 367 | } 368 | } 369 | c.sendEvent(eventWrite, en) 370 | c.stats.RecordLoadSuccess(loadTime) 371 | return v, nil 372 | } 373 | 374 | // refreshAsync reloads value in a go routine or using custom executor if defined. 375 | func (c *localCache) refreshAsync(en *entry) bool { 376 | if en.setLoading(true) { 377 | // Only do refresh if it isn't running. 378 | if c.reloader == nil { 379 | go c.refresh(en) 380 | } else { 381 | c.reload(en) 382 | } 383 | return true 384 | } 385 | return false 386 | } 387 | 388 | // refresh reloads value for the given key. If loader returns an error, 389 | // that error will be omitted. Otherwise, the entry value will be updated. 390 | // This function would only be called by refreshAsync. 391 | func (c *localCache) refresh(en *entry) { 392 | defer en.setLoading(false) 393 | 394 | start := currentTime() 395 | v, err := c.loader(en.key) 396 | now := currentTime() 397 | loadTime := now.Sub(start) 398 | if err == nil { 399 | en.setValue(v) 400 | c.setEntryWriteTime(en, now) 401 | c.sendEvent(eventWrite, en) 402 | c.stats.RecordLoadSuccess(loadTime) 403 | } else { 404 | // TODO: Log error 405 | c.stats.RecordLoadError(loadTime) 406 | } 407 | } 408 | 409 | // reload uses user-defined reloader to reloads value. 410 | func (c *localCache) reload(en *entry) { 411 | start := currentTime() 412 | setFn := func(newValue Value, err error) { 413 | defer en.setLoading(false) 414 | now := currentTime() 415 | loadTime := now.Sub(start) 416 | if err == nil { 417 | en.setValue(newValue) 418 | c.setEntryWriteTime(en, now) 419 | c.sendEvent(eventWrite, en) 420 | c.stats.RecordLoadSuccess(loadTime) 421 | } else { 422 | c.stats.RecordLoadError(loadTime) 423 | } 424 | } 425 | c.reloader.Reload(en.key, en.getValue(), setFn) 426 | } 427 | 428 | // postReadCleanup is run after entry access/delete event. 429 | // This function must only be called from processEntries goroutine. 430 | func (c *localCache) postReadCleanup() { 431 | if atomic.AddInt32(&c.readCount, 1) > drainThreshold { 432 | atomic.StoreInt32(&c.readCount, 0) 433 | c.expireEntries() 434 | } 435 | } 436 | 437 | // postWriteCleanup is run after entry add event. 438 | // This function must only be called from processEntries goroutine. 439 | func (c *localCache) postWriteCleanup() { 440 | atomic.StoreInt32(&c.readCount, 0) 441 | c.expireEntries() 442 | } 443 | 444 | // expireEntries removes expired entries. 445 | func (c *localCache) expireEntries() { 446 | remain := drainMax 447 | now := currentTime() 448 | if c.expireAfterAccess > 0 { 449 | expiry := now.Add(-c.expireAfterAccess).UnixNano() 450 | c.accessQueue.iterate(func(en *entry) bool { 451 | if remain == 0 || en.getAccessTime() >= expiry { 452 | // Can stop as the entries are sorted by access time. 453 | // (the next entry is accessed more recently.) 454 | return false 455 | } 456 | // accessTime + expiry passed 457 | c.remove(en) 458 | c.stats.RecordEviction() 459 | remain-- 460 | return remain > 0 461 | }) 462 | } 463 | if remain > 0 && c.expireAfterWrite > 0 { 464 | expiry := now.Add(-c.expireAfterWrite).UnixNano() 465 | c.writeQueue.iterate(func(en *entry) bool { 466 | if remain == 0 || en.getWriteTime() >= expiry { 467 | return false 468 | } 469 | // writeTime + expiry passed 470 | c.remove(en) 471 | c.stats.RecordEviction() 472 | remain-- 473 | return remain > 0 474 | }) 475 | } 476 | if remain > 0 && c.loader != nil && c.refreshAfterWrite > 0 { 477 | expiry := now.Add(-c.refreshAfterWrite).UnixNano() 478 | c.writeQueue.iterate(func(en *entry) bool { 479 | if remain == 0 || en.getWriteTime() >= expiry { 480 | return false 481 | } 482 | // FIXME: This can cause deadlock if the custom executor runs refresh in current go routine. 483 | // The refresh function, when finish, will send to event channels. 484 | if c.refreshAsync(en) { 485 | // TODO: Maybe move this entry up? 486 | remain-- 487 | } 488 | return remain > 0 489 | }) 490 | } 491 | } 492 | 493 | func (c *localCache) isExpired(en *entry, now time.Time) bool { 494 | if en.getInvalidated() { 495 | return true 496 | } 497 | if c.expireAfterAccess > 0 && en.getAccessTime() < now.Add(-c.expireAfterAccess).UnixNano() { 498 | // accessTime + expiry passed 499 | return true 500 | } 501 | if c.expireAfterWrite > 0 && en.getWriteTime() < now.Add(-c.expireAfterWrite).UnixNano() { 502 | // writeTime + expiry passed 503 | return true 504 | } 505 | return false 506 | } 507 | 508 | func (c *localCache) needRefresh(en *entry, now time.Time) bool { 509 | if en.getLoading() { 510 | return false 511 | } 512 | if c.refreshAfterWrite > 0 { 513 | tm := en.getWriteTime() 514 | if tm > 0 && tm < now.Add(-c.refreshAfterWrite).UnixNano() { 515 | // writeTime + refresh passed 516 | return true 517 | } 518 | } 519 | return false 520 | } 521 | 522 | // setEntryAccessTime sets access time if needed. 523 | func (c *localCache) setEntryAccessTime(en *entry, now time.Time) { 524 | if c.expireAfterAccess > 0 { 525 | en.setAccessTime(now.UnixNano()) 526 | } 527 | } 528 | 529 | // setEntryWriteTime sets write time if needed. 530 | func (c *localCache) setEntryWriteTime(en *entry, now time.Time) { 531 | if c.expireAfterWrite > 0 || c.refreshAfterWrite > 0 { 532 | en.setWriteTime(now.UnixNano()) 533 | } 534 | } 535 | 536 | // New returns a local in-memory Cache. 537 | func New(options ...Option) Cache { 538 | c := newLocalCache() 539 | for _, opt := range options { 540 | opt(c) 541 | } 542 | c.init() 543 | return c 544 | } 545 | 546 | // NewLoadingCache returns a new LoadingCache with given loader function 547 | // and cache options. 548 | func NewLoadingCache(loader LoaderFunc, options ...Option) LoadingCache { 549 | c := newLocalCache() 550 | c.loader = loader 551 | for _, opt := range options { 552 | opt(c) 553 | } 554 | c.init() 555 | return c 556 | } 557 | 558 | // Option add options for default Cache. 559 | type Option func(c *localCache) 560 | 561 | // WithMaximumSize returns an Option which sets maximum size for the cache. 562 | // Any non-positive numbers is considered as unlimited. 563 | func WithMaximumSize(size int) Option { 564 | if size < 0 { 565 | size = 0 566 | } 567 | if size > maximumCapacity { 568 | size = maximumCapacity 569 | } 570 | return func(c *localCache) { 571 | c.cap = size 572 | } 573 | } 574 | 575 | // WithRemovalListener returns an Option to set cache to call onRemoval for each 576 | // entry evicted from the cache. 577 | func WithRemovalListener(onRemoval Func) Option { 578 | return func(c *localCache) { 579 | c.onRemoval = onRemoval 580 | } 581 | } 582 | 583 | // WithExpireAfterAccess returns an option to expire a cache entry after the 584 | // given duration without being accessed. 585 | func WithExpireAfterAccess(d time.Duration) Option { 586 | return func(c *localCache) { 587 | c.expireAfterAccess = d 588 | } 589 | } 590 | 591 | // WithExpireAfterWrite returns an option to expire a cache entry after the 592 | // given duration from creation. 593 | func WithExpireAfterWrite(d time.Duration) Option { 594 | return func(c *localCache) { 595 | c.expireAfterWrite = d 596 | } 597 | } 598 | 599 | // WithRefreshAfterWrite returns an option to refresh a cache entry after the 600 | // given duration. This option is only applicable for LoadingCache. 601 | func WithRefreshAfterWrite(d time.Duration) Option { 602 | return func(c *localCache) { 603 | c.refreshAfterWrite = d 604 | } 605 | } 606 | 607 | // WithStatsCounter returns an option which overrides default cache stats counter. 608 | func WithStatsCounter(st StatsCounter) Option { 609 | return func(c *localCache) { 610 | c.stats = st 611 | } 612 | } 613 | 614 | // WithPolicy returns an option which sets cache policy associated to the given name. 615 | // Supported policies are: lru, slru, tinylfu. 616 | func WithPolicy(name string) Option { 617 | return func(c *localCache) { 618 | c.policyName = name 619 | } 620 | } 621 | 622 | // WithReloader returns an option which sets reloader for a loading cache. 623 | // By default, each asynchronous reload is run in a go routine. 624 | // This option is only applicable for LoadingCache. 625 | func WithReloader(reloader Reloader) Option { 626 | return func(c *localCache) { 627 | c.reloader = reloader 628 | } 629 | } 630 | 631 | // withInsertionListener is used for testing. 632 | func withInsertionListener(onInsertion Func) Option { 633 | return func(c *localCache) { 634 | c.onInsertion = onInsertion 635 | } 636 | } 637 | --------------------------------------------------------------------------------