Skip to content

[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

Merged
merged 12 commits into from
Sep 13, 2023

Conversation

mikeland73
Copy link
Contributor

@mikeland73 mikeland73 commented Sep 12, 2023

Summary

This change simplifies code, eliminates manual locks. It:

  • uses sync.Map
  • Changes fillNarInfoCache to be fillNarInfoCacheIfNeeded
  • Simplifies IsInBinaryCache
  • Simplifies goroutine in 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

@mikeland73 mikeland73 requested review from gcurtis and savil September 12, 2023 19:47
@mikeland73 mikeland73 marked this pull request as draft September 12, 2023 19:54
@mikeland73

This comment was marked as resolved.

@mikeland73 mikeland73 changed the title [narInfoCache] Simplify IsInBinaryCache [narInfoCache] Use sync.Map and simplify IsInBinaryCache Sep 12, 2023
@mikeland73 mikeland73 marked this pull request as ready for review September 12, 2023 20:08
@mikeland73 mikeland73 requested a review from savil September 12, 2023 20:14
Copy link
Collaborator

@gcurtis gcurtis left a 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.Onces 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)()

Comment on lines 90 to 93
if status, alreadySet := isNarInfoInCache.Load(p.Raw); alreadySet {
return status.(bool), nil
}
isNarInfoInCache.Store(p.Raw, false) // store in case of failure
Copy link
Collaborator

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.

Copy link
Contributor Author

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)

Comment on lines 93 to 96
defer func() {
// If there's an error and nothing got cached, just store false.
_, _ = isNarInfoInCache.LoadOrStore(p.Raw, false)
}()
Copy link
Contributor Author

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.

Copy link
Collaborator

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.Onces? I think that would address this.

Copy link
Collaborator

@savil savil left a 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!

Comment on lines 93 to 96
defer func() {
// If there's an error and nothing got cached, just store false.
_, _ = isNarInfoInCache.LoadOrStore(p.Raw, false)
}()
Copy link
Collaborator

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.Onces? I think that would address this.

@mikeland73
Copy link
Contributor Author

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?

This was in reference to the first version of this PR (and @gcurtis's comment). In the first version if fillNarInfoCacheIfNeeded got called twice simultaneously, it's possbie the first one would initialize the status to false, and the second one would return false right away, even thought he ultimate value would be true. Using defer which does the cleanup at the end eliminates this case.

Did you try @gcurtis 's suggestion of using sync.Onces? I think that would address this.

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.

Comment on lines 86 to 92
f, ok := narInfoStatusFnCache.Load(p.Raw)
if !ok {
f = inCacheFunc(sync.OnceValues(func() (bool, error) {
return p.fetchNarInfoStatus()
}))
f, _ = narInfoStatusFnCache.LoadOrStore(p.Raw, f)
}
Copy link
Contributor Author

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.

@mikeland73
Copy link
Contributor Author

@gcurtis implemented your suggestion. PTAL

Comment on lines 88 to 90
f = inCacheFunc(sync.OnceValues(func() (bool, error) {
return p.fetchNarInfoStatus()
}))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
f = inCacheFunc(sync.OnceValues(func() (bool, error) {
return p.fetchNarInfoStatus()
}))
f = inCacheFunc(sync.OnceValues(p.fetchNarInfoStatus))

@mikeland73 mikeland73 merged commit 62f25d0 into main Sep 13, 2023
@mikeland73 mikeland73 deleted the landau/simplify branch September 13, 2023 19:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants