-
Notifications
You must be signed in to change notification settings - Fork 2k
Add FnCache.GetIfExists method #61776
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
base: master
Are you sure you want to change the base?
Conversation
daa8545 to
cde9681
Compare
cde9681 to
bd288bc
Compare
| // return false immediately without blocking. Get returns false for entries that | ||
| // contain errors. | ||
| // For most of the cases the FnCacheGet function should be used instead. | ||
| func (c *FnCache) GetIfExists(key any) (any, bool) { |
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 make this a free function similar to FnCacheGet instead of a receiver so that we can leverage generics and eliminate the need to cast from an any by the caller?
| func (c *FnCache) GetIfExists(key any) (any, bool) { | |
| func FnCacheGetIfExists[K comparable, T any](cache *FnCache, key K) (T, bool) { |
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.
See the PR desc: #61776 (comment)
Why not a global function like: utils.FnCacheGetIfExists[K comparable, T any](cache *FnCache, key K) (T, bool) { ?
The main reason is cumbersome usageutils.FnCacheGetIfExists[string, string](cache, key) where you need to define return types because compiler can't deduce this types (I have prototyped this and after seeing the function call signature I decided to revert back to cache.GetIfExists that is much readable in case of readability for me vs FnCacheGetIfExists[string, string](cache, key) callback )
Also cache already have Public functions like cache.Set cache.Remove so the cache.Get... method seems to be missing.
Where even if we have FnCacheGet(cache) function you can still call cache.Set() or cache.Remove and the free function doesn't restrict the cache.Public Function usage.
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.
Hrmm, that doesn't appear to be the case for existing calls to FnCacheGet - why is FnCacheGetIfExists different?
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.
Please see the PR description where this is explained:
Why not a global function like: utils.FnCacheGetIfExists[K comparable, T any](cache *FnCache, key K) (T, bool) { ?
... go compiler can't deduce the generic type during the call so the call will look like FnCacheGetIfExists[string, string](cache, key)
So having raw call withut explict types nCacheGetIfExistsstring, string like:
FnCacheGetIfExists(cache, "test-key")
gives compiler error
in call to FnCacheGetIfExists, cannot infer T (declared at ./fncache.go:185:39)
You can find many articles how GO generic types are deduced. In this case it is not possible.
So you need to call FnCacheGetIfExists[string, string](cache, "test-key") with explicit types provided [string, string] that is very ugly and obscure for the usage and is quite easy to misused the 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.
Why is this not the case for existing calls to FnCacheGet?
https://github.com/search?q=repo%3Agravitational%2Fteleport%20utils.FnCacheGet&type=code
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.
Go complier can't infer function type arguments from function like func FnCacheGetIfExists[K comparable, T any](cache *FnCache, key K) (T, bool) {
where FnCacheGetIfExists and FnCacheGet have totally diffrent signature.
One more time please see PR desc:
Why not a global function like: utils.FnCacheGetIfExists[K comparable, T any](cache *FnCache, key K) (T, bool) { ?
... go compiler can't deduce the generic type during the call ...
Where I would like avoid spending time discussing go compiler limitation
So as I said in #61776 (comment) to make it work you need to specify the [string, string] type explicitly: FnCacheGetIfExists[string, string](cache, "test-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.
I think the issue here is the following. The existing FnCacheGet method has the following signature:
func FnCacheGet[K comparable, T any](ctx context.Context, cache *FnCache, key K, loadfn func(ctx context.Context) (T, error)) (T, error)Here, the T type is present as a part of the loadfn callback function among the method's input parameters, so the Go compiler can infer its type from there.
The new one being added has the following signature:
func FnCacheGetIfExists[K comparable, T any](cache *FnCache, key K) (T, bool)The difference is that here, T is not among input arguments so Go compiler can't infer its type and hence it has to be specified explicitly at the call site.
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 understand the motivation for the change, I was seeking an answer as to what was different between FnCacheGet and FnCacheGetIfExists that allows the former to work but not the latter. The answer is that callers define T explicitly in the loadFn provided to FnCacheGet. I think that information would've been helpful to include in the PR description.
In regards to the generic types being specified at the call site explicitly, is that truly worse than the alternative. Consider the following two contrived examples.
valueAny, ok := cache.GetIfExists("foo")
if ok {
if foo, ok := valueAny.(string); ok {
// use foo here
}
}
if foo, ok := FnCacheGetIfExists[string, string](cache, "foo"); ok {
}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.
So you need to call FnCacheGetIfExistsstring, string with explicit types provided [string, string] that is very ugly and obscure for the usage and is quite easy to misused the types
I prefer:
bucketSet, ok := l.rateLimits.GetIfExists(token)
if !ok {
return false
}
bucket, ok := bucketSet.(*ratelimit.TokenBucketSet)
if !ok {
return false
}
...
vs:
bucketSet, ok := FnCacheGetIfExists[string, *ratelimit.TokenBucketSet)](cache, "foo")
if !ok {
return false
}
because FnCacheGetIfExists[string, *ratelimit.TokenBucketSet)]( l.rateLimits., "foo") is very confusing for me.
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've no strong preference towards one way or another being discussed in the thread here but @smallinsky I'm wondering if you considered a potential alternative - can we just use existing FnCacheGet method and provide it a no-op load function in cases where we don't want to load the object on miss? Would it work for your use-case?
| // return false immediately without blocking. Get returns false for entries that | ||
| // contain errors. | ||
| // For most of the cases the FnCacheGet function should be used instead. | ||
| func (c *FnCache) GetIfExists(key any) (any, bool) { |
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 think the issue here is the following. The existing FnCacheGet method has the following signature:
func FnCacheGet[K comparable, T any](ctx context.Context, cache *FnCache, key K, loadfn func(ctx context.Context) (T, error)) (T, error)Here, the T type is present as a part of the loadfn callback function among the method's input parameters, so the Go compiler can infer its type from there.
The new one being added has the following signature:
func FnCacheGetIfExists[K comparable, T any](cache *FnCache, key K) (T, bool)The difference is that here, T is not among input arguments so Go compiler can't infer its type and hence it has to be specified explicitly at the call site.
I think that having explicit behavior via named function is more straightforward that injecting the empty callback to the FnCacheGet and mixing the logic in one flow. But I don't have strong preference |
r0mant
left a comment
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 think that having explicit behavior via named function is more straightforward that injecting the empty callback to the FnCacheGet and mixing the logic in one flow.
But I don't have strong preference
lol yeah, me neither. I think in the case where neither side has a strong preference, going with the author's preference is reasonable.
What
Add GetIfExists method that allows to "Peek" for the cache key without triggering the Cache key creation function.
What is the difference between and
FnCacheGetvsGetIfExistsThe
FnCacheGetalways triggers keys creation if key don't exist in cache where FnCacheGetIfExists is designed to not change the state cache and just return existing state.Why this function is needed. Do we have any buisness case for this peeking cache funtionality ?
Yes, There are severals business cases where we want to check if key exist without creating a value in cache like logic where produce and consumer are independed, like checking rate limits for IP key without applying rate limit to IP key.
See #61788 for the Usage
Why not a global function like:
utils.FnCacheGetIfExists[K comparable, T any](cache *FnCache, key K) (T, bool) {?The global funtion usage is every obscure because
go compilercan't deduce the generic type during the call so the call will look likeFnCacheGetIfExists[string, string](cache, key)vscache.GetIfExists(key)Where currently cache have methods like
cache.Setcache.Removeso thecache.Get...method is missing.