Code Monkey home page Code Monkey logo

ristretto's Introduction

Ristretto

Go Doc ci-ristretto-tests ci-ristretto-lint Coverage Status Go Report Card

Ristretto is a fast, concurrent cache library built with a focus on performance and correctness.

The motivation to build Ristretto comes from the need for a contention-free cache in Dgraph.

Features

  • High Hit Ratios - with our unique admission/eviction policy pairing, Ristretto's performance is best in class.
    • Eviction: SampledLFU - on par with exact LRU and better performance on Search and Database traces.
    • Admission: TinyLFU - extra performance with little memory overhead (12 bits per counter).
  • Fast Throughput - we use a variety of techniques for managing contention and the result is excellent throughput.
  • Cost-Based Eviction - any large new item deemed valuable can evict multiple smaller items (cost could be anything).
  • Fully Concurrent - you can use as many goroutines as you want with little throughput degradation.
  • Metrics - optional performance metrics for throughput, hit ratios, and other stats.
  • Simple API - just figure out your ideal Config values and you're off and running.

Status

Ristretto is production-ready. See Projects using Ristretto.

Table of Contents

Usage

Example

package main

import (
	"fmt"

	"github.com/dgraph-io/ristretto"
)

func main() {
	cache, err := ristretto.NewCache(&ristretto.Config{
		NumCounters: 1e7,     // number of keys to track frequency of (10M).
		MaxCost:     1 << 30, // maximum cost of cache (1GB).
		BufferItems: 64,      // number of keys per Get buffer.
	})
	if err != nil {
		panic(err)
	}

	// set a value with a cost of 1
	cache.Set("key", "value", 1)

	// wait for value to pass through buffers
	cache.Wait()

	// get value from cache
	value, found := cache.Get("key")
	if !found {
		panic("missing value")
	}
	fmt.Println(value)

	// del value from cache
	cache.Del("key")
}

Config

The Config struct is passed to NewCache when creating Ristretto instances (see the example above).

NumCounters int64

NumCounters is the number of 4-bit access counters to keep for admission and eviction. We've seen good performance in setting this to 10x the number of items you expect to keep in the cache when full.

For example, if you expect each item to have a cost of 1 and MaxCost is 100, set NumCounters to 1,000. Or, if you use variable cost values but expect the cache to hold around 10,000 items when full, set NumCounters to 100,000. The important thing is the number of unique items in the full cache, not necessarily the MaxCost value.

MaxCost int64

MaxCost is how eviction decisions are made. For example, if MaxCost is 100 and a new item with a cost of 1 increases total cache cost to 101, 1 item will be evicted.

MaxCost can also be used to denote the max size in bytes. For example, if MaxCost is 1,000,000 (1MB) and the cache is full with 1,000 1KB items, a new item (that's accepted) would cause 5 1KB items to be evicted.

MaxCost could be anything as long as it matches how you're using the cost values when calling Set.

BufferItems int64

BufferItems is the size of the Get buffers. The best value we've found for this is 64.

If for some reason you see Get performance decreasing with lots of contention (you shouldn't), try increasing this value in increments of 64. This is a fine-tuning mechanism and you probably won't have to touch this.

Metrics bool

Metrics is true when you want real-time logging of a variety of stats. The reason this is a Config flag is because there's a 10% throughput performance overhead.

OnEvict func(hashes [2]uint64, value interface{}, cost int64)

OnEvict is called for every eviction.

KeyToHash func(key interface{}) [2]uint64

KeyToHash is the hashing algorithm used for every key. If this is nil, Ristretto has a variety of defaults depending on the underlying interface type.

Note that if you want 128bit hashes you should use the full [2]uint64, otherwise just fill the uint64 at the 0 position and it will behave like any 64bit hash.

Cost func(value interface{}) int64

Cost is an optional function you can pass to the Config in order to evaluate item cost at runtime, and only for the Set calls that aren't dropped (this is useful if calculating item cost is particularly expensive and you don't want to waste time on items that will be dropped anyways).

To signal to Ristretto that you'd like to use this Cost function:

  1. Set the Cost field to a non-nil function.
  2. When calling Set for new items or item updates, use a cost of 0.

Benchmarks

The benchmarks can be found in https://github.com/dgraph-io/benchmarks/tree/master/cachebench/ristretto.

Hit Ratios

Search

This trace is described as "disk read accesses initiated by a large commercial search engine in response to various web search requests."

Database

This trace is described as "a database server running at a commercial site running an ERP application on top of a commercial database."

Looping

This trace demonstrates a looping access pattern.

CODASYL

This trace is described as "references to a CODASYL database for a one hour period."

Throughput

All throughput benchmarks were ran on an Intel Core i7-8700K (3.7GHz) with 16gb of RAM.

Mixed

Read

Write

Projects Using Ristretto

Below is a list of known projects that use Ristretto:

  • Badger - Embeddable key-value DB in Go
  • Dgraph - Horizontally scalable and distributed GraphQL database with a graph backend
  • Vitess - Database clustering system for horizontal scaling of MySQL
  • SpiceDB - Horizontally scalable permissions database

FAQ

How are you achieving this performance? What shortcuts are you taking?

We go into detail in the Ristretto blog post, but in short: our throughput performance can be attributed to a mix of batching and eventual consistency. Our hit ratio performance is mostly due to an excellent admission policy and SampledLFU eviction policy.

As for "shortcuts," the only thing Ristretto does that could be construed as one is dropping some Set calls. That means a Set call for a new item (updates are guaranteed) isn't guaranteed to make it into the cache. The new item could be dropped at two points: when passing through the Set buffer or when passing through the admission policy. However, this doesn't affect hit ratios much at all as we expect the most popular items to be Set multiple times and eventually make it in the cache.

Is Ristretto distributed?

No, it's just like any other Go library that you can import into your project and use in a single process.

ristretto's People

Contributors

ahsanbarkati avatar ajeetdsouza avatar all-seeing-code avatar aman-bansal avatar ashish-goswami avatar chewxy avatar ckarenz avatar codeofnode avatar evanj avatar joshua-goldstein avatar karlmcguire avatar kevburnsjr avatar mangalaman93 avatar manishrjain avatar martinmr avatar matthewmcneely avatar minhaj-shakeel avatar muesli avatar namanjain8 avatar pete-woods avatar poonai avatar psampaz avatar requilence avatar rmsafe avatar ryanfoxtyler avatar sesposito avatar singhvikash11 avatar tchaudhry91 avatar vmg avatar warashi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ristretto's Issues

Why is ristretto using so much memory?

package main

import (
	"fmt"
	"math/rand"
	"strconv"

	"github.com/dgraph-io/ristretto"
)

const (
	maxCnt  = 1e7
	maxLoop = 1e8
)

func main() {
	cahchePool, err := ristretto.NewCache(&ristretto.Config{
		NumCounters: 1 * 1e7,
		MaxCost:     200 * (1 << 20), // allocate 200M, but running it need 2GB, why?(when i run this program, it kill by OOM)
		BufferItems: 64,
		Metrics:     true,
	})

	if err != nil {
		panic(err)
	}

	key := "def48b5abb6388d3cbbb18e550f47b4cYB6eRI3cK1VN2qCEHp8kvuMuH20dq10cYDcG2e"
	for i := 0; i < maxLoop; i++ {
		suffix := strconv.Itoa(rand.Intn(maxCnt))
		cahchePool.Set(key+suffix, suffix, int64(len(key+suffix)))
		if (i % maxCnt) == 0 {
			fmt.Println(cahchePool.Metrics)
		}
	}

	cnt := 0
	for i := 0; i < maxCnt; i++ {
		if _, found := cahchePool.Get(key + strconv.Itoa(i)); found {
			cnt++
		}
	}
	fmt.Println("cnt:", cnt, "\n", cahchePool.Metrics)
}

Where are the releases?

I see you keep creating milestones and closing them, iterating the version. But where are the releases?

I'm sure you have your reasons, but would you be interested in doing git tags+github releases as well?

Ristretto causing memory corruption

After adding Ristretto to our micro-service orchestrator written in go, we have been having data corruption issues. The cached data has randomly been inaccurate, forcing me to remove this in-memory cache implementation. Not sure how I would go about logging this information. I can try and take some time over the weekend to create an example repo to exhibit the issue.

cmSketch not benefitting from four rows

I believe the commit f823dc4 broke the purpose of the cm sketch to have four rows. The bitwise-xor and mask are not creating the mix of indexes expected by the design. The same Increment/Estimate results, the same values, are achieved from a single row with or without the bitwise-xor. The earlier implementation seems to have been a good and very fast approximation to a distinct hash for each row except when the high 32 bits were all zero. One solution to fixing the earlier version could be to bitwise-or a single bit into the top 32 half. I can provide my testing and benchmarking on this offline if interested.

For small values of row length as used in the current unit tests, this doesn't matter. As the row length gets larger, the gap between the earlier algorithm and the current one widens and I believe becomes significant.

Please publish a Release

The current and only release is old. Go get will fetch this old version by default, without the TTL functionality. Go get will thus fetch a version that does not comply with the README.

Can you publish a release that matches the documentation?
Thank you.

MaxCost sometimes ignored

I have written a simple program that Gets and Sets entries in Ristretto. The program sets a MaxCost (in bytes) and passes a value for the cost to Set based on the size in bytes of the value.

Code can be found here: https://gist.github.com/erikdubbelboer/bd904c51f00079421fd3eb2a061a50c0

As you can see from the source Ristretto should be limited to 1GB of memory. The program doesn't do anything strange.

In most cases this program works fine and Ristretto limits the memory usage correctly. But in the case of the exact code as above the memory usage keeps growing.

Also keep in mind that runtime.GC() is run every second so it's not just uncollected allocations we seeing here. Not running runtime.GC() has the same result just more variation in the amount allocated.

This is the output when run for a couple of seconds (just go run ristretto-bug.go):

time: 1s alloc: 1033.72 MiB hits: 43 misses: 2614
time: 2s alloc: 1035.74 MiB hits: 71 misses: 3230
time: 3s alloc: 1038.76 MiB hits: 98 misses: 3210
time: 4s alloc: 1048.77 MiB hits: 87 misses: 3236
time: 5s alloc: 1065.79 MiB hits: 109 misses: 3229
...
time: 45s alloc: 5893.52 MiB hits: 642 misses: 2773
time: 46s alloc: 6049.54 MiB hits: 704 misses: 2689

The output of http://localhost:6060/debug/pprof/heap?debug=1 shows that all memory is allocated inside strings.Repeat.

My guess is that in some cases Ristretto keeps references to memory for entries that aren't part of the cache anymore.

Set up TeamCity

Migrate away from Github actions and use something else as discussed.

Add mechanism to clear entire cache.

I am adding ristretto to Dgraph but I am seeing a few issues that are caused because the cache is not being cleared after a DropAll operation.

I tried fixing this by recreating the entire cache every time that a DropAll op is processed, but this leads to out of memory issues. Ideally, ristretto has an API to efficiently clear the entire contents of the cache.

I'll take a look at the code and see if I can come up with something myself, but most likely I will need help from someone more familiar with the internals of ristretto.

`bench` fails to build

# github.com/dgraph-io/ristretto/bench
src/github.com/dgraph-io/ristretto/bench/bench.go:325:10: undefined: ristretto.PolicyLog

Found by gopkgs.io

Snapshot of current cache items

Hello there ๐Ÿ˜„ thanks for developing ristretto!
This is more of a general question rather than a issue: is there a way of creating a persistent snapshot for a running cache?
I've been trying with gob.NewEncoder(buffer).Encode(cache) but I have an error gob: type ristretto.Metrics has no exported fields which is (apparently) the intended behaviour of gob package (reference).
Any ideas if this is possible?
Thanks

Add ability to hook into the "eviction" event

Please provide another member onEvict in the Config struct which would be a function that would be called on eviction if it is present. This is needed so that users would be able to do some more, extra house-keeping from their side.

Race in cache.clear

The following race condition was seen in badger on PR dgraph-io/badger#1066

------- Stdout: -------
=== RUN   TestDropAllWithPendingTxn
badger 2019/10/21 08:34:34 INFO: All 0 tables opened in 0s
badger 2019/10/21 08:34:35 DEBUG: Flushing memtable, mt.size=34867 size of flushChan: 0
badger 2019/10/21 08:34:35 DEBUG: Storing value log head: {Fid:0 Len:30 Offset:4831937}
badger 2019/10/21 08:34:35 DEBUG: Storing value log head: {Fid:1 Len:30 Offset:4026620}
badger 2019/10/21 08:34:35 DEBUG: Flushing memtable, mt.size=34837 size of flushChan: 0
badger 2019/10/21 08:34:35 DEBUG: Storing value log head: {Fid:2 Len:30 Offset:3221294}
badger 2019/10/21 08:34:35 DEBUG: Flushing memtable, mt.size=34825 size of flushChan: 0
badger 2019/10/21 08:34:36 DEBUG: Storing value log head: {Fid:3 Len:30 Offset:2415968}
badger 2019/10/21 08:34:36 DEBUG: Flushing memtable, mt.size=34777 size of flushChan: 0
badger 2019/10/21 08:34:36 DEBUG: Storing value log head: {Fid:4 Len:30 Offset:1610642}
badger 2019/10/21 08:34:36 DEBUG: Flushing memtable, mt.size=34757 size of flushChan: 0
badger 2019/10/21 08:34:36 DEBUG: Storing value log head: {Fid:5 Len:30 Offset:805316}
badger 2019/10/21 08:34:36 DEBUG: Flushing memtable, mt.size=34753 size of flushChan: 0
badger 2019/10/21 08:34:36 INFO: Got compaction priority: {level:0 score:1.2 dropPrefix:[]}
badger 2019/10/21 08:34:36 INFO: Running for level: 0
badger 2019/10/21 08:34:36 DEBUG: LOG Compact. Added 3534 keys. Skipped 0 keys. Iteration took: 47.099272ms
badger 2019/10/21 08:34:36 DEBUG: Discard stats: map[]
badger 2019/10/21 08:34:36 INFO: LOG Compact 0->1, del 6 tables, add 1 tables, took 64.409113ms
badger 2019/10/21 08:34:36 INFO: Compaction for level: 0 DONE
badger 2019/10/21 08:34:37 DEBUG: Flushing memtable, mt.size=34895 size of flushChan: 0
badger 2019/10/21 08:34:37 DEBUG: Storing value log head: {Fid:5 Len:30 Offset:5637272}
badger 2019/10/21 08:34:37 DEBUG: Flushing memtable, mt.size=34775 size of flushChan: 0
badger 2019/10/21 08:34:37 DEBUG: Storing value log head: {Fid:6 Len:30 Offset:4831946}
badger 2019/10/21 08:34:37 DEBUG: Storing value log head: {Fid:7 Len:31 Offset:4026628}
badger 2019/10/21 08:34:37 DEBUG: Flushing memtable, mt.size=34753 size of flushChan: 0
badger 2019/10/21 08:34:38 DEBUG: Flushing memtable, mt.size=29077 size of flushChan: 0
badger 2019/10/21 08:34:38 DEBUG: Storing value log head: {Fid:8 Len:31 Offset:2415973}
badger 2019/10/21 08:34:38 DEBUG: Storing value log head: {Fid:9 Len:31 Offset:1610645}
badger 2019/10/21 08:34:38 DEBUG: Flushing memtable, mt.size=34745 size of flushChan: 0
badger 2019/10/21 08:34:38 DEBUG: Storing value log head: {Fid:10 Len:31 Offset:805317}
badger 2019/10/21 08:34:38 DEBUG: Flushing memtable, mt.size=34945 size of flushChan: 0
badger 2019/10/21 08:34:38 INFO: Got compaction priority: {level:0 score:1.2 dropPrefix:[]}
badger 2019/10/21 08:34:38 INFO: Running for level: 0
badger 2019/10/21 08:34:38 DEBUG: LOG Compact. Added 3603 keys. Skipped 0 keys. Iteration took: 40.49591ms
badger 2019/10/21 08:34:38 DEBUG: LOG Compact. Added 3367 keys. Skipped 0 keys. Iteration took: 29.324284ms
badger 2019/10/21 08:34:38 DEBUG: Discard stats: map[]
badger 2019/10/21 08:34:38 INFO: LOG Compact 0->1, del 7 tables, add 2 tables, took 84.556191ms
badger 2019/10/21 08:34:38 INFO: Compaction for level: 0 DONE
badger 2019/10/21 08:34:39 DEBUG: Flushing memtable, mt.size=34643 size of flushChan: 0
badger 2019/10/21 08:34:39 DEBUG: Storing value log head: {Fid:10 Len:31 Offset:5637285}
badger 2019/10/21 08:34:39 DEBUG: Flushing memtable, mt.size=34795 size of flushChan: 0
badger 2019/10/21 08:34:39 DEBUG: Storing value log head: {Fid:11 Len:31 Offset:4831957}
badger 2019/10/21 08:34:39 DEBUG: Flushing memtable, mt.size=34803 size of flushChan: 0
badger 2019/10/21 08:34:39 DEBUG: Storing value log head: {Fid:12 Len:31 Offset:4026629}
badger 2019/10/21 08:34:40 DEBUG: Storing value log head: {Fid:13 Len:31 Offset:3221301}
badger 2019/10/21 08:34:40 DEBUG: Flushing memtable, mt.size=34793 size of flushChan: 0
badger 2019/10/21 08:34:40 DEBUG: Storing value log head: {Fid:14 Len:31 Offset:2415973}
badger 2019/10/21 08:34:40 DEBUG: Flushing memtable, mt.size=34805 size of flushChan: 0
badger 2019/10/21 08:34:40 INFO: Got compaction priority: {level:0 score:1 dropPrefix:[]}
badger 2019/10/21 08:34:40 INFO: Running for level: 0
badger 2019/10/21 08:34:40 DEBUG: LOG Compact. Added 3603 keys. Skipped 0 keys. Iteration took: 44.412168ms
badger 2019/10/21 08:34:40 DEBUG: LOG Compact. Added 3607 keys. Skipped 0 keys. Iteration took: 39.613965ms
badger 2019/10/21 08:34:40 DEBUG: LOG Compact. Added 2705 keys. Skipped 0 keys. Iteration took: 28.006555ms
badger 2019/10/21 08:34:40 DEBUG: Discard stats: map[]
badger 2019/10/21 08:34:40 INFO: LOG Compact 0->1, del 7 tables, add 3 tables, took 127.699034ms
badger 2019/10/21 08:34:40 INFO: Compaction for level: 0 DONE
badger 2019/10/21 08:34:43 INFO: DropAll called. Blocking writes...
badger 2019/10/21 08:34:43 INFO: Writes flushed. Stopping compactions now...
badger 2019/10/21 08:34:43 INFO: Deleted 3 SSTables. Now deleting value logs...
badger 2019/10/21 08:34:43 INFO: Value logs deleted. Creating value log file: 0
badger 2019/10/21 08:34:43 INFO: Deleted 15 value log files. DropAll done.
==================
WARNING: DATA RACE
Write at 0x00c174a2fc50 by goroutine 40:
  github.com/dgraph-io/ristretto.(*Cache).collectMetrics()
      /home/pawan0201/go/src/github.com/dgraph-io/ristretto/cache.go:287 +0x55
  github.com/dgraph-io/ristretto.(*Cache).Clear()
      /home/pawan0201/go/src/github.com/dgraph-io/ristretto/cache.go:241 +0x14e
  github.com/dgraph-io/badger.(*DB).dropAll()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/db.go:1472 +0x556
  github.com/dgraph-io/badger.(*DB).DropAll()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/db.go:1428 +0x44
  github.com/dgraph-io/badger.TestDropAllWithPendingTxn.func4()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/managed_db_test.go:236 +0x6f

Previous read at 0x00c174a2fc50 by goroutine 253:
  github.com/dgraph-io/ristretto.(*Cache).Get()
      /home/pawan0201/go/src/github.com/dgraph-io/ristretto/cache.go:167 +0x103
  github.com/dgraph-io/badger/table.(*Table).block()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/table/table.go:364 +0x164c
  github.com/dgraph-io/badger/table.(*Iterator).next()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/table/iterator.go:296 +0x216
  github.com/dgraph-io/badger/table.(*Iterator).next()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/table/iterator.go:311 +0x6a6
  github.com/dgraph-io/badger/table.(*Iterator).Next()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/table/iterator.go:367 +0x70
  github.com/dgraph-io/badger/table.(*ConcatIterator).Next()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/table/iterator.go:485 +0x59
  github.com/dgraph-io/badger/y.(*MergeIterator).Next()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/y/iterator.go:216 +0xca
  github.com/dgraph-io/badger.(*Iterator).parseItem()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/iterator.go:647 +0x98e
  github.com/dgraph-io/badger.(*Iterator).Next()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/iterator.go:560 +0x20c
  github.com/dgraph-io/badger.TestDropAllWithPendingTxn.func3()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/managed_db_test.go:206 +0x157

Goroutine 40 (running) created at:
  github.com/dgraph-io/badger.TestDropAllWithPendingTxn()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/managed_db_test.go:233 +0x428
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:865 +0x163

Goroutine 253 (running) created at:
  github.com/dgraph-io/badger.TestDropAllWithPendingTxn()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/managed_db_test.go:198 +0x3cf
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:865 +0x163
==================
badger 2019/10/21 08:34:43 INFO: Resuming writes
badger 2019/10/21 08:34:44 INFO: Got compaction priority: {level:0 score:1.73 dropPrefix:[]}
--- FAIL: TestDropAllWithPendingTxn (9.40s)
    managed_db_test.go:216: Got number of keys: 10000
    managed_db_test.go:216: Got number of keys: 10000
    managed_db_test.go:220: Got error during key lookup: Key not found
    testing.go:809: race detected during execution of test

https://teamcity.dgraph.io/viewLog.html?buildId=29213&buildTypeId=Badger_UnitTests&tab=buildResultsDiv

Time aware LFU (windowing, temporary immunity)

Would you consider adding a time aware LFU, such that completely new entries can't have the probability of being evicted right after insertion?

Eg. New entries are given a 1 minute time frame to become useful. Potentially increasing survival rates of fresh entries.

keyhash and conflict all same

assume Key A, Key B

A(keyHash) A(Confilct) equal B(keyHash) B(Confilct)

In this case, do I have to double check the value every time?

Thanks

Race condition between Cache#Set() and Cache#Del()

Thanks for the great write-up on this exciting project. Per a note in HN from one of the maintainers, I'm inquiring here about a possible race condition between Set() and Del().

As of this writing, Set() asynchronously enqueues the actual write, where as Del() seems to synchronously update the internal store. In other words, Del() does not appear to be aware of the write buffering implemented by Set().

At first glance, this would seem to introduce a race condition for invalidation. Wouldn't it be possible for the following sequence to be incorrect?

_ := c.Set(k, v, cost)
c.Del(k)
got, ok := c.Get(k)

I think most or all client code would expect read-your-write consistency here, but it does seem like Get() could return a valid value for k despite the invalidation on the prior line.

Documented Behaviour of NumCounters Not Observed

Currently it is stated that

// For example, if you expect your cache to hold 1,000,000 items when full,
// NumCounters should be 10,000,000 (10x). Each counter takes up 4 bits, so
// keeping 10,000,000 counters would require 5MB of memory.

over here. However this behaviour isnt correct when utlising the cache. When utilising the cache with the following config

cache, err := ristretto.NewCache(&ristretto.Config{
		NumCounters: 10000,           // number of keys to track frequency of (10000).
		MaxCost:     1000, // maximum cost of cache (1000 objects).
		BufferItems: 64,             // number of keys per Get buffer.
		Cost: func(value interface{})int64{
			return 1
		},
	})

After the cache is filled with the first 1000 objects and is at its maximum capacity, any new items cannot be added into it. All my following Set fail. None of the old items currently in the cache are evicted to make way for new ones. This is however resolved if the NumCounters is set the same as the maximum size of the cache(1000 objects).

cache, err := ristretto.NewCache(&ristretto.Config{
		NumCounters: 1000,           // number of keys to track frequency of (1000).
		MaxCost:     1000, // maximum cost of cache (1000 objects).
		BufferItems: 64,             // number of keys per Get buffer.
		Cost: func(value interface{})int64{
			return 1
		},
	})

The above allowed the cache to work as normal, with majority of my Sets succeeding and my hit ratio staying at 0.94 instead of rapidly dropping. Is there anything else that needs to be done to get the cache working with 10x the number of keys or is this a gap in the documentation ?

Add ability to specify custom hashing functions

Currently here https://github.com/dgraph-io/ristretto/blob/master/z/z.go#L20 all of the possible types are written out. However, users like me may want to use a "custom" struct as a key but that does not work at the moment since it is not written there and then my whole program panics.

I propose the following: add some kind of "advanced" option to the library which is a function (func (key interface{}) (uint64, error)) which accepts the key (interface{}) and produces a uint64 hash. It then could be called in the default case. If an error happens - only then we should panic.

What do you think?

more structured access to metrics.

Metrics returns a private type that doesn't show it's methods in the GoDoc. Ideally I'd like to be able to expose the metrics to a prometheus collector. It looks like I could possibly parse some string formatted output, but, ideally, I'd be able to get a current set of structuted mwtrics from the call.

[PR #122] It's not possible to reuse a key after TTL eviction

Hello,

I've found an issue when playing with the new TTL feature.
It's not possible to reuse a key to update the cache with a new value.

The sample use case :

c, err := NewCache(...)

// Add a value with key 1 in the cache
c.SetWithTTL(1, 1, 1, 5*time.Second)
_, ok := c.Get(1) // return true

// some time later
// Add another value with key 1 in the cache
c.SetWithTTL(1, 1, 1, 5*time.Second)
_, ok := c.Get(1) // return false

I submit the following PR to correct this issue : #130

NewDefaultCache to encapsulate defaults

The current NewCache constructor requires the user to hard code ristretto defaults in their code.

Specifically, NumCounters is 100 x estimated items. The interface should allow the user to avoid hardcoding this into their code. Similarly, BufferItems: 64 is just a good number that works. The user should not need to hardcode this into client software either.

If any implementation changes in the future affect these currently valid defaults, the lack of encapsulation will require unnecessary changes to client code.

I suggest adding something like the following code (which also encapsulates the most likely default panic behavior on constructor errors). This allows the client to use defaults appropriate to their current version of ristretto. Also, it provides a simplified constructor that does not require client code knowledge of typically encapsulated implementation details.

As a small side benefit, the example code will also be cleaner and clearer.

// CountersPerItem is a good multiple of the number of expected items to track
const CountersPerItem = 100
// DefaultBufferItems is a good default size for Get buffers
const DefaultBufferItems = 64

func NewDefaultCache(expectedItems, avgCostPerItem int64) (cache *Cache) {
	
	cache, err := NewCache(&Config{
		NumCounters: expectedItems * CountersPerItem,
		MaxCost:     expectedItems * avgCostPerItem,
		BufferItems: DefaultBufferItems,  // number of keys per Get buffer.
	})
	if err != nil {
		panic(err)
	}
	return
} 

[Question] over time can TinyLFU make it more difficult to add cache entries?

It sounded to me that if the value calculated by new entries is higher than the lowest entry in a random sample, the new entry gets admitted. Otherwise not.

What I wonder is if it the value is calculated based off on the frequency from LFU. If so, won't the number of discarded items go up over time? Imagine if there are only reads for a minute or 10minutes, the frequency of most items increase and they gain a higher value than any new incoming entries.

I'm not confident in how the value is calculated, so I apologies if this is way off. Also curious if you have metrics for discarded (new items that was never added) metrics?

Problems with the expiration

Question 1:

We know that the expired data is stored in the map's map, The key generation strategy is related to time.

like

time:0s storageBucket:1 cleanupBucket:0
time:1s storageBucket:1 cleanupBucket:0
time:2s storageBucket:1 cleanupBucket:0
time:3s storageBucket:1 cleanupBucket:0
time:4s storageBucket:1 cleanupBucket:0
time:5s storageBucket:2 cleanupBucket:1
time:6s storageBucket:2 cleanupBucket:1
time:7s storageBucket:2 cleanupBucket:1
time:8s storageBucket:2 cleanupBucket:1
time:9s storageBucket:2 cleanupBucket:1
time:10s storageBucket:3 cleanupBucket:2
time:11s storageBucket:3 cleanupBucket:2
time:12s storageBucket:3 cleanupBucket:2
time:13s storageBucket:3 cleanupBucket:2
time:14s storageBucket:3 cleanupBucket:2
time:15s storageBucket:4 cleanupBucket:3

If my cleanup time set is not good, will some records be skipped?

eg: Every 15s, only found bucket3. bucket1, bucket2 is never be cleaned up again, and alway in the expiration map, is it right?

Question2:

cleanup code as below

func (m *expirationMap) cleanup(store store, policy policy, onEvict onEvictFunc) {

	now := time.Now()
	bucketNum := cleanupBucket(now)
	keys := m.buckets[bucketNum]
	delete(m.buckets, bucketNum)
	
	for key, conflict := range keys {
		// if key is expired del it
		// bla bla
	}
}

assume now want to clean bucket1, bucket1 has 100 keys, 60 expired, 40 not, according to the code, 60 keys will be deleted, and bucket1 deleted
the other 40keys, will in store, but never be clean up, it doesn't matter? why design like this?
At the same time, I also noticed that when I get, I only check whether it is expired, and I did not delete it at this time, Wouldn't it be better to delete?

Question3:
cleanupTicker is check every two seconds, Checked the bucket about 5 seconds ago, not all keys expire within 5s(eg. 1Day), So I guess that most of the keys will not be cleaned, but bucket is deleled. So I wonder if this design can be improved?

Thank you very much, please forgive my english

Run tests on Go 1.13

The ci pipeline introduced in #48 run tests only on go 1.12

Tests should run on latest stable version (1.13) also.

Race condition in setBuf

==================
WARNING: DATA RACE
Read at 0x00c019349648 by goroutine 35:
  github.com/dgraph-io/ristretto.(*Cache).Set()
      /home/pawan0201/go/pkg/mod/github.com/dgraph-io/[email protected]/cache.go:205 +0x1ed
  github.com/dgraph-io/badger/v2/table.(*Table).block()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/table/table.go:459 +0xe9a
  github.com/dgraph-io/badger/v2/table.(*Iterator).next()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/table/iterator.go:296 +0x216
  github.com/dgraph-io/badger/v2/table.(*Iterator).next()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/table/iterator.go:311 +0x699
  github.com/dgraph-io/badger/v2/table.(*Iterator).Next()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/table/iterator.go:367 +0x70
  github.com/dgraph-io/badger/v2/table.(*ConcatIterator).Next()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/table/iterator.go:485 +0x59
  github.com/dgraph-io/badger/v2/table.(*node).next()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/table/merge_iterator.go:82 +0xce
  github.com/dgraph-io/badger/v2/table.(*MergeIterator).Next()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/table/merge_iterator.go:158 +0x6a
  github.com/dgraph-io/badger/v2/table.(*node).next()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/table/merge_iterator.go:80 +0x73
  github.com/dgraph-io/badger/v2/table.(*MergeIterator).Next()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/table/merge_iterator.go:158 +0x6a
  github.com/dgraph-io/badger/v2.(*Iterator).parseItem()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/iterator.go:648 +0x933
  github.com/dgraph-io/badger/v2.(*Iterator).Next()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/iterator.go:561 +0x1fd
  github.com/dgraph-io/badger/v2.TestDropAllWithPendingTxn.func3()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/managed_db_test.go:218 +0x183

Previous write at 0x00c019349648 by goroutine 82:
  github.com/dgraph-io/ristretto.(*Cache).Clear()
      /home/pawan0201/go/pkg/mod/github.com/dgraph-io/[email protected]/cache.go:254 +0x9f
  github.com/dgraph-io/badger/v2.(*DB).dropAll()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/db.go:1511 +0x57e
  github.com/dgraph-io/badger/v2.(*DB).DropAll()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/db.go:1467 +0x47
  github.com/dgraph-io/badger/v2.TestDropAllWithPendingTxn.func4()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/managed_db_test.go:248 +0x7f

Goroutine 35 (running) created at:
  github.com/dgraph-io/badger/v2.TestDropAllWithPendingTxn()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/managed_db_test.go:210 +0x3e6
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:909 +0x199

Goroutine 82 (running) created at:
  github.com/dgraph-io/badger/v2.TestDropAllWithPendingTxn()
      /home/pawan0201/go/src/github.com/dgraph-io/badger/managed_db_test.go:245 +0x43f
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:909 +0x199
==================
badger 2020/02/05 10:11:51 INFO: Resuming writes
badger 2020/02/05 10:11:51 INFO: Got compaction priority: {level:0 score:1.73 dropPrefix:[]}
--- FAIL: TestDropAllWithPendingTxn (8.77s)
    managed_db_test.go:228: Got number of keys: 10000
    managed_db_test.go:224: Got error during value copy: Invalid value pointer offset: 3689714 greater than current offset: 20
    testing.go:853: race detected during execution of test

The line in question is

case c.setBuf <- i:
and

c.setBuf = make(chan *item, setBufSize)

https://teamcity.dgraph.io/viewLog.html?buildId=43004&buildTypeId=Badger_UnitTests revealed the race condition

Memory leak when initializing multiple instances

I have the following test case that creates a lot of cache instances, and somehow the memory can not be garbage collected, and therefore used up a lot of memory.

func TestMemLeak(t *testing.T) {
	type state struct {
		cache *ristretto.Cache
	}

	t.Run("test_cache", func(t *testing.T) {
		for i := 0; i < 10; i++ {

			engines := make([]*state, 0)
			for e := 0; e < 100; e++ {
				cache, err := ristretto.NewCache(&ristretto.Config{
					NumCounters: 1e7, // 10x num items on full cache
					MaxCost:     1e6, // 1 MB total size
					BufferItems: 64,  // recommended value
				})
				require.Nil(t, err)
				cache.Close()

				engines = append(engines, &state{
                                        // setting the cache as nil will stop the memory leak
                                        // cache: nil, 
					cache: cache,
				})
			}
		}
	})
}

I measured the memory usage with /usr/bin/time like this:
time -l go test mem_leak_test.go -v -count=1 -run=TestMemLeak

Running the above test will use 8056582144 maximum resident set size, 8GB memory was used.

However, if I set cache: nil then there is no memory leak, 157466624 maximum resident set size, only 150MB was used.

When using `string` as keys, there could be hash collisions, and thus `Get` could return the wrong data

This is more a "design question" than an actual "issue", however given its implications I think it is an important question which impacts either the design or the usage constraints.

Given that the KeyToHash (1) function supports string (and []byte), and it returns uint64, it is possible that there are hash collisions.

However the cache.Get (2) method doesn't check if the found entry (if any) actually has the given key. (In fact the store doesn't even support storing the key.)

(1) https://github.com/dgraph-io/ristretto/blob/master/z/z.go#L20
(2) https://github.com/dgraph-io/ristretto/blob/master/cache.go#L83

go get ristretto fails with error downloading bench/trace/ds1.arc.gz on windows

Ensure you do not have ristretto on your system and run go get -v github.com/dgraph-io/ristretto

$ go get -v github.com/dgraph-io/ristretto
go get: warning: modules disabled by GO111MODULE=auto in GOPATH/src;
        ignoring go.mod;
        see 'go help modules'
github.com/dgraph-io/ristretto (download)
# cd .; git clone https://github.com/dgraph-io/ristretto C:\Users\Ibrahim\go\src\github.com\dgraph-io\ristretto
Cloning into 'C:\Users\Ibrahim\go\src\github.com\dgraph-io\ristretto'...
Downloading bench/trace/ds1.arc.gz (7.4 MB)
Error downloading object: bench/trace/ds1.arc.gz (1c4eb88): Smudge error: Error downloading bench/trace/ds1.arc.gz (1c4eb8811b9840a6a006b5c6560e0a93e2dc978fae274378fb48ce2969e78366): batch response: This repository is over its data quota. Purchase more data packs to restore access.

Errors logged to C:\Users\Ibrahim\go\src\github.com\dgraph-io\ristretto\.git\lfs\logs\20190906T074835.9821484.log
Use `git lfs logs last` to view the log.
error: external filter 'git-lfs filter-process' failed
fatal: bench/trace/ds1.arc.gz: smudge filter lfs failed
warning: Clone succeeded, but checkout failed.
You can inspect what was checked out with 'git status'
and retry the checkout with 'git checkout -f HEAD'

package github.com/dgraph-io/ristretto: exit status 128

A similar failure was seen on https://ci.appveyor.com/project/manishrjain/badger/builds/27224437

TestRingLossy fails with drain error

TestRingLossy fails on master with drain error

Run

while go clean -testcache && go test -v -race -run='TestRingLossy' -timeout=1m; do :; done    

to reproduce the failure

=== RUN   TestRingLossy
--- FAIL: TestRingLossy (0.01s)
    ring_test.go:57: drain error
FAIL
exit status 1
FAIL	github.com/dgraph-io/ristretto	0.012s

ristretto/ring_test.go

Lines 41 to 59 in ae326c3

func TestRingLossy(t *testing.T) {
drainCount := 0
buffer := newRingBuffer(ringLossy, &ringConfig{
Consumer: &TestConsumer{
push: func(items []uint64) {
drainCount++
},
},
Capacity: 4,
})
buffer.Push(1)
buffer.Push(2)
buffer.Push(3)
buffer.Push(4)
time.Sleep(5 * time.Millisecond)
if drainCount == 0 {
t.Fatal("drain error")
}
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.