lrucache

package
v1.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 7, 2023 License: MIT Imports: 5 Imported by: 0

README

In-Memory LRU Cache for Golang Applications

This library can be embedded into your existing go applications and play the role Memcached or Redis might play for others. It is inspired by PHP Symfony's Cache Components, having a similar API. This library can not be used for persistance, is not properly tested yet and a bit special in a few ways described below (Especially with regards to the memory usage/size).

In addition to the interface described below, a http.Handler that can be used as middleware is provided as well.

  • Advantages:
    • Anything (interface{}) can be stored as value
    • As it lives in the application itself, no serialization or de-serialization is needed
    • As it lives in the application itself, no memory moving/networking is needed
    • The computation of a new value for a key does not block the full cache (only the key)
  • Disadvantages:
    • You have to provide a size estimate for every value
    • This size estimate should not change (i.e. values should not mutate)
    • The cache can only be accessed by one application

Example

// Go look at the godocs and ./cache_test.go for more documentation and examples

maxMemory := 1000
cache := lrucache.New(maxMemory)

bar = cache.Get("foo", func () (value interface{}, ttl time.Duration, size int) {
	return "bar", 10 * time.Second, len("bar")
}).(string)

// bar == "bar"

bar = cache.Get("foo", func () (value interface{}, ttl time.Duration, size int) {
	panic("will not be called")
}).(string)

Why does cache.Get take a function as argument?

Using the mechanism described below is optional, the second argument to Get can be nil and there is a Put function as well.

Because this library is meant to be used by multi threaded applications and the following would result in the same data being fetched twice if both goroutines run in parallel:

// This code shows what could happen with other cache libraries
c := lrucache.New(MAX_CACHE_ENTRIES)

for i := 0; i < 2; i++ {
    go func(){
        // This code will run twice in different goroutines,
        // it could overlap. As `fetchData` probably does some
        // I/O and takes a long time, the probability of both
        // goroutines calling `fetchData` is very high!
        url := "http://example.com/foo"
        contents := c.Get(url)
        if contents == nil {
            contents = fetchData(url)
            c.Set(url, contents)
        }

        handleData(contents.([]byte))
    }()
}

Here, if one wanted to make sure that only one of both goroutines fetches the data, the programmer would need to build his own synchronization. That would suck!

c := lrucache.New(MAX_CACHE_SIZE)

for i := 0; i < 2; i++ {
    go func(){
        url := "http://example.com/foo"
        contents := c.Get(url, func()(interface{}, time.Time, int) {
            // This closure will only be called once!
            // If another goroutine calls `c.Get` while this closure
            // is still being executed, it will wait.
            buf := fetchData(url)
            return buf, 100 * time.Second, len(buf)
        })

        handleData(contents.([]byte))
    }()
}

This is much better as less resources are wasted and synchronization is handled by the library. If it gets called, the call to the closure happens synchronously. While it is being executed, all other cache keys can still be accessed without having to wait for the execution to be done.

How Get works

The closure passed to Get will be called if the value asked for is not cached or expired. It should return the following values:

  • The value corresponding to that key and to be stored in the cache
  • The time to live for that value (how long until it expires and needs to be recomputed)
  • A size estimate

When maxMemory is reached, cache entries need to be evicted. Theoretically, it would be possible to use reflection on every value placed in the cache to get its exact size in bytes. This would be very expansive and slow though. Also, size can change. Instead of this library calculating the size in bytes, you, the user, have to provide a size for every value in whatever unit you like (as long as it is the same unit everywhere).

Suggestions on what to use as size: len(str) for strings, len(slice) * size_of_slice_type, etc.. It is possible to use 1 as size for every entry, in that case at most maxMemory entries will be in the cache at the same time.

Affects on GC

Because of the way a garbage collector decides when to run (explained in the runtime package), having large amounts of data sitting in your cache might increase the memory consumption of your process by two times the maximum size of the cache. You can decrease the target percentage to reduce the effect, but then you might have negative performance effects when your cache is not filled.

Documentation

Overview

Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg. All rights reserved. Use of this source code is governed by a MIT-style license that can be found in the LICENSE file.

Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg. All rights reserved. Use of this source code is governed by a MIT-style license that can be found in the LICENSE file.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewMiddleware

func NewMiddleware(maxmemory int, ttl time.Duration) func(http.Handler) http.Handler

gorilla/mux style middleware:

Types

type Cache

type Cache struct {
	// contains filtered or unexported fields
}

func New

func New(maxmemory int) *Cache

Return a new instance of a LRU In-Memory Cache. Read [the README](./README.md) for more information on what is going on with `maxmemory`.

func (*Cache) Del

func (c *Cache) Del(key string) bool

Remove the value at key `key` from the cache. Return true if the key was in the cache and false otherwise. It is possible that true is returned even though the value already expired. It is possible that false is returned even though the value will show up in the cache if this function is called on a key while that key is beeing computed.

func (*Cache) Get

func (c *Cache) Get(key string, computeValue ComputeValue) interface{}

Return the cached value for key `key` or call `computeValue` and store its return value in the cache. If called, the closure will be called synchronous and __shall not call methods on the same cache__ or a deadlock might ocure. If `computeValue` is nil, the cache is checked and if no entry was found, nil is returned. If another goroutine is currently computing that value, the result is waited for.

func (*Cache) Keys

func (c *Cache) Keys(f func(key string, val interface{}))

Call f for every entry in the cache. Some sanity checks and eviction of expired keys are done as well. The cache is fully locked for the complete duration of this call!

func (*Cache) Put

func (c *Cache) Put(key string, value interface{}, size int, ttl time.Duration)

Put a new value in the cache. If another goroutine is calling `Get` and computing the value, this function waits for the computation to be done before it overwrites the value.

type ComputeValue

type ComputeValue func() (value interface{}, ttl time.Duration, size int)

Type of the closure that must be passed to `Get` to compute the value in case it is not cached.

returned values are the computed value to be stored in the cache, the duration until this value will expire and a size estimate.

type HttpHandler

type HttpHandler struct {

	// Allows overriding the way the cache key is extracted
	// from the http request. The defailt is to use the RequestURI.
	CacheKey func(*http.Request) string
	// contains filtered or unexported fields
}

HttpHandler is can be used as HTTP Middleware in order to cache requests, for example static assets. By default, the request's raw URI is used as key and nothing else. Results with a status code other than 200 are cached with a TTL of zero seconds, so basically re-fetched as soon as the current fetch is done and a new request for that URI is done.

func NewHttpHandler

func NewHttpHandler(maxmemory int, ttl time.Duration, fetcher http.Handler) *HttpHandler

Returns a new caching HttpHandler. If no entry in the cache is found or it was too old, `fetcher` is called with a modified http.ResponseWriter and the response is stored in the cache. If `fetcher` sets the "Expires" header, the ttl is set appropriately (otherwise, the default ttl passed as argument here is used). `maxmemory` should be in the unit bytes.

func (*HttpHandler) ServeHTTP

func (h *HttpHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request)

Tries to serve a response to r from cache or calls next and stores the response to the cache for the next time.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL