cachefunk

package module
v0.4.1 Latest Latest
Warning

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

Go to latest
Published: Aug 28, 2025 License: MIT Imports: 24 Imported by: 0

README

cachefunk

Use wrapper functions to cache function output in golang.

Go Report Card Test Go Reference

Features

  • Currently supported cache adapters:
    • any GORM-supported database
    • in-memory caching
    • files on disk
  • Supports custom marshal / unmarshal: json, msgpack, string
  • Supports compression: zstd, gzip, brotli
  • Configurable TTL and TTL jitter
  • Configurable fallback to expired when downstream fails
  • Cleanup function for periodic removal of expired entries
  • Uses go generics, in IDE type checked parameters and result
  • Cache can be ignored, either by boolean or by ctx key

Getting Started

Dependencies
  • go version that supports generics (tested v1.23 and v1.24)
Installing

go get -u github.com/rohfle/cachefunk

Example

import (
	"fmt"
	"testing"
	"time"

	"github.com/rohfle/cachefunk"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)


func main() {
	db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}

	config := cachefunk.Config{
		Configs: {
			"hello": {
				TTL: 3600,
				// TTLJitter: 0,
				// FallbackToExpired: false,
				// BodyCompression: cachefunk.ZstdCompression,
				// BodyCodec: cachefunk.JSONCodec,
				// ParamsCodec: cachefunk.JSONParams,
			}
		}
	}
	storage := cachefunk.NewGORMStorage(db)
	cache := cachefunk.CacheFunk{
		Config: config,
		Storage: storage,
	}

	// ignoreCache is passed through to the target function for nested caching calls
	// All other arguments are passed in as a struct (HelloWorldParams)
	// The params argument and the return type must be serializable by the codec Marshal
	type HelloWorldParams struct {
		Name string
	}

	helloWorldRaw := func(ignoreCache bool, params *HelloWorldParams) (string, error) {
		return "Hello " + params.Name, nil
	}

	// Wrap the function
	HelloWorld := cachefunk.WrapWithIgnore(cache, "hello", helloWorldRaw)

	// First call will get value from wrapped function
	value, err := HelloWorld(false, &HelloWorldParams{
		Name: "bob",
	})
	fmt.Println("First call:", value, err)

	// Second call will get value from cache
	value, err = HelloWorld(false, &HelloWorldParams{
		Name: "bob",
	})
	fmt.Println("Second call:", value, err)
}

API

  • Wrap
  • WrapWithIgnore
  • WrapWithContext
  • Cache
  • CacheWithIgnore
  • CacheWithContext

Notes about timestamps

  • Timestamps store the time when the cached item was saved with jitter applied
  • It is easier to apply jitter to timestamps at save even though jitter TTL might change
  • The expire time is not stored because cache config TTL might change on subsequent runs
  • Cache items must be able to immediately expire and never expire, regardless of stored timestamp
  • Cache get calls should not expire items - only return no match in case subsequent retrieve fails
  • Any entry with a timestamp before the expire time is said to have expired
  • A TTL of 0 or TTLEntryImmediatelyExpires is used for immediate expiry (MaxTime, 9999-01-01)
  • A TTL of -1 or TTLEntryNeverExpires is used for no expiry (MinTime, 1970-01-01)

Version History

  • 0.4.1
    • Cache TTLEntryImmediatelyExpires entries when FallbackToExpired is set
    • Added lazy load of existing expired entries
    • Fallback to Expired
    • Changed Wrap to WrapWithIgnore, Cache to CacheWithIgnore
    • Removed ability set ignore cache ctx key
    • Ensure DiskStorage file paths are within the cache
  • 0.4.0
    • Complete rewrite
    • Compression and Codec methods are now per config key
    • Removed string / object specific functions, now unified type handling
    • Added zstd, brotli, msgpack support
    • Added warning log, DisableWarnings and EnableWarnings function
  • 0.3.0
    • Added disk cache
    • Changed from storing expire time to timestamp when entry was cached
    • Added gzip compression
    • Changed CacheResult to CacheObject, CacheWithContext to CacheObjectWithContext
    • Moved TTL configuration to cache initialization function
    • Removed TTL value for store indefinitely
    • Messed around with git version tags to try to erase history
  • 0.2.0
    • Created CacheResult, CacheString, CacheWithContext, CacheStringWithContext functions
  • 0.1.0
    • Initial release

License

© Rohan Fletcher 2025

This project is licensed under the MIT License - see the LICENSE file for details

Documentation

Overview

Package cachefunk provides caching wrappers for functions

Index

Examples

Constants

View Source
const TTLEntryImmediatelyExpires = 0

TTLEntryImmediatelyExpires means that cached values immediately expire All cache Sets will do nothing and all non-fallback Gets return no result

View Source
const TTLEntryNeverExpires = -1

TTLEntryNeverExpires means that cached values will never expire

Variables

View Source
var (
	ErrEntryNotFound = errors.New("cache entry not found")
	ErrEntryExpired  = errors.New("cache entry expired")
)
View Source
var BrotliCompression = &brotliCompression{}
View Source
var DefaultBodyCodec = JSONCodec
View Source
var DefaultBodyCompression = ZstdCompression
View Source
var DefaultDiskStoragePather = SHA256HexPather
View Source
var DefaultKeyConfig = &KeyConfig{
	TTL:               3600,
	TTLJitter:         300,
	FallbackToExpired: false,
	BodyCompression:   DefaultBodyCompression,
	BodyCodec:         DefaultBodyCodec,
	ParamCodec:        DefaultParamCodec,
}

DefaultKeyConfig holds the settings for any key that does not have config defined

View Source
var DefaultParamCodec = JSONParams
View Source
var GzipCompression = &gzipCompression{}
View Source
var JSONBase64Params = &jsonBase64Params{}
View Source
var JSONCodec = &jsonCodec{}
View Source
var JSONParams = &jsonParams{}
View Source
var MaxDate = time.Date(9999, 1, 1, 0, 0, 0, 0, time.UTC)

MaxDate is the latest time that cachefunk uses (9999-01-01)

View Source
var MinDate = time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)

MinDate is the earliest time that cachefunk uses (1970-01-01)

View Source
var MsgPackCodec = &msgpackCodec{}
View Source
var NoCompression = &noCompression{}
View Source
var StringCodec = &stringCodec{}
View Source
var TTLMax = int64(MaxDate.Sub(MinDate) / time.Second)
View Source
var ZstdCompression = &zstdCompression{}

Functions

func Cache

func Cache[Params any, ResultType any](
	cache *CacheFunk,
	key string,
	resolverFunc func(Params) (ResultType, error),
	params Params,
) (ResultType, error)

func CacheWithContext added in v0.4.0

func CacheWithContext[Params any, ResultType any](
	cache *CacheFunk,
	key string,
	resolverFunc func(context.Context, Params) (ResultType, error),
	ctx context.Context,
	params Params,
) (ResultType, error)

func CacheWithIgnore added in v0.4.1

func CacheWithIgnore[Params any, ResultType any](
	cache *CacheFunk,
	key string,
	resolverFunc func(bool, Params) (ResultType, error),
	ignoreCache bool,
	params Params,
) (ResultType, error)

func DisableWarnings added in v0.4.0

func DisableWarnings()

func EnableWarnings added in v0.4.0

func EnableWarnings()

func GetIgnoreCacheFromContext added in v0.4.1

func GetIgnoreCacheFromContext(ctx context.Context) bool

func SHA256Base64Pather added in v0.4.0

func SHA256Base64Pather(key string, params string) []string

func SHA256HexPather added in v0.4.0

func SHA256HexPather(key string, params string) []string

func SetIgnoreCacheInContext added in v0.4.1

func SetIgnoreCacheInContext(ctx context.Context, value bool) context.Context

func SetWarningLog added in v0.4.0

func SetWarningLog(logger *log.Logger)

SetWarningLog allows redirection of warning output

func Wrap added in v0.1.0

func Wrap[Params any, ResultType any](
	cache *CacheFunk,
	key string,
	retrieveFunc func(Params) (ResultType, error),
) func(Params) (ResultType, error)

Wrap type functions These don't work with type methods unfortunately

func WrapWithContext added in v0.2.0

func WrapWithContext[Params any, ResultType any](
	cache *CacheFunk,
	key string,
	retrieveFunc func(context.Context, Params) (ResultType, error),
) func(context.Context, Params) (ResultType, error)

func WrapWithIgnore added in v0.4.1

func WrapWithIgnore[Params any, ResultType any](
	cache *CacheFunk,
	key string,
	retrieveFunc func(bool, Params) (ResultType, error),
) func(bool, Params) (ResultType, error)

Types

type BodyCodec added in v0.4.0

type BodyCodec interface {
	Marshal(v any) ([]byte, error)
	Unmarshal(data []byte, v any) error
	String() string
}

type CacheEntry

type CacheEntry struct {
	ID              int64     `json:"id" gorm:"primaryKey"`
	Timestamp       time.Time `json:"timestamp" gorm:"not null"`
	Key             string    `json:"key" gorm:"uniqueIndex:idx_key_params;not null"`
	Params          string    `json:"params" gorm:"uniqueIndex:idx_key_params;not null"`
	CompressionType string    `json:"compression_type" gorm:"not null"`
	Data            []byte    `json:"data" gorm:"not null"`
}

type CacheFunk added in v0.4.0

type CacheFunk struct {
	Config  *Config
	Storage CacheStorage
}

func (*CacheFunk) Cleanup added in v0.4.0

func (c *CacheFunk) Cleanup(forceCleanupExpired bool)

func (*CacheFunk) Clear added in v0.4.0

func (c *CacheFunk) Clear() error

func (*CacheFunk) EntryCount added in v0.4.0

func (c *CacheFunk) EntryCount() (int64, error)

func (*CacheFunk) ExpiredEntryCount added in v0.4.0

func (c *CacheFunk) ExpiredEntryCount() (int64, error)

func (*CacheFunk) Get added in v0.4.0

func (c *CacheFunk) Get(key string, config *KeyConfig, params string, value any) error

func (*CacheFunk) GetLazy added in v0.4.1

func (c *CacheFunk) GetLazy(key string, config *KeyConfig, params string) (LazyLoad, error)

func (*CacheFunk) Set added in v0.4.0

func (c *CacheFunk) Set(key string, config *KeyConfig, params string, value any) error

type CacheStorage added in v0.4.0

type CacheStorage interface {
	// Get a value from the cache if it exists
	Get(key string, config *KeyConfig, params string, expireBeforeTime time.Time) (value []byte, err error)
	// Set a raw value for key in the cache
	Set(key string, config *KeyConfig, params string, value []byte, timestamp time.Time) (err error)
	// Get the number of entries in the cache
	EntryCount() (count int64, err error)
	// Get how many entries have expired in the cache compared to expireBeforeTime
	ExpiredEntryCount(key string, config *KeyConfig, expireBeforeTime time.Time) (count int64, err error)
	// Delete all entries in the cache
	Clear() error
	// Delete entries that have timestamps in cache before expireBeforeTime
	Cleanup(key string, config *KeyConfig, expireBeforeTime time.Time) error
	// Print all cached entries for test debugging purposes
	Dump(n int64)
}

type Compression added in v0.4.0

type Compression interface {
	Compress(input []byte) ([]byte, error)
	Decompress(input []byte) ([]byte, error)
	CompressAndWrite(w io.Writer, data []byte) error
	ReadAndDecompress(r io.Reader) ([]byte, error)
	String() string
}

type Config added in v0.1.0

type Config struct {
	Defaults *KeyConfig
	Configs  map[string]*KeyConfig
}

Config stores the various settings for different cached endpoints

func (*Config) Get added in v0.4.0

func (c *Config) Get(key string) *KeyConfig

type DiskStorage added in v0.4.0

type DiskStorage struct {
	BasePath      string
	CalculatePath DiskStoragePather
}

DiskStorage stores cached items on disk in a tree of folders

Example
type HelloWorldParams struct {
	Name string
}

helloWorld := func(ignoreCache bool, params *HelloWorldParams) (string, error) {
	return "Hello " + params.Name, nil
}

config := &Config{}
storage, err := NewDiskStorage("/path/to/cache", DefaultDiskStoragePather)
if err != nil {
	fmt.Printf("Error while creating disk storage: %s\n", err)
	return
}
cache := &CacheFunk{
	Config:  config,
	Storage: storage,
}

HelloWorld := WrapWithIgnore(cache, "hello", helloWorld)
params := &HelloWorldParams{
	Name: "bob",
}

// First call will get value from wrapped function
value, err := HelloWorld(false, params)
fmt.Println("First call:", value, err)
// Second call will get value from cache
value, err = HelloWorld(false, params)
fmt.Println("Second call:", value, err)

func NewDiskStorage added in v0.4.0

func NewDiskStorage(basePath string, pather DiskStoragePather) (*DiskStorage, error)

func (*DiskStorage) Cleanup added in v0.4.0

func (c *DiskStorage) Cleanup(key string, config *KeyConfig, expireBeforeTime time.Time) error

Cleanup will delete all cache entries that have expired

func (*DiskStorage) Clear added in v0.4.0

func (c *DiskStorage) Clear() error

Clear will delete all cache entries

func (*DiskStorage) Dump added in v0.4.0

func (c *DiskStorage) Dump(n int64)

Dump prints out a sample hexdump of cached content

func (*DiskStorage) EntryCount added in v0.4.0

func (c *DiskStorage) EntryCount() (int64, error)

func (*DiskStorage) ExpiredEntryCount added in v0.4.0

func (c *DiskStorage) ExpiredEntryCount(key string, config *KeyConfig, expireBeforeTime time.Time) (int64, error)

func (*DiskStorage) Get added in v0.4.0

func (c *DiskStorage) Get(key string, config *KeyConfig, params string, expireBeforeTime time.Time) ([]byte, error)

func (*DiskStorage) IterateFiles added in v0.4.0

func (c *DiskStorage) IterateFiles(basePath string, callback func(string, fs.DirEntry))

func (*DiskStorage) Set added in v0.4.0

func (c *DiskStorage) Set(key string, config *KeyConfig, params string, value []byte, timestamp time.Time) error

type DiskStoragePather added in v0.4.0

type DiskStoragePather func(key string, params string) []string

DiskStoragePather returns a path representing the key and params. The path is broken down into an array of directories to help limit file counts in folders when there are thousands of cache entries.

type GORMStorage added in v0.4.0

type GORMStorage struct {
	DB *gorm.DB
}

GORMStorage stores cached items in a database table

Example
type HelloWorldParams struct {
	Name string
}

helloWorld := func(ignoreCache bool, params *HelloWorldParams) (string, error) {
	return "Hello " + params.Name, nil
}

db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
if err != nil {
	panic("failed to connect database")
}

config := &Config{}
storage, err := NewGORMStorage(db)
if err != nil {
	panic("failed to setup gorm cache storage")
}
cache := &CacheFunk{
	Config:  config,
	Storage: storage,
}

HelloWorld := WrapWithIgnore(cache, "hello", helloWorld)
params := &HelloWorldParams{
	Name: "bob",
}

// First call will get value from wrapped function
value, err := HelloWorld(false, params)
fmt.Println("First call:", value, err)
// Second call will get value from cache
value, err = HelloWorld(false, params)
fmt.Println("Second call:", value, err)

func NewGORMStorage added in v0.4.0

func NewGORMStorage(db *gorm.DB) (*GORMStorage, error)

func (*GORMStorage) Cleanup added in v0.4.0

func (c *GORMStorage) Cleanup(key string, config *KeyConfig, expireBeforeTime time.Time) error

Cleanup will delete all cache entries that have expired

func (*GORMStorage) Clear added in v0.4.0

func (c *GORMStorage) Clear() error

Clear will delete all cache entries

func (*GORMStorage) Dump added in v0.4.0

func (c *GORMStorage) Dump(n int64)

func (*GORMStorage) EntryCount added in v0.4.0

func (c *GORMStorage) EntryCount() (int64, error)

func (*GORMStorage) ExpiredEntryCount added in v0.4.0

func (c *GORMStorage) ExpiredEntryCount(key string, config *KeyConfig, expireBeforeTime time.Time) (int64, error)

func (*GORMStorage) Get added in v0.4.0

func (c *GORMStorage) Get(key string, config *KeyConfig, params string, expireBeforeTime time.Time) ([]byte, error)

func (*GORMStorage) Set added in v0.4.0

func (c *GORMStorage) Set(key string, config *KeyConfig, params string, value []byte, timestamp time.Time) error

type InMemoryStorage added in v0.4.0

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

InMemoryStorage stores cache entries in a map

Example
type HelloWorldParams struct {
	Name string
}

helloWorld := func(ignoreCache bool, params *HelloWorldParams) (string, error) {
	return "Hello " + params.Name, nil
}

config := &Config{}

cache := &CacheFunk{
	Config:  config,
	Storage: NewInMemoryStorage(),
}

HelloWorld := WrapWithIgnore(cache, "hello", helloWorld)
params := &HelloWorldParams{
	Name: "bob",
}

// First call will get value from wrapped function
value, err := HelloWorld(false, params)
fmt.Println("First call:", value, err)
// Second call will get value from cache
value, err = HelloWorld(false, params)
fmt.Println("Second call:", value, err)

func NewInMemoryStorage added in v0.4.0

func NewInMemoryStorage() *InMemoryStorage

func (*InMemoryStorage) Cleanup added in v0.4.0

func (c *InMemoryStorage) Cleanup(key string, config *KeyConfig, expireBeforeTime time.Time) error

func (*InMemoryStorage) Clear added in v0.4.0

func (c *InMemoryStorage) Clear() error

func (*InMemoryStorage) Dump added in v0.4.0

func (c *InMemoryStorage) Dump(n int64)

func (*InMemoryStorage) EntryCount added in v0.4.0

func (c *InMemoryStorage) EntryCount() (int64, error)

func (*InMemoryStorage) ExpiredEntryCount added in v0.4.0

func (c *InMemoryStorage) ExpiredEntryCount(key string, config *KeyConfig, expireBeforeTime time.Time) (int64, error)

func (*InMemoryStorage) Get added in v0.4.0

func (c *InMemoryStorage) Get(key string, config *KeyConfig, params string, expireBeforeTime time.Time) ([]byte, error)

func (*InMemoryStorage) Set added in v0.4.0

func (c *InMemoryStorage) Set(key string, config *KeyConfig, params string, value []byte, timestamp time.Time) error

type InMemoryStorageEntry added in v0.4.0

type InMemoryStorageEntry struct {
	Data            []byte
	Timestamp       time.Time
	CompressionType string
}

type KeyConfig

type KeyConfig struct {
	// TTL is time to live in seconds before the cache value can be deleted
	// If TTL is 0, cache value will expire immediately
	// If TTL is less than 0, cache value will "never" expire (not until year 9999)
	// Some useful TTL values:
	// - 3600: one hour
	// - 86400: one day
	// - 604800: one week
	// - 2419200: four weeks
	// - 31536000: one year
	TTL int64
	// When TTLJitter is > 0, a random value from 1 to TTLJitter will be added to TTL
	// This spreads cache expire time to stop retrieval of fresh responses all at once
	TTLJitter int64
	// Use expired entries when a fresh value cannot be successfully retrieved
	FallbackToExpired bool
	// Settings for transformation of parameters
	ParamCodec ParamCodec
	// Settings for transformation of body
	BodyCodec       BodyCodec
	BodyCompression Compression
}

KeyConfig is a set of specific settings for a cached endpoint

func (*KeyConfig) DecompressAndUnmarshal added in v0.4.1

func (kc *KeyConfig) DecompressAndUnmarshal(valueData []byte, value any) error

func (*KeyConfig) GetBodyCodec added in v0.4.0

func (kc *KeyConfig) GetBodyCodec() BodyCodec

func (*KeyConfig) GetBodyCompression added in v0.4.0

func (kc *KeyConfig) GetBodyCompression() Compression

func (*KeyConfig) GetExpireTime added in v0.4.0

func (kc *KeyConfig) GetExpireTime(now time.Time) time.Time

GetExpireTime calculates an expire time from the key config within reasonable bounds for modern filesystems (as of year 2017) any entry with timestamp before the expire time is said to have expired

func (*KeyConfig) GetParamCodec added in v0.4.0

func (kc *KeyConfig) GetParamCodec() ParamCodec

func (*KeyConfig) GetTimestamp added in v0.4.0

func (kc *KeyConfig) GetTimestamp(now time.Time) time.Time

GetTimestamp the timestamp of a file with TTLJitter applied

func (*KeyConfig) MarshalAndCompress added in v0.4.1

func (kc *KeyConfig) MarshalAndCompress(value any) ([]byte, error)

func (*KeyConfig) MarshalJSON added in v0.4.0

func (kc *KeyConfig) MarshalJSON() ([]byte, error)

MarshalJSON helps to marshal the different codecs and compression

func (*KeyConfig) UnmarshalJSON added in v0.4.0

func (kc *KeyConfig) UnmarshalJSON(data []byte) error

UnmarshalJSON helps to unmarshal the different codecs and compression

type LazyLoad added in v0.4.1

type LazyLoad func(any) error

type ParamCodec added in v0.4.0

type ParamCodec interface {
	Marshal(v any) (string, error)
	Unmarshal(data string, v any) error
	String() string
}

type ParameterEncoder added in v0.4.0

type ParameterEncoder func(interface{}) (string, error)

Jump to

Keyboard shortcuts

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