Merge pull request #4824 from MichaelEischer/fix-cache-race

bloblru: Fix flaky test due to race condition
This commit is contained in:
Michael Eischer 2024-05-30 13:52:34 +02:00 committed by GitHub
commit 3d4a620089
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -97,10 +97,16 @@ func (c *Cache) GetOrCompute(id restic.ID, compute func() ([]byte, error)) ([]by
// check for parallel download or start our own // check for parallel download or start our own
finish := make(chan struct{}) finish := make(chan struct{})
c.mu.Lock() c.mu.Lock()
waitForResult, isDownloading := c.inProgress[id] waitForResult, isComputing := c.inProgress[id]
if !isDownloading { if !isComputing {
c.inProgress[id] = finish c.inProgress[id] = finish
}
c.mu.Unlock()
if isComputing {
// wait for result of parallel download
<-waitForResult
} else {
// remove progress channel once finished here // remove progress channel once finished here
defer func() { defer func() {
c.mu.Lock() c.mu.Lock()
@ -109,15 +115,18 @@ func (c *Cache) GetOrCompute(id restic.ID, compute func() ([]byte, error)) ([]by
close(finish) close(finish)
}() }()
} }
c.mu.Unlock()
if isDownloading { // try again. This is necessary independent of whether isComputing is true or not.
// wait for result of parallel download // The calls to `c.Get()` and checking/adding the entry in `c.inProgress` are not atomic,
<-waitForResult // thus the item might have been computed in the meantime.
blob, ok := c.Get(id) // The following scenario would compute() the value multiple times otherwise:
if ok { // Goroutine A does not find a value in the initial call to `c.Get`, then goroutine B
return blob, nil // takes over, caches the computed value and cleans up its channel in c.inProgress.
} // Then goroutine A continues, does not detect a parallel computation and would try
// to call compute() again.
blob, ok = c.Get(id)
if ok {
return blob, nil
} }
// download it // download it