Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Off-chain] feat: in-memory query cache(s) #994

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions pkg/client/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ type BankQueryClient interface {

// QueryCache is a key/value store style interface for a cache of a single type.
// It is intended to be used to cache query responses (or derivatives thereof),
// where each key uniquely indexes its most recent value.
// where each key uniquely indexes the most recent query response.
type QueryCache[T any] interface {
Get(key string) (T, error)
Set(key string, value T) error
Expand All @@ -375,8 +375,8 @@ type QueryCache[T any] interface {
// at multiple heights for a given key.
type HistoricalQueryCache[T any] interface {
QueryCache[T]
// GetAtHeight retrieves the nearest value <= the specified height
GetAtHeight(key string, height int64) (T, error)
// SetAtHeight adds or updates a value at a specific height
SetAtHeight(key string, value T, height int64) error
// GetAsOfVersion retrieves the nearest value <= the specified version number.
GetAsOfVersion(key string, version int64) (T, error)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we go with one of the following instead:

  1. GetVersioned
  2. GetAtVersion
  3. GetHistorical

Don't care which one but AsOf in a function name just doesn't feel right.

Copy link
Contributor Author

@bryanchriswhite bryanchriswhite Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, GetVersion and SetVersion would be my preference. Any objections @red-0ne @Olshansk?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also adding GetLatestVersion.

// SetAsOfVersion adds or updates a value at a specific version number.
SetAsOfVersion(key string, value T, version int64) error
}
75 changes: 52 additions & 23 deletions pkg/client/query/cache/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"time"
)

// EvictionPolicy determines how items are removed when number of keys in the cache reaches MaxKeys.
// EvictionPolicy determines which values are removed when number of keys in the cache reaches maxKeys.
type EvictionPolicy int64

const (
Expand All @@ -13,57 +13,86 @@ const (
LeastFrequentlyUsed
)

// queryCacheConfig is the configuration for query caches. It is intended to be
// configured via QueryCacheOptionFn functions.
// queryCacheConfig is the configuration for query caches.
// It is intended to be configured via QueryCacheOptionFn functions.
type queryCacheConfig struct {
// MaxKeys is the maximum number of items (key/value pairs) the cache can
// maxKeys is the maximum number of key/value pairs the cache can
// hold before it starts evicting.
MaxKeys int64
EvictionPolicy EvictionPolicy
// TTL is how long items should remain in the cache. Items older than the TTL
// MAY not be evicted but SHOULD not be considered as cache hits.
TTL time.Duration
maxKeys int64

// historical determines whether the cache will cache a single value for each
// key (false), or whether it will cache a history of values for each key (true).
// TODO_CONSIDERATION:
//
// maxValueSize is the maximum cumulative size of all values in the cache.
// maxValueSize int64
// maxCacheSize is the maximum cumulative size of all keys AND values in the cache.
// maxCacheSize int64

// evictionPolicy determines which values are removed when number of keys in the cache reaches maxKeys.
evictionPolicy EvictionPolicy
// ttl is how long values should remain valid in the cache. Items older than the
Olshansk marked this conversation as resolved.
Show resolved Hide resolved
// ttl MAY NOT be evicted immediately, but are NEVER considered as cache hits.
ttl time.Duration
// historical determines whether each key will point to a single values (false)
// or a history (i.e. reverse chronological list) of values (true).
historical bool
// pruneOlderThan is the number of past blocks for which to keep historical
// values. If 0, no historical pruning is performed. It only applies when
// historical is true.
pruneOlderThan int64
// maxVersionAge is the max difference between the latest known version and
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this sentence is hard to grok.

Any chance you could improve it with an example?

// any other version, below which value versions are retained, and above which
// value versions are pruned. If 0, no historical pruning is performed.
// It only applies when historical is true.
maxVersionAge int64
}

// QueryCacheOptionFn is a function which receives a queryCacheConfig for configuration.
type QueryCacheOptionFn func(*queryCacheConfig)

// WithHistoricalMode enables historical caching with the given pruneOlderThan
// Validate ensures that the queryCacheConfig isn't configured with incompatible options.
func (cfg *queryCacheConfig) Validate() error {
switch cfg.evictionPolicy {
case FirstInFirstOut:
// TODO_IMPROVE: support LeastRecentlyUsed and LeastFrequentlyUsed policies.
default:
return ErrQueryCacheConfigValidation.Wrapf("eviction policy %d not imlemented", cfg.evictionPolicy)
}

if cfg.maxVersionAge > 0 && !cfg.historical {
return ErrQueryCacheConfigValidation.Wrap("maxVersionAge > 0 requires historical mode to be enabled")
}

if cfg.historical && cfg.maxVersionAge < 0 {
return ErrQueryCacheConfigValidation.Wrapf("maxVersionAge MUST be >= 0, got: %d", cfg.maxVersionAge)
}

return nil
bryanchriswhite marked this conversation as resolved.
Show resolved Hide resolved
}

// WithHistoricalMode enables historical caching with the given maxVersionAge
// configuration; if 0, no historical pruning is performed.
func WithHistoricalMode(pruneOlderThan int64) QueryCacheOptionFn {
func WithHistoricalMode(numRetainedVersions int64) QueryCacheOptionFn {
return func(cfg *queryCacheConfig) {
cfg.historical = true
cfg.pruneOlderThan = pruneOlderThan
cfg.maxVersionAge = numRetainedVersions
}
}

// WithMaxKeys sets the maximum number of distinct key/value pairs the cache will
Olshansk marked this conversation as resolved.
Show resolved Hide resolved
// hold before evicting according to the configured eviction policy.
func WithMaxKeys(maxKeys int64) QueryCacheOptionFn {
return func(cfg *queryCacheConfig) {
cfg.MaxKeys = maxKeys
cfg.maxKeys = maxKeys
}
}

// WithEvictionPolicy sets the eviction policy.
func WithEvictionPolicy(policy EvictionPolicy) QueryCacheOptionFn {
return func(cfg *queryCacheConfig) {
cfg.EvictionPolicy = policy
cfg.evictionPolicy = policy
}
}

// WithTTL sets the time-to-live for cached items. Items older than the TTL
// MAY not be evicted but SHOULD not be considered as cache hits.
// WithTTL sets the time-to-live for cached values. Values older than the TTL
// MAY NOT be evicted immediately, but are NEVER considered as cache hits.
func WithTTL(ttl time.Duration) QueryCacheOptionFn {
return func(cfg *queryCacheConfig) {
cfg.TTL = ttl
cfg.ttl = ttl
}
}
7 changes: 5 additions & 2 deletions pkg/client/query/cache/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import "cosmossdk.io/errors"
const codesace = "client/query/cache"

var (
ErrCacheMiss = errors.Register(codesace, 1, "cache miss")
ErrHistoricalModeNotEnabled = errors.Register(codesace, 2, "historical mode not enabled")
ErrCacheMiss = errors.Register(codesace, 1, "cache miss")
ErrHistoricalModeNotEnabled = errors.Register(codesace, 2, "historical mode not enabled")
ErrQueryCacheConfigValidation = errors.Register(codesace, 3, "invalid query cache config")
ErrCacheInternal = errors.Register(codesace, 4, "cache internal error")
ErrUnsupportedHistoricalModeOp = errors.Register(codesace, 5, "operation not supported in historical mode")
)
Loading
Loading