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

[RelayMiner] Implement relayminer query caching #1050

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
28 changes: 16 additions & 12 deletions pkg/client/query/accquerier.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package query

import (
"context"
"sync"

"cosmossdk.io/depinject"
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
Expand All @@ -11,6 +10,7 @@ import (
grpc "github.com/cosmos/gogoproto/grpc"

"github.com/pokt-network/poktroll/pkg/client"
"github.com/pokt-network/poktroll/pkg/polylog"
)

var _ client.AccountQueryClient = (*accQuerier)(nil)
Expand All @@ -21,11 +21,10 @@ var _ client.AccountQueryClient = (*accQuerier)(nil)
type accQuerier struct {
clientConn grpc.ClientConn
accountQuerier accounttypes.QueryClient
logger polylog.Logger

// accountCache is a cache of accounts that have already been queried.
// TODO_TECHDEBT: Add a size limit to the cache and consider an LRU cache.
Olshansk marked this conversation as resolved.
Show resolved Hide resolved
accountCache map[string]types.AccountI
accountCacheMu sync.Mutex
Olshansk marked this conversation as resolved.
Show resolved Hide resolved
// accountsCache caches accountQueryClient.Account requests
accountsCache KeyValueCache[types.AccountI]
}

// NewAccountQuerier returns a new instance of a client.AccountQueryClient by
Expand All @@ -34,11 +33,13 @@ type accQuerier struct {
// Required dependencies:
// - clientCtx
func NewAccountQuerier(deps depinject.Config) (client.AccountQueryClient, error) {
aq := &accQuerier{accountCache: make(map[string]types.AccountI)}
aq := &accQuerier{}

if err := depinject.Inject(
deps,
&aq.clientConn,
&aq.logger,
&aq.accountsCache,
); err != nil {
return nil, err
}
Expand All @@ -53,13 +54,16 @@ func (aq *accQuerier) GetAccount(
ctx context.Context,
address string,
) (types.AccountI, error) {
aq.accountCacheMu.Lock()
defer aq.accountCacheMu.Unlock()
logger := aq.logger.With("query_client", "account", "method", "GetAccount")

if foundAccount, isAccountFound := aq.accountCache[address]; isAccountFound {
return foundAccount, nil
// Check if the account is present in the cache.
if account, found := aq.accountsCache.Get(address); found {
logger.Debug().Msgf("cache hit for key: %s", address)
return account, nil
}

logger.Debug().Msgf("cache miss for key: %s", address)

// Query the blockchain for the account record
req := &accounttypes.QueryAccountRequest{Address: address}
res, err := aq.accountQuerier.Account(ctx, req)
Expand All @@ -81,8 +85,8 @@ func (aq *accQuerier) GetAccount(
return nil, ErrQueryPubKeyNotFound
}

aq.accountCache[address] = fetchedAccount

// Cache the fetched account for future queries.
aq.accountsCache.Set(address, fetchedAccount)
Olshansk marked this conversation as resolved.
Show resolved Hide resolved
return fetchedAccount, nil
}

Expand Down
39 changes: 39 additions & 0 deletions pkg/client/query/appquerier.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
grpc "github.com/cosmos/gogoproto/grpc"

"github.com/pokt-network/poktroll/pkg/client"
"github.com/pokt-network/poktroll/pkg/polylog"
apptypes "github.com/pokt-network/poktroll/x/application/types"
)

Expand All @@ -18,6 +19,12 @@ var _ client.ApplicationQueryClient = (*appQuerier)(nil)
type appQuerier struct {
clientConn grpc.ClientConn
applicationQuerier apptypes.QueryClient
logger polylog.Logger

// applicationsCache caches applicationQueryClient.Application requests
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// applicationsCache caches applicationQueryClient.Application requests
// applicationsCache caches application.Applications returned from applicationQueryClient.Application requests

applicationsCache KeyValueCache[apptypes.Application]
// paramsCache caches applicationQueryClient.Params requests
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as 👆

(seems like other places as well)

paramsCache ParamsCache[apptypes.Params]
}

// NewApplicationQuerier returns a new instance of a client.ApplicationQueryClient
Expand All @@ -31,6 +38,9 @@ func NewApplicationQuerier(deps depinject.Config) (client.ApplicationQueryClient
if err := depinject.Inject(
deps,
&aq.clientConn,
&aq.logger,
&aq.applicationsCache,
&aq.paramsCache,
); err != nil {
return nil, err
}
Expand All @@ -45,17 +55,33 @@ func (aq *appQuerier) GetApplication(
ctx context.Context,
appAddress string,
) (apptypes.Application, error) {
logger := aq.logger.With("query_client", "application", "method", "GetApplication")

// Check if the application is present in the cache.
if app, found := aq.applicationsCache.Get(appAddress); found {
logger.Debug().Msgf("cache hit for key: %s", appAddress)
return app, nil
red-0ne marked this conversation as resolved.
Show resolved Hide resolved
}

logger.Debug().Msgf("cache miss for key: %s", appAddress)

req := apptypes.QueryGetApplicationRequest{Address: appAddress}
res, err := aq.applicationQuerier.Application(ctx, &req)
if err != nil {
return apptypes.Application{}, apptypes.ErrAppNotFound.Wrapf("app address: %s [%v]", appAddress, err)
}

// Cache the application.
aq.applicationsCache.Set(appAddress, res.Application)
return res.Application, nil
}

// GetAllApplications returns all staked applications
func (aq *appQuerier) GetAllApplications(ctx context.Context) ([]apptypes.Application, error) {
req := apptypes.QueryAllApplicationsRequest{}
// TODO_OPTIMIZE: Fill the cache with all applications and mark it as
// having been filled, such that subsequent calls to this function will
// return the cached value.
res, err := aq.applicationQuerier.AllApplications(ctx, &req)
if err != nil {
return []apptypes.Application{}, err
Expand All @@ -65,10 +91,23 @@ func (aq *appQuerier) GetAllApplications(ctx context.Context) ([]apptypes.Applic

// GetParams returns the application module parameters
func (aq *appQuerier) GetParams(ctx context.Context) (*apptypes.Params, error) {
logger := aq.logger.With("query_client", "application", "method", "GetParams")

// Check if the application module parameters are present in the cache.
if params, found := aq.paramsCache.Get(); found {
logger.Debug().Msg("cache hit")
return &params, nil
red-0ne marked this conversation as resolved.
Show resolved Hide resolved
}

logger.Debug().Msg("cache miss")

req := apptypes.QueryParamsRequest{}
res, err := aq.applicationQuerier.Params(ctx, &req)
if err != nil {
return nil, err
}

// Update the cache with the newly retrieved application module parameters.
aq.paramsCache.Set(res.Params)
return &res.Params, nil
}
20 changes: 20 additions & 0 deletions pkg/client/query/bankquerier.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (

"github.com/pokt-network/poktroll/app/volatile"
"github.com/pokt-network/poktroll/pkg/client"
querytypes "github.com/pokt-network/poktroll/pkg/client/query/types"
"github.com/pokt-network/poktroll/pkg/polylog"
)

var _ client.BankQueryClient = (*bankQuerier)(nil)
Expand All @@ -19,6 +21,10 @@ var _ client.BankQueryClient = (*bankQuerier)(nil)
type bankQuerier struct {
clientConn grpc.ClientConn
bankQuerier banktypes.QueryClient
logger polylog.Logger

// balancesCache caches bankQueryClient.GetBalance requests
balancesCache KeyValueCache[querytypes.Balance]
}

// NewBankQuerier returns a new instance of a client.BankQueryClient by
Expand All @@ -32,6 +38,8 @@ func NewBankQuerier(deps depinject.Config) (client.BankQueryClient, error) {
if err := depinject.Inject(
deps,
&bq.clientConn,
&bq.logger,
&bq.balancesCache,
); err != nil {
return nil, err
}
Expand All @@ -46,12 +54,24 @@ func (bq *bankQuerier) GetBalance(
ctx context.Context,
address string,
) (*sdk.Coin, error) {
logger := bq.logger.With("query_client", "bank", "method", "GetBalance")

// Check if the account balance is present in the cache.
if balance, found := bq.balancesCache.Get(address); found {
logger.Debug().Msgf("cache hit for key: %s", address)
return balance, nil
}

logger.Debug().Msgf("cache miss for key: %s", address)

// Query the blockchain for the balance record
req := &banktypes.QueryBalanceRequest{Address: address, Denom: volatile.DenomuPOKT}
res, err := bq.bankQuerier.Balance(ctx, req)
if err != nil {
return nil, ErrQueryBalanceNotFound.Wrapf("address: %s [%s]", address, err)
}

// Cache the balance for future queries
bq.balancesCache.Set(address, res.Balance)
return res.Balance, nil
}
83 changes: 83 additions & 0 deletions pkg/client/query/cache/cache_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package cache_test

import (
"testing"

"github.com/stretchr/testify/require"

"github.com/pokt-network/poktroll/pkg/client/query/cache"
)

func TestKeyValueCache(t *testing.T) {
kvCache := cache.NewKeyValueCache[any]()

// Test Get on an empty cache
_, found := kvCache.Get("key")
require.False(t, found)

// Set a value in the cache
kvCache.Set("key", "value")

// Test Get on a non-empty cache
value, found := kvCache.Get("key")
require.True(t, found)
require.Equal(t, "value", value)

// Test Delete on a non-empty cache
kvCache.Delete("key")

// Test Get on a deleted key
_, found = kvCache.Get("key")
require.False(t, found)

// Set multiple values in the cache
kvCache.Set("key1", "value1")
kvCache.Set("key2", "value2")

// Test Clear on a non-empty cache
kvCache.Clear()

// Test Get on an empty cache
_, found = kvCache.Get("key1")
require.False(t, found)

_, found = kvCache.Get("key2")
require.False(t, found)

// Delete a non-existing key
kvCache.Delete("key1")

// Test Get on a deleted key
_, found = kvCache.Get("key1")
require.False(t, found)

// Test Clear on an empty cache
kvCache.Clear()

// Test Get on an empty cache
_, found = kvCache.Get("key2")
require.False(t, found)
}

func TestParamsCache(t *testing.T) {
paramsCache := cache.NewParamsCache[any]()

// Test Get on an empty cache
_, found := paramsCache.Get()
require.False(t, found)

// Set a value in the cache
paramsCache.Set("value")

// Test Get on a non-empty cache
value, found := paramsCache.Get()
require.True(t, found)
require.Equal(t, "value", value)

// Test Clear on a non-empty cache
paramsCache.Clear()

// Test Get on an empty cache
_, found = paramsCache.Get()
require.False(t, found)
}
61 changes: 61 additions & 0 deletions pkg/client/query/cache/kvcache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package cache

import (
"sync"

"github.com/pokt-network/poktroll/pkg/client/query"
)

var _ query.KeyValueCache[any] = (*keyValueCache[any])(nil)

// keyValueCache is a simple in-memory key-value cache implementation.
// It is safe for concurrent use.
type keyValueCache[V any] struct {
cacheMu sync.RWMutex
valuesMap map[string]V
}

// NewKeyValueCache returns a new instance of a KeyValueCache.
func NewKeyValueCache[T any]() query.KeyValueCache[T] {
return &keyValueCache[T]{
valuesMap: make(map[string]T),
}
}

// Get returns the value for the given key.
// A boolean is returned as the second value to indicate if the key was found in the cache.
func (c *keyValueCache[V]) Get(key string) (value V, found bool) {
c.cacheMu.RLock()
defer c.cacheMu.RUnlock()

value, found = c.valuesMap[key]
return value, found
}

// Set sets the value for the given key.
// TODO_CONSIDERATION: Add a method to set many values and indicate whether it
// is the result of a GetAll operation. This would allow us to know whether the
// cache is populated with all the possible values, so any other GetAll operation
// could be returned from the cache.
func (c *keyValueCache[V]) Set(key string, value V) {
c.cacheMu.Lock()
defer c.cacheMu.Unlock()

c.valuesMap[key] = value
}

// Delete deletes the value for the given key.
func (c *keyValueCache[V]) Delete(key string) {
c.cacheMu.Lock()
defer c.cacheMu.Unlock()

delete(c.valuesMap, key)
}

// Clear empties the whole cache.
func (c *keyValueCache[V]) Clear() {
c.cacheMu.Lock()
defer c.cacheMu.Unlock()

c.valuesMap = make(map[string]V)
}
Loading
Loading