Change:
Failed due to an apparently unrelated PR: https://travis-
Problem here is that I left a small chance of a double-call in my parallelized cache. Didn't realize this was being tested. Will fix.

Discussion:
} // returns the item and true if it is found and not expired, otherwise nil and false. <ins> // If this returns false, it has locked c.inFlightLock and it is caller's responsibility // to unlock that. </ins> func (c *timeCache) get(key string) (T, bool) { c.lock.RLock() defer c.lock.RUnlock() data, ok := c.cache[key] now := c.clock.Now() if !ok || now.Sub(data.lastUpdate) > c.ttl { <ins> // We must lock this while we hold c.lock-- otherwise, a writer could // write to c.cache and remove the channel from c.inFlight before we // manage to read c.inFlight. c.inFlightLock.Lock() </ins> return nil, false } return data.item, true } <ins> // c.inFlightLock MUST be locked before calling this. fillOrWait will unlock it. </ins> func (c *timeCache) fillOrWait(key string) chan T { <del> c.inFlightLock.Lock() </del> defer c.inFlightLock.Unlock() // Already a call in progress?

result := make(chan T, 1) // non-blocking c.inFlight[key] = result go func() { <del> // Make potentially slow call </del> <ins> // Make potentially slow call. // While this call is in flight, fillOrWait will // presumably exit. </ins> data := timeCacheEntry{ item:       c.fillFunc(key), lastUpdate: c.clock.Now(),

// Store in cache c.lock.Lock() <ins> defer c.lock.Unlock() </ins> c.cache[key] = data <del> c.lock.Unlock() </del> // Remove in flight entry c.inFlightLock.Lock() <ins> defer c.inFlightLock.Unlock() </ins> delete(c.inFlight, key) <del> c.inFlightLock.Unlock() </del> }() return result }

package util import ( <ins> "math/rand" "runtime" </ins> "sync" "testing" "time"

t.Errorf("Expected %v, got %v", e, a) } } <ins> func TestCacheParallelNoDeadlocksNoDoubleCalls(t *testing.T) { // Make 50 random keys keys := []string{} fuzz.New().NilChance(0).NumElements(50, 50).Fuzz(&keys) // Data structure for tracking when each key is accessed. type callTrack struct { sync.Mutex accessTimes []time.Time } calls := map[string]*callTrack{} for _, k := range keys { calls[k] = &callTrack{} } // This is called to fill the cache in the case of a cache miss // or cache entry expiration. We record the time. ff := func(key string) T { ct := calls[key] ct.Lock() ct.accessTimes = append(ct.accessTimes, time.Now()) ct.Unlock() // make sure that there is time for multiple requests to come in // for the same key before this returns. time.Sleep(time.Millisecond) return key } cacheDur := 10 * time.Millisecond c := NewTimeCache(RealClock{}, cacheDur, ff) // Spawn a bunch of goroutines, each of which sequentially requests // 500 random keys from the cache. runtime.GOMAXPROCS(16) var wg sync.WaitGroup for i := 0; i < 500; i++ { wg.Add(1) go func(seed int64) { r := rand.New(rand.NewSource(seed)) for i := 0; i < 500; i++ { c.Get(keys[r.Intn(len(keys))]) } wg.Done() }(rand.Int63()) } wg.Wait() // Since the cache should hold things for 10ms, no calls for a given key // should be more closely spaced than that. for k, ct := range calls { if len(ct.accessTimes) < 2 { continue } cur := ct.accessTimes[0] for i := 1; i < len(ct.accessTimes); i++ { next := ct.accessTimes[i] if next.Sub(cur) < cacheDur { t.Errorf("%v was called at %v and %v", k, cur, next) } cur = next } } } </ins>