-
Notifications
You must be signed in to change notification settings - Fork 12
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
base: main
Are you sure you want to change the base?
Conversation
The image is going to be pushed after the next commit. You can use If you also want to run E2E tests, please add |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@red-0ne I did a first partial review but have a lot of comments & questions.
Here's a high-level summary but PTAL at the actual comments as well:
- Need to understand if/how this can build on top of [Off-chain] feat: in-memory query cache(s) #994 w/ @bryanchriswhite
- See a few comments (logs + comments) that need to be addressed in multiple places
- I’m a bit concerned (and don’t understand) how we’re not using “height” to retrieve things from the cache, especially when values are always changing
- When does the cache ever get cleared?
- Light on tests
- I’d be interested to see numbers of performance improvement
@@ -49,11 +53,19 @@ func NewSharedQuerier(deps depinject.Config) (client.SharedQueryClient, error) { | |||
// Once `ModuleParamsClient` is implemented, use its replay observable's `#Last()` method | |||
// to get the most recently (asynchronously) observed (and cached) value. | |||
func (sq *sharedQuerier) GetParams(ctx context.Context) (*sharedtypes.Params, error) { | |||
// Get the params from the cache if they exist. | |||
if params, found := sq.paramsCache.Get(); found { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm concerned (and don't fully understand) the lack of a "height" param when retrieving things from the cache.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This cache implementation does not add any new functionality besides caching whatever has been queried.
It does not alter the RelayMiner
s current behavior
RelayMiner
cold start- React to
Param
s change
For those reasons, it does not leverage historical data that justifies the usage of height
for cache querying.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My understanding is that the cache implementations here are NOT historical; i.e. ONLY the most recently observed value is cached for each ParamsCache
instance (or key, in the case of KeyValueCache
).
While #994 does include historical caching as well (via the HistoricalQueryCache
interface, that's an additional and distinct feature.
This shouldn't be necessary here because we're clearing the cache on every new block. The end result being, somewhat sub-optimal, but significant caching. This reformulates the number off-chain queries from being a function of API usage, to no more than one per block, per cache.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice one @red-0ne! 🙌
Thanks for doing this! ❤️
} | ||
|
||
// KeyValueCache is an interface for a simple in-memory key-value cache implementation. | ||
type KeyValueCache[V any] interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pkg/client/query/sessionquerier.go
Outdated
clientConn grpc.ClientConn | ||
sessionQuerier sessiontypes.QueryClient | ||
sharedQueryClient client.SharedQueryClient | ||
sessionsCache KeyValueCache[*sessiontypes.Session] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
@@ -49,11 +53,19 @@ func NewSharedQuerier(deps depinject.Config) (client.SharedQueryClient, error) { | |||
// Once `ModuleParamsClient` is implemented, use its replay observable's `#Last()` method | |||
// to get the most recently (asynchronously) observed (and cached) value. | |||
func (sq *sharedQuerier) GetParams(ctx context.Context) (*sharedtypes.Params, error) { | |||
// Get the params from the cache if they exist. | |||
if params, found := sq.paramsCache.Get(); found { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My understanding is that the cache implementations here are NOT historical; i.e. ONLY the most recently observed value is cached for each ParamsCache
instance (or key, in the case of KeyValueCache
).
While #994 does include historical caching as well (via the HistoricalQueryCache
interface, that's an additional and distinct feature.
This shouldn't be necessary here because we're clearing the cache on every new block. The end result being, somewhat sub-optimal, but significant caching. This reformulates the number off-chain queries from being a function of API usage, to no more than one per block, per cache.
@@ -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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// applicationsCache caches applicationQueryClient.Application requests | |
// applicationsCache caches application.Applications returned from applicationQueryClient.Application requests |
|
||
// applicationsCache caches applicationQueryClient.Application requests | ||
applicationsCache KeyValueCache[apptypes.Application] | ||
// paramsCache caches applicationQueryClient.Params requests |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leaving a partial review.
-
@bryanchriswhite Can you please prioritize getting [Off-chain] feat: in-memory query cache(s) #994 in? It's the most mature/versatile cache, and I'd like us to just build on top of 1 thing.
-
@red-0ne See some of my nits/edits, but in particular around using gomock for proper mocks.
Will do a full review after (1) & (2) are done.
Few notes:
- If you think we should take a different direction, let's jump on a call.
- We have other (large) parallel efforts going on, so there shouldn't be any blockers
- I strongly believe we should benchmark in this PR. Seems like something an LLM can help get done in a couple of hours.
c.callCount++ | ||
} | ||
|
||
// MockServiceQueryServer is a mock implementation of the servicetypes.QueryServer interface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Up until now we've always been using gomock
to generate this sort of thing, which has support for call counters.
I feel strongly that we should not be changing patterns now.
config.NewSupplyKeyValueCacheFn[*sessiontypes.Session](cache.WithNewBlockCacheClearing), | ||
config.NewSupplyKeyValueCacheFn[*cosmostypes.Coin](cache.WithNewBlockCacheClearing), | ||
|
||
config.NewSupplySharedQueryClientFn(), // leaf |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have some //leaf
comments before the new code and some (this one) after the new code.
What's the idea behind this code organization?
config.NewSupplyKeyValueCacheFn[apptypes.Application](cache.WithNewBlockCacheClearing), | ||
config.NewSupplyKeyValueCacheFn[cosmostypes.AccountI](cache.WithNewBlockCacheClearing), | ||
config.NewSupplyKeyValueCacheFn[sharedtypes.Supplier](cache.WithNewBlockCacheClearing), | ||
config.NewSupplyKeyValueCacheFn[*sessiontypes.Session](cache.WithNewBlockCacheClearing), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#PUC
config.NewSupplySharedQueryClientFn(), // leaf | ||
|
||
// Setup the params caches and configure them to clear on new blocks. | ||
config.NewSupplyParamsCacheFn[sharedtypes.Params](cache.WithNewBlockCacheClearing), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#PUC
config.NewSupplySharedQueryClientFn(), // leaf | ||
|
||
// Setup the params caches and configure them to clear on new blocks. | ||
// TODO_TECHDEBT: Consider a flag to change client queriers caching behavior. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm going to push on the fact that this is a TODO_IN_THIS_PR.
It's not that hard and I want to understand the benefit (if any) of this cache.
It feels like we've built a car but not actually checking if it works.
} | ||
|
||
// KeyValueCache is an interface for a simple in-memory key-value cache implementation. | ||
type KeyValueCache[V any] interface { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left a comment on discord, but I feel strongly that we should finish #994 and rebase on top of it.
Now is the time to do this right.
![Screenshot 2025-02-11 at 4 31 05 PM](https://private-user-images.githubusercontent.com/1892194/412212692-8de639a5-abd1-4e06-8513-f8b8b2ca49bb.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MzkzOTY3NjIsIm5iZiI6MTczOTM5NjQ2MiwicGF0aCI6Ii8xODkyMTk0LzQxMjIxMjY5Mi04ZGU2MzlhNS1hYmQxLTRlMDYtODUxMy1mOGI4YjJjYTQ5YmIucG5nP1gtQW16LUFsZ29yaXRobT1BV1M0LUhNQUMtU0hBMjU2JlgtQW16LUNyZWRlbnRpYWw9QUtJQVZDT0RZTFNBNTNQUUs0WkElMkYyMDI1MDIxMiUyRnVzLWVhc3QtMSUyRnMzJTJGYXdzNF9yZXF1ZXN0JlgtQW16LURhdGU9MjAyNTAyMTJUMjE0MTAyWiZYLUFtei1FeHBpcmVzPTMwMCZYLUFtei1TaWduYXR1cmU9MGRhYTcxNGYwYTZmMWFlZWUxY2Q2ZDIyMDNjOTRmZWZlNjI2NzBmMzg5ZDE2N2Q4MmZmMzdjNjg5ZjQ4YWEyMiZYLUFtei1TaWduZWRIZWFkZXJzPWhvc3QifQ.eXSKDfe2f_NeCeAd2w7dmyqK2lScm76wAOR86X18Y-Y)
// Balance represents a pointer to a Cosmos SDK Coin, specifically used for bank balance queries. | ||
// It is deliberately defined as a distinct type (not a type alias) to ensure clear dependency | ||
// injection and to differentiate it from other coin caches in the system. This type helps | ||
// maintain separation of concerns between different types of coin-related data in the caching | ||
// layer. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Balance represents a pointer to a Cosmos SDK Coin, specifically used for bank balance queries. | |
// It is deliberately defined as a distinct type (not a type alias) to ensure clear dependency | |
// injection and to differentiate it from other coin caches in the system. This type helps | |
// maintain separation of concerns between different types of coin-related data in the caching | |
// layer. | |
// Balance represents a pointer to a Cosmos SDK Coin used for bank balance queries. | |
// It is defined as a distinct type (not an alias) to: | |
// - Ensure clear dependency injection | |
// - Differentiate from other coin caches in the system | |
// - Maintain separation of concerns between coin-related data in the caching layer | |
type Balance *sdk.Coin |
@red-0ne Have you used the code-cleaner Claude project yet?
// BlockHash represents a byte slice, specifically used for bank balance query caches. | ||
// It is deliberately defined as a distinct type (not a type alias) to ensure clear | ||
// dependency injection and to differentiate it from other byte slice caches in the system. | ||
// This type helps maintain separation of concerns between different types of | ||
// byte slice data in the caching layer. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// BlockHash represents a byte slice, specifically used for bank balance query caches. | |
// It is deliberately defined as a distinct type (not a type alias) to ensure clear | |
// dependency injection and to differentiate it from other byte slice caches in the system. | |
// This type helps maintain separation of concerns between different types of | |
// byte slice data in the caching layer. | |
// BlockHash represents a byte slice used for bank balance query caches. | |
// It is defined as a distinct type (not an alias) to: | |
// - Ensure clear dependency injection | |
// - Differentiate from other byte slice caches | |
// - Maintain separation of concerns between byte slice data in caching layer |
@@ -0,0 +1,8 @@ | |||
package types |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having separate files for this feels like overkill.
Can we just have a types.go
?
return supplier, nil | ||
} | ||
|
||
logger.Debug().Msgf("cache miss for key: %s", operatorAddress) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leaving one comment but please update everywhere. If these logs ever becomes the source for debugging, you want it to be ULTRA obvious.
logger.Debug().Msgf("cache miss for key: %s", operatorAddress) | |
logger.Debug().Msgf("cache miss for operator address key: %s", operatorAddress) |
Summary
Implements caching layer for query clients to reduce network calls and improve performance
Primary Changes:
KeyValueCache
andParamsCache
interfaces with thread-safe implementationsAccount
,Application
,Bank
,Service
, etc.)WithNewBlockCacheClearing
optionSecondary Changes:
sync.Mutex
implementation inaccQuerier
with new cache interfaceIssue
The RelayMiner RPC queries are not cached, which puts excessive load on the configured full node, degrading the performance of both off-chain and on-chain components.
Type of change
Select one or more from the following:
consensus-breaking
label if so. See [Infra] Automatically add theconsensus-breaking
label #791 for detailsSanity Checklist
assignees
,reviewers
,labels
,project
,iteration
andmilestone
make docusaurus_start
make go_develop_and_test
andmake test_e2e
devnet-test-e2e
label to run E2E tests in CI