Skip to content
/ reqcache Public

ReqCache is a Go package for caching data within a single request context, allowing pre-allocation of memory blocks to reduce garbage collector load and caching objects by a unique key.

License

Notifications You must be signed in to change notification settings

n-r-w/reqcache

Repository files navigation

Go Reference Go Coverage CI Status Stability Go Report

ReqCache

ReqCache is a Go package for caching data within a single request context. Usable in web servers, gRPC servers, and other applications with a request-response lifecycle. It allows to:

  • Pre-allocate a single block of memory for creating multiple objects and reduce the load on the garbage collector
  • Cache objects by unique keys

Installation

To install the package, you need to run:

go get -u github.com/n-r-w/reqcache@latest

Benchmarks with and without batch allocation

BenchmarkWithoutBatchAllocation-32          747    1513720 ns/op 10240114 B/op    10002 allocs/op
BenchmarkWithBatchAllocation-32            4189     251629 ns/op     2598 B/op        3 allocs/op

Usage

Create a reqcache object

Cache object can be shared between multiple requests. It is recommended to create a single cache object for each unique type of object and key.

const (
    // number of pre-allocated objects. can be 0 if pre-allocation is not needed
    preAllocatedObjects = 1000
    // maximum number of objects in the cache. can be 0 if the cache not needed
    maxCacheSize = 10000
)

cache := reqcache.New[KeyType, ObjectType](
    preAllocatedObjects, maxCacheSize,    
    reqcache.WithLogger("cache name", logger), // used for logging/metrics pre-allocated memory overflow and cache hits   
)

Start a new session

NewSession adds a new session key to the context. It must be called once at the beginning of the request processing.

ctx = reqcache.NewSession(ctx)

End the session

EndSession removes all cache data from the reqcache object, associated with the session key.

defer cache.EndSession(ctx)

Create a new object

NewObject takes a pointer to object from the pre-allocated memory. If no pre-allocated memory is available (because too many objects have already been taken from the cache), a new object is created. In case of an object pool overflow, the logger will be called.

newObj := cache.NewObject(ctx)

Put an object into the cache

Put adds an object to the cache by a unique key.

cache.Put(ctx, key, newObj)

Get an object from the cache

Get returns an object from the cache by a unique key.

obj, ok := cache.Get(ctx, key)

Other methods

  • Exists checks if an object exists in the cache.
  • Delete removes an object from the cache.
  • GetOrFetch returns data from the cache or fetches it from the fetcher function (for example, from a database).
  • GetOrNew returns data from the cache or creates it and prepares with the prepare function.

Example

//nolint:gochecknoglobals,revive // example
package main

import (
    "context"
    "fmt"
    "log"
    "net/http"

    "github.com/n-r-w/reqcache"
)

type MyCache = reqcache.ReqCache[myKey, myObject]

func main() {
    const (
        objSize   = 5  // number of pre-allocated objects
        cacheSize = 10 // maximum number of objects in the cache

    )

    cache := reqcache.New[myKey, myObject](
        objSize, cacheSize,
        reqcache.WithLogger("example", &myLogger{}), // logging and metrics for object pool overflows
    )

    http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Prepare context for cache operations
        ctx := reqcache.NewSession(r.Context())

        // clean up the cache data after the request
        defer cache.EndSession(ctx)

        // Simulate some data processing
        workFunc1(ctx, cache)
        workFunc2(ctx, cache)

        _, _ = fmt.Fprintf(w, "request processed")

        log.Println("Request processed")
    }))

    log.Println("Server started at http://127.0.0.1:8080")

    if err := http.ListenAndServe(":8080", nil); err != nil { //nolint:gosec // no need for example
        log.Fatal(err)
    }
}

var (
    dataKey1 = myKey{Key1: "some-key11", Key2: "some-key12"}
    dataKey2 = myKey{Key1: "some-key21", Key2: "some-key22"}
    dataKey3 = myKey{Key1: "some-key31", Key2: "some-key32"}
    dataKey4 = myKey{Key1: "some-key41", Key2: "some-key42"}
)

func workFunc1(ctx context.Context, cache *MyCache) {
    // Create a new object from the pre-allocated memory
    newObj1 := cache.NewObject(ctx)
    // Set the value
    newObj1.Value = "Hello, World 1!"

    // Put the object into the cache
    cache.Put(ctx, dataKey1, newObj1)

    // Create another object manually
    newObj2 := &myObject{Value: "Hello, World 2!"}

    // Put the object into the cache
    cache.Put(ctx, dataKey2, newObj2)
}

func workFunc2(ctx context.Context, cache *MyCache) {
    // obj1 is cached
    if obj1, ok := cache.Get(ctx, dataKey1); ok {
        log.Println("obj1 is cached:", obj1.Value)
    }

    // obj3 is not cached. will be fetched and cached
    if obj3, err := cache.GetOrFetch(ctx, dataKey3,
        func(_ context.Context) (*myObject, error) {
            // fetching data
            return &myObject{Value: "Hello, World 3!"}, nil
        }); err != nil {
        log.Println(err.Error())
    } else {
        log.Println("obj3 is fetched:", obj3.Value)
    }

    workFunc3(ctx, cache)
}

func workFunc3(ctx context.Context, cache *MyCache) {
    // obj3 is cached
    if obj3, ok := cache.Get(ctx, dataKey3); ok {
        log.Println("obj3 is cached:", obj3.Value)
    }

    // obj4 is not cached. will be created by NewObject, prepared and cached
    if obj4, err := cache.GetOrNew(ctx, dataKey4,
        func(_ context.Context, obj *myObject) error {
            // preparing data
            obj.Value = "Hello, World 4!"
            return nil
        }); err != nil {
        log.Println(err.Error())
    } else {
        log.Println("obj4 is created by NewObject and prepared:", obj4.Value)
    }
}

// myLogger is a custom logger for ReqCache. It logs object pool overflows and cache hits.
// Not required.
type myLogger struct{}

func (m *myLogger) LogObjectPoolHitRatio(_ context.Context, name string, hit bool) {
    log.Printf("Object pool hit: %s, hit: %v", name, hit)
}

func (m *myLogger) LogCacheHitRatio(_ context.Context, name string, hit bool) {
    log.Printf("Cache hit: %s, hit: %v", name, hit)
}

// myKey is a custom key type for ReqCache.
type myKey struct {
    Key1 string
    Key2 string
}

// myObject is a custom object type for ReqCache.
type myObject struct {
    Value string
}

About

ReqCache is a Go package for caching data within a single request context, allowing pre-allocation of memory blocks to reduce garbage collector load and caching objects by a unique key.

Topics

Resources

Stars

Watchers

Forks