/* * * Copyright 2019 gRPC authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package cache import ( "strconv" "sync" "testing" "time" "google.golang.org/grpc/internal/grpctest" ) const ( testCacheTimeout = 100 * time.Millisecond ) type s struct { grpctest.Tester } func Test(t *testing.T) { grpctest.RunSubTests(t, s{}) } func (c *TimeoutCache) getForTesting(key interface{}) (*cacheEntry, bool) { c.mu.Lock() defer c.mu.Unlock() r, ok := c.cache[key] return r, ok } // TestCacheExpire attempts to add an entry to the cache and verifies that it // was added successfully. It then makes sure that on timeout, it's removed and // the associated callback is called. func (s) TestCacheExpire(t *testing.T) { const k, v = 1, "1" c := NewTimeoutCache(testCacheTimeout) callbackChan := make(chan struct{}) c.Add(k, v, func() { close(callbackChan) }) if gotV, ok := c.getForTesting(k); !ok || gotV.item != v { t.Fatalf("After Add(), before timeout, from cache got: %v, %v, want %v, %v", gotV.item, ok, v, true) } select { case <-callbackChan: case <-time.After(testCacheTimeout * 2): t.Fatalf("timeout waiting for callback") } if _, ok := c.getForTesting(k); ok { t.Fatalf("After Add(), after timeout, from cache got: _, %v, want _, %v", ok, false) } } // TestCacheRemove attempts to remove an existing entry from the cache and // verifies that the entry is removed and the associated callback is not // invoked. func (s) TestCacheRemove(t *testing.T) { const k, v = 1, "1" c := NewTimeoutCache(testCacheTimeout) callbackChan := make(chan struct{}) c.Add(k, v, func() { close(callbackChan) }) if got, ok := c.getForTesting(k); !ok || got.item != v { t.Fatalf("After Add(), before timeout, from cache got: %v, %v, want %v, %v", got.item, ok, v, true) } time.Sleep(testCacheTimeout / 2) gotV, gotOK := c.Remove(k) if !gotOK || gotV != v { t.Fatalf("After Add(), before timeout, Remove() got: %v, %v, want %v, %v", gotV, gotOK, v, true) } if _, ok := c.getForTesting(k); ok { t.Fatalf("After Add(), before timeout, after Remove(), from cache got: _, %v, want _, %v", ok, false) } select { case <-callbackChan: t.Fatalf("unexpected callback after retrieve") case <-time.After(testCacheTimeout * 2): } } // TestCacheClearWithoutCallback attempts to clear all entries from the cache // and verifies that the associated callbacks are not invoked. func (s) TestCacheClearWithoutCallback(t *testing.T) { var values []string const itemCount = 3 for i := 0; i < itemCount; i++ { values = append(values, strconv.Itoa(i)) } c := NewTimeoutCache(testCacheTimeout) done := make(chan struct{}) defer close(done) callbackChan := make(chan struct{}, itemCount) for i, v := range values { callbackChanTemp := make(chan struct{}) c.Add(i, v, func() { close(callbackChanTemp) }) go func() { select { case <-callbackChanTemp: callbackChan <- struct{}{} case <-done: } }() } for i, v := range values { if got, ok := c.getForTesting(i); !ok || got.item != v { t.Fatalf("After Add(), before timeout, from cache got: %v, %v, want %v, %v", got.item, ok, v, true) } } time.Sleep(testCacheTimeout / 2) c.Clear(false) for i := range values { if _, ok := c.getForTesting(i); ok { t.Fatalf("After Add(), before timeout, after Remove(), from cache got: _, %v, want _, %v", ok, false) } } select { case <-callbackChan: t.Fatalf("unexpected callback after Clear") case <-time.After(testCacheTimeout * 2): } } // TestCacheClearWithCallback attempts to clear all entries from the cache and // verifies that the associated callbacks are invoked. func (s) TestCacheClearWithCallback(t *testing.T) { var values []string const itemCount = 3 for i := 0; i < itemCount; i++ { values = append(values, strconv.Itoa(i)) } c := NewTimeoutCache(time.Hour) testDone := make(chan struct{}) defer close(testDone) var wg sync.WaitGroup wg.Add(itemCount) for i, v := range values { callbackChanTemp := make(chan struct{}) c.Add(i, v, func() { close(callbackChanTemp) }) go func() { defer wg.Done() select { case <-callbackChanTemp: case <-testDone: } }() } allGoroutineDone := make(chan struct{}, itemCount) go func() { wg.Wait() close(allGoroutineDone) }() for i, v := range values { if got, ok := c.getForTesting(i); !ok || got.item != v { t.Fatalf("After Add(), before timeout, from cache got: %v, %v, want %v, %v", got.item, ok, v, true) } } time.Sleep(testCacheTimeout / 2) c.Clear(true) for i := range values { if _, ok := c.getForTesting(i); ok { t.Fatalf("After Add(), before timeout, after Remove(), from cache got: _, %v, want _, %v", ok, false) } } select { case <-allGoroutineDone: case <-time.After(testCacheTimeout * 2): t.Fatalf("timeout waiting for all callbacks") } } // TestCacheRetrieveTimeoutRace simulates the case where an entry's timer fires // around the same time that Remove() is called for it. It verifies that there // is no deadlock. func (s) TestCacheRetrieveTimeoutRace(t *testing.T) { c := NewTimeoutCache(time.Nanosecond) done := make(chan struct{}) go func() { for i := 0; i < 1000; i++ { // Add starts a timer with 1 ns timeout, then remove will race // with the timer. c.Add(i, strconv.Itoa(i), func() {}) c.Remove(i) } close(done) }() select { case <-time.After(time.Second): t.Fatalf("Test didn't finish within 1 second. Deadlock") case <-done: } }