-
Notifications
You must be signed in to change notification settings - Fork 249
[narInfoCache] Use sync.Map and simplify IsInBinaryCache #1473
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
Conversation
This comment was marked as resolved.
This comment was marked as resolved.
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.
If the cache is just a global variable, using a map of sync.Once
s might simplify things.
(I haven't tested any of this)
var cache sync.Map
func isInBinaryCache(key string) (bool, error) {
type inCacheFunc func() (bool, error)
f, ok := cache.Load(key)
if !ok {
f = inCacheFunc(sync.OnceValues(func() (bool, error) { /* check cache */ }))
f, _ = cache.LoadOrStore(key, f)
}
return f.(inCacheFunc)()
internal/devpkg/narinfo_cache.go
Outdated
if status, alreadySet := isNarInfoInCache.Load(p.Raw); alreadySet { | ||
return status.(bool), nil | ||
} | ||
isNarInfoInCache.Store(p.Raw, false) // store in case of failure |
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 could be a race here where multiple goroutines see alreadySet == false
before reaching line 93. Or one goroutine might reach line 93 and then another sees status.(bool) == false
on line 91.
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.
Good call. I think our calling patterns make this unlikely but better to avoid if possible.
I think using defer
might solve most race conditions. It's not immune to a success following a failure (but neither is the current code so it would not be a regression)
internal/devpkg/narinfo_cache.go
Outdated
defer func() { | ||
// If there's an error and nothing got cached, just store false. | ||
_, _ = isNarInfoInCache.LoadOrStore(p.Raw, false) | ||
}() |
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.
@gcurtis @savil using defer here is functionally identical to what was previously handled in line 119 except now it is handled 100% of the time.
This in conjunction with LoadOrStore
in line 125 guarantee that the first result is preserved forever (so a race condition cannot replace an existing value). This fixes an existing race condition where two calls to fillNarInfoCacheIfNeeded
could override each other if they had different results.
This fix also removes the race condition where an early false
causes another goroutine to return false
early.
It partially* fixes multiple goroutines trying to fetch the same result at the same time. In that case only the first result is saved and other goroutines return the same result. Previously, each call to fillNarInfoCacheIfNeeded
could return a different result.
- I say "partially" because ideally we don't make 2 http requests, but I think that's not a big deal so unless the fix is easy, I would avoid complicating the 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.
This fix also removes the race condition where an early false causes another goroutine to return false early.
I don't quite understand this scenario. Care to elaborate?
I say "partially" because ideally we don't make 2 http requests, but I think that's not a big deal so unless the fix is easy, I would avoid complicating the code.
Did you try @gcurtis 's suggestion of using sync.Once
s? I think that would address this.
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.
@mikeland73 thanks for simplifying and hardening this code!
internal/devpkg/narinfo_cache.go
Outdated
defer func() { | ||
// If there's an error and nothing got cached, just store false. | ||
_, _ = isNarInfoInCache.LoadOrStore(p.Raw, false) | ||
}() |
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 fix also removes the race condition where an early false causes another goroutine to return false early.
I don't quite understand this scenario. Care to elaborate?
I say "partially" because ideally we don't make 2 http requests, but I think that's not a big deal so unless the fix is easy, I would avoid complicating the code.
Did you try @gcurtis 's suggestion of using sync.Once
s? I think that would address this.
This was in reference to the first version of this PR (and @gcurtis's comment). In the first version if
I think his solution is better because it guarantees only one call per package. I'll hold off on merging and see if I can get it to work. |
internal/devpkg/narinfo_cache.go
Outdated
f, ok := narInfoStatusFnCache.Load(p.Raw) | ||
if !ok { | ||
f = inCacheFunc(sync.OnceValues(func() (bool, error) { | ||
return p.fetchNarInfoStatus() | ||
})) | ||
f, _ = narInfoStatusFnCache.LoadOrStore(p.Raw, f) | ||
} |
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 could replace this with a single LoadOrStore but that would create throwaway OnceValues function every time.
@gcurtis implemented your suggestion. PTAL |
internal/devpkg/narinfo_cache.go
Outdated
f = inCacheFunc(sync.OnceValues(func() (bool, error) { | ||
return p.fetchNarInfoStatus() | ||
})) |
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.
f = inCacheFunc(sync.OnceValues(func() (bool, error) { | |
return p.fetchNarInfoStatus() | |
})) | |
f = inCacheFunc(sync.OnceValues(p.fetchNarInfoStatus)) |
Summary
This change simplifies code, eliminates manual locks. It:
fillNarInfoCache
to befillNarInfoCacheIfNeeded
IsInBinaryCache
FillNarInfoCache
because we no longer have to check for values that are already set and we don't have to handle error.How was it tested?
devbox run build