-
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
[Off-chain] feat: in-memory query cache(s) #994
base: main
Are you sure you want to change the base?
Conversation
(cherry picked from commit 7d48aef23354497be55958ad2f484e1734550249)
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.
@bryanchriswhite I did a superficial review but did not dive into the validation of the business logic line-by-line.
Is there any section where you'd want another pair of 👀 ?
pkg/client/query/cache/memory.go
Outdated
c.itemsMu.Lock() | ||
defer c.itemsMu.Unlock() | ||
|
||
delete(c.items, key) |
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.
What if we delete the (key,value) pair for latestHeight
?
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.
Could you elaborate? I'm not quite following.
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.
Yea, this comment doesn't make sense if you don't know where I was looking or what I was thinking...
If we delete the key for the latest height, do we need to also make an appropriate call to c.latestHeight.Store(??)
?
If this is indeed the case, please add a unit test for it.
If not, would just appreciate a quick (no need for anything big) explanation.
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.
Gotcha, good question. I don't think we need to update c.latestHeight
, regardless of which key is being deleted. For Get
s, this should be completely transparent to the caller; for Set
s, it means that if you call Delete
, followed by Set
(as opposed to SetAtHeight
), then the height you're setting will be the last c.latestHeight
. I think this behavior is correct.
If not, would just appreciate a quick (no need for anything big) explanation.
I did see 👆 but had to think it through for myself anyways:
flowchart LR
subgraph hc2[Historical Cache]
lh2["latestHeight: 2"]
subgraph k2_2h["key2 history"]
k2_2h1["height: 1; value 100"]
end
subgraph k2_1h["key1 history"]
k2_1h1["height: 1; value 10"]
k2_1h2["height: 2; value 20"]
end
end
subgraph hc3[Historical Cache]
lh3["latestHeight: 2"]
subgraph k3_2h["key2 history"]
k3_2h1["height: 1; value 100"]
end
end
subgraph hc4[Historical Cache]
lh4["latestHeight: 2"]
subgraph k4_1h["key1 history"]
k4_1h2["height: 2; value 30"]
end
end
hc2 --"Delete(key1)"--> hc3
hc3 --"Set(key1, 30)"--> hc4
lh2 --> k2_1h2
lh4 --> k4_1h2
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.
Given the explanation and diagram, I don't fully understand the need for latestHeight
at all. In fact, I think we should delete it now if the behaviour you described is expected.
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 failed to reconcile with this thread but after discussing with @red-0ne, we decided that it should be an error to call Set()
on a HistoricalQueryCache
. I think this indicates that HistoricalQueryCache
SHOULD NOT embed QueryCache
after all. Subsequently, latestVersion
is no longer necessary and can be removed.
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.
// 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 the most recent query response.
type QueryCache[T any] interface {
Get(key string) (T, error)
Set(key string, value T) error
Delete(key string)
Clear()
}
// HistoricalQueryCache extends QueryCache to support getting and setting values
// at multiple heights for a given key.
type HistoricalQueryCache[T any] interface {
// GetLatestVersion retrieves the historical value with the highest version number.
GetLatestVersion(key string) (T, error)
// GetAsOfVersion retrieves the nearest value <= the specified version number.
GetAsOfVersion(key string, version int64) (T, error)
// SetAsOfVersion adds or updates a value at a specific version number.
SetAsOfVersion(key string, value T, version int64) error
}
(class diagrams also updated)
Co-authored-by: Daniel Olshansky <[email protected]>
4ac0eef
to
011d2d7
Compare
011d2d7
to
d5ce62f
Compare
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.
Love the iteration. Thanks @bryanchriswhite!
Left a few new comments, a few replies to older threads, but it should be g2g after the next round 🙌
pkg/client/query/cache/memory.go
Outdated
} | ||
|
||
// GetAtHeight retrieves the value from the cache with the given key, at the given | ||
// height. If a value is not found for that height, the value at the nearest previous |
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.
When I see a function like GetAtHeight
, my immediate thought is that it'll return the value at the height provided (if available) or nil otherwise.
For height=5
, it's the difference between select * from table where height=5
and select * from table where height <= 5
.
There could be business logic that depends on state transitions AT A SPECIFIC HEIGHT, so I'm just afraid that it's not explicit enough.
pkg/client/query/cache/memory.go
Outdated
c.itemsMu.Lock() | ||
defer c.itemsMu.Unlock() | ||
|
||
delete(c.items, key) |
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.
Yea, this comment doesn't make sense if you don't know where I was looking or what I was thinking...
If we delete the key for the latest height, do we need to also make an appropriate call to c.latestHeight.Store(??)
?
If this is indeed the case, please add a unit test for it.
If not, would just appreciate a quick (no need for anything big) explanation.
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.
Great caching design.
Left some comments but did not tend to pkg/client/query/cache/memory_test.go
yet. Which I'll do right after this one.
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.
Asking for a small change, but otherwise LGTM.
Not approving yet as I want review the tests too.
// 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) |
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.
Can we go with one of the following instead:
- GetVersioned
- GetAtVersion
- GetHistorical
Don't care which one but AsOf
in a function name just doesn't feel right.
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.
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.
Also adding GetLatestVersion
.
// 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 |
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 sentence is hard to grok.
Any chance you could improve it with an example?
items map[string]any | ||
type inMemoryCache[T any] struct { | ||
config queryCacheConfig | ||
latestVersion atomic.Int64 |
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.
Per my other comment, can you #PUC why we need this and how its used?
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.
🫡 😅
Summary
Adds the
QueryCache[T any]
andHistoricalQueryCache[T any]
interfaces,InMemoryCache[T any]
implementation, configurations, and options functions.Issue
Type of change
Select one or more from the following:
consensus-breaking
label if so. See [Infra] Automatically add theconsensus-breaking
label #791 for detailsTesting
make docusaurus_start
; only needed if you make doc changesmake go_develop_and_test
make test_e2e
devnet-test-e2e
label to the PR.Sanity Checklist