cachecalc

package module
v1.5.0 Latest Latest
Warning

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

Go to latest
Published: Jan 12, 2024 License: MIT Imports: 16 Imported by: 1

README

CachedCalculations

Coordinated and Cached Calculations (Golang)

CachedCalculation

CachedCalculation is a small and simple implementation to coordinate and cache calculations.

Requirements

For backend, it is a frequent situation when some resource is requested many times a minute and takes a few seconds to compose and return the result. Most of the time the result will be the same as the underlying data is not changed that often. For example it could be the list of countries, the list of commodities and their prices at warehouse, etc. Still, all of this calculation's tasks, if not properly coordinated, will be making the same queries to the database, combine the results, and return the same JSON data. The situation could be improved by :

  1. Avoiding concurrent calculation of the same resource which decrease the load of the database, etc.
  2. Cache the response result based on the request and return subsequent responses from the cache rather than calculating them again.

Meet the CachedCalculations, which I created to implement these specific requirements as a backend developer.

Implementation

  1. Provided, that some calculation is "cached", it guarantees that when it does not hit the cache, it automatically performs necessary calculation, caches response, and returns it to the client. All the requests for the same resource will be waiting for the result if calculation for the resource has been started, so the same calculation is never completed concurrently, either on the same machine or distributed.
  2. It provides the facility for cache entry expiration:
  • If the data isn't too old (MinTTL), let's say 1-10 seconds, for MinTTL time just cached response is returned for the subsequent requests of the same resource.
  • When MinTTL for the data expired, it still returns cached response to the client but immediately performs the calculation of the resource in background (refresh). When refresh finished, it stores the response to the cache, and will return the refreshed response for subsequent calls.
  • When the data has expired totally (MaxTTL), it's forcefully removed and will be recalculated on demand.
  1. CachedCalculation provides a simple method GetCachedCalc of refactoring existing backend methods to use its infrastructure. The idea is to wrap all the existing methods that handle some request to a function of type
CalculateValue func(context.Context) (any, error)

where any in the type of the result, in most cases will be the string with JSON. To do this, wrap your implementation into a closure called e.g. slowGet, which sees all the parameters of your method and use it like this:

result, err := cachecalc.GetCachedCalc(ctx, key, maxTTL, minTTL, true, slowGet)

It's a straightforward refactoring process which will drastically improves the performance of your backend!

  1. CachedCalculation takes lock before calculation using external cache if provided. Those who were unable to take lock patiently wait when the result appears in the external cache. This allows to avoid concurrency issues mentioned in point #1. When the calculated value is ready, all clients are provided with it from the cache. If you know that you have only a single instance of the server, you don't need an external cache. In this case it will work significantly faster due the absence of networking delays. Although it works with an external cache asynchronously, it still takes time for coordination between processes. My rule of thumb in this case: if you have a simple REST API handler without heavy load, give it more RAM and CPU, and single Golang server will handle your request much faster than in Kubernetes or other distributed systems. Don't use a sledgehammer to crack a nut!

TODO

Make TTL returned by calculation. It might know better when returned value expires. For example ttl for token returned by calculation.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var DefaultCCs = NewCachedCalculations(4, nil)

DefaultCCs default cached calculations cache used by GetCachedCalc It does not use external cache for coordinating between multiple distributed

Functions

func DeserializeValue added in v1.3.0

func DeserializeValue(buf []byte, valPtr any) error

func GetCachedCalc

func GetCachedCalc[T any](ctx context.Context, key any, minTTL, maxTTL time.Duration, limitWorker bool,
	calculateValue func(ctx context.Context) (T, error)) (result T, err error)

GetCachedCalc uses default cached calculations cache as GetCachedCalcX(DefaultCCs,...) for convenience it is created with default for no external cache, but that can be redefined by app

func GetCachedCalcOpt added in v1.1.1

func GetCachedCalcOpt[T any](ctx context.Context, key any,
	calculateValueAndOpt func(ctx context.Context) (T, CachedCalcOpts, error), limitWorkers bool) (T, error)

GetCachedCalc uses default cached calculations cache as GetCachedCalcX(DefaultCCs,...) for convenience it is created with default for no external cache, but that can be redefined by app

func GetCachedCalcOptX added in v1.1.1

func GetCachedCalcOptX[T any](cc *CachedCalculations, ctx context.Context, key any,
	calculateValueAndOpt func(ctx context.Context) (T, CachedCalcOpts, error), limitWorkers bool) (result T, err error)

func GetCachedCalcX

func GetCachedCalcX[T any](cc *CachedCalculations, ctx context.Context, key any, minTTL, maxTTL time.Duration, limitWorker bool,
	calculateValue func(ctx context.Context) (T, error)) (T, error)

GetCachedCalcX is used wherever you need to perform cached and coordinated calculation instead of regular and uncoordinated

ctx is a parent context for calculation if parent context is cancelled then all child context are cancelled as well

params of cachedCalculation - see description of how cachedCalculation defined

func GetRedis

func GetRedis(ctx context.Context) (*redis.Client, error)

GetRedis returns redis client which is used internally in this module but can be used otherwise when env variable REDIS_URL is set it uses to connect to redis. Otherwise it tries redis://127.0.0.1. returns redis.Client instance and nil for error on success

Types

type CacheEntry added in v1.2.1

type CacheEntry struct {
	Expire       time.Time     // time of expiration of this entry
	Refresh      time.Time     // time when this value should be refreshed
	CalcDuration time.Duration // how much time it took to calculate the value
	Err          error         // stores the last error status of calculations
	Value        []byte        // stores the serialized value of last calculations

	sync.WaitGroup
	sync.RWMutex
	// contains filtered or unexported fields
}

type CachedCalcOpts added in v1.1.1

type CachedCalcOpts struct {
	MaxTTL, MinTTL time.Duration
	CalcTime       time.Duration // the duration of last calculation
}

type CachedCalculations

type CachedCalculations struct {
	sync.Mutex
	sync.WaitGroup
	// contains filtered or unexported fields
}

CachedCalculations has the only method: GetCachedCalc. It is used for easy refactoring of slow/long calculating backend methods. See examples

func NewCachedCalculations

func NewCachedCalculations(maxWorkers int, externalCache ExternalCache) *CachedCalculations

NewCachedCalculations is used to create app's instance of CachedCalculations. It creates two threads which handle and coordinate cached backend calculations Graceful exit from app should include expiring ctx context and then smartCacheInstance.Wait() This will gracefully finish the job of those threads

func (*CachedCalculations) Close

func (cc *CachedCalculations) Close()

Close is automatically called on expired context. It is safe to call it multiple times it tries to gracefully interrupt all ongoing calculations using their context when it succeeds in this it removes the record about job

func (*CachedCalculations) RemoveEntries added in v1.2.1

func (cc *CachedCalculations) RemoveEntries(filter func(key any, entry *CacheEntry) bool)

type CalculateValue

type CalculateValue func(context.Context) (any, error)

CalculateValue this is type of function which returns interface{} type

type CalculateValueAndOpt added in v1.1.1

type CalculateValueAndOpt func(context.Context) (any, CachedCalcOpts, error)

CalculateValue this is type of function which returns interface{} type

type ExternalCache

type ExternalCache interface {
	// Set sets key to hold the string value.
	// If key already holds a value, it is overwritten, regardless of its type.
	// Any previous time to live associated with the key is discarded on successful SET operation.
	// Should return nil if successful
	Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
	// SetNX sets key to hold string value if key does not exist.
	// In that case, it is equal to SET.
	// When key already holds a value, no operation is performed.
	// SETNX is short for "SET if Not eXists".
	// keyCreated must return true if value is set
	SetNX(ctx context.Context, key string, value []byte, ttl time.Duration) (keyCreated bool, err error)
	// GetCachedCalc gets the value of key.
	// exists will be false If the key does not exist.
	// An error is returned if the implementor want to signal any errors.
	Get(ctx context.Context, key string) (value []byte, exists bool, err error)
	// Del Removes the specified key. A key is ignored if it does not exist.
	Del(ctx context.Context, key string) error
	Close() error // closes the connection
}

ExternalCache interface defines a set of required method to provide service of external cache which SmartCache can use to coordinate several of its distributed instances.

func NewPostgresCache added in v1.4.1

func NewPostgresCache(ctx context.Context, dbUrl string) (ExternalCache, error)

func NewRedisCache

func NewRedisCache(ctx context.Context) (ExternalCache, error)

NewRedisCache creates an instance of ExternalCache which is connected to ENV{REDIS_URL} or local redis server if not specified.

type PostgresCache added in v1.4.1

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

func (*PostgresCache) Close added in v1.4.1

func (p *PostgresCache) Close() error

func (*PostgresCache) Del added in v1.4.1

func (p *PostgresCache) Del(ctx context.Context, key string) error

func (*PostgresCache) Get added in v1.4.1

func (p *PostgresCache) Get(ctx context.Context, key string) (value []byte, exists bool, err error)

func (*PostgresCache) Set added in v1.4.1

func (p *PostgresCache) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error

func (*PostgresCache) SetNX added in v1.4.1

func (p *PostgresCache) SetNX(ctx context.Context, key string, value []byte, ttl time.Duration) (keyCreated bool, err error)

type RedisExternalCache

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

func (*RedisExternalCache) Close added in v1.4.1

func (r *RedisExternalCache) Close() error

func (*RedisExternalCache) Del

func (r *RedisExternalCache) Del(ctx context.Context, key string) error

func (*RedisExternalCache) Get

func (r *RedisExternalCache) Get(ctx context.Context, key string) (value []byte, exists bool, err error)

func (*RedisExternalCache) Set

func (r *RedisExternalCache) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error

func (*RedisExternalCache) SetNX

func (r *RedisExternalCache) SetNX(ctx context.Context, key string, value []byte, ttl time.Duration) (bool, error)

type SQLiteCache

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

SQLiteCache implements the ExternalCache interface using a SQLite database.

func NewSQLiteCache

func NewSQLiteCache(dbPath string) (*SQLiteCache, error)

NewSQLiteCache creates a new instance of SQLiteCache.

func (*SQLiteCache) Close added in v1.4.1

func (c *SQLiteCache) Close() error

Close closes the SQLite database connection.

func (*SQLiteCache) Del

func (c *SQLiteCache) Del(ctx context.Context, key string) error

Del Removes the specified key. A key is ignored if it does not exist.

func (*SQLiteCache) Get

func (c *SQLiteCache) Get(ctx context.Context, key string) (value []byte, exists bool, err error)

GetCachedCalc gets the value of key. exists will be false If the key does not exist. An error is returned if the implementor want to signal any errors.

func (*SQLiteCache) Set

func (c *SQLiteCache) Set(ctx context.Context, key string, value []byte, ttl time.Duration) error

Set sets key to hold the string value. If key already holds a value, it is overwritten, regardless of its type. Any previous time to live associated with the key is discarded on successful SET operation. Should return nil if successful

func (*SQLiteCache) SetNX

func (c *SQLiteCache) SetNX(ctx context.Context, key string, value []byte, ttl time.Duration) (keyCreated bool, err error)

SetNX sets key to hold string value if key does not exist. In that case, it is equal to SET. When key already holds a value, no operation is performed. SETNX is short for "SET if Not eXists". keyCreated must return true if value is set

Jump to

Keyboard shortcuts

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