rclone/lib/cache/cache_test.go
Nick Craig-Wood bfcf6baf93 lib/cache: fix locking so we don't try to create the item many times
Before this fix, if several Get requests were submitted very quickly,
this could run the item create function multiple times due to the
unlock of the mutex in the creation code.

This fixes the problem by having a mutex in each cache entry which is
held when the item is being created.
2021-02-05 19:08:00 +00:00

283 lines
5.3 KiB
Go

package cache
import (
"errors"
"fmt"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
called = int32(0)
errSentinel = errors.New("an error")
errCached = errors.New("a cached error")
)
func setup(t *testing.T) (*Cache, CreateFunc) {
called = 0
create := func(path string) (interface{}, bool, error) {
newCalled := atomic.AddInt32(&called, 1)
assert.Equal(t, int32(1), newCalled)
switch path {
case "/":
time.Sleep(100 * time.Millisecond)
return "/", true, nil
case "/file.txt":
return "/file.txt", true, errCached
case "/error":
return nil, false, errSentinel
}
assert.Fail(t, fmt.Sprintf("Unknown path %q", path))
return nil, false, nil
}
c := New()
return c, create
}
func TestGet(t *testing.T) {
c, create := setup(t)
assert.Equal(t, 0, len(c.cache))
f, err := c.Get("/", create)
require.NoError(t, err)
assert.Equal(t, 1, len(c.cache))
f2, err := c.Get("/", create)
require.NoError(t, err)
assert.Equal(t, f, f2)
}
func TestGetConcurrent(t *testing.T) {
c, create := setup(t)
assert.Equal(t, 0, len(c.cache))
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_, err := c.Get("/", create)
require.NoError(t, err)
}()
}
wg.Wait()
assert.Equal(t, 1, len(c.cache))
}
func TestGetFile(t *testing.T) {
c, create := setup(t)
assert.Equal(t, 0, len(c.cache))
f, err := c.Get("/file.txt", create)
require.Equal(t, errCached, err)
assert.Equal(t, 1, len(c.cache))
f2, err := c.Get("/file.txt", create)
require.Equal(t, errCached, err)
assert.Equal(t, f, f2)
}
func TestGetError(t *testing.T) {
c, create := setup(t)
assert.Equal(t, 0, len(c.cache))
f, err := c.Get("/error", create)
require.Equal(t, errSentinel, err)
require.Equal(t, nil, f)
assert.Equal(t, 0, len(c.cache))
}
func TestPut(t *testing.T) {
c, create := setup(t)
assert.Equal(t, 0, len(c.cache))
c.Put("/alien", "slime")
assert.Equal(t, 1, len(c.cache))
fNew, err := c.Get("/alien", create)
require.NoError(t, err)
require.Equal(t, "slime", fNew)
assert.Equal(t, 1, len(c.cache))
}
func TestCacheExpire(t *testing.T) {
c, create := setup(t)
c.expireInterval = time.Millisecond
assert.Equal(t, false, c.expireRunning)
_, err := c.Get("/", create)
require.NoError(t, err)
c.mu.Lock()
entry := c.cache["/"]
assert.Equal(t, 1, len(c.cache))
c.mu.Unlock()
c.cacheExpire()
c.mu.Lock()
assert.Equal(t, 1, len(c.cache))
entry.lastUsed = time.Now().Add(-c.expireDuration - 60*time.Second)
assert.Equal(t, true, c.expireRunning)
c.mu.Unlock()
time.Sleep(250 * time.Millisecond)
c.mu.Lock()
assert.Equal(t, false, c.expireRunning)
assert.Equal(t, 0, len(c.cache))
c.mu.Unlock()
}
func TestCachePin(t *testing.T) {
c, create := setup(t)
_, err := c.Get("/", create)
require.NoError(t, err)
// Pin a non existent item to show nothing happens
c.Pin("notfound")
c.mu.Lock()
entry := c.cache["/"]
assert.Equal(t, 1, len(c.cache))
c.mu.Unlock()
c.cacheExpire()
c.mu.Lock()
assert.Equal(t, 1, len(c.cache))
c.mu.Unlock()
// Pin the entry and check it does not get expired
c.Pin("/")
// Reset last used to make the item expirable
c.mu.Lock()
entry.lastUsed = time.Now().Add(-c.expireDuration - 60*time.Second)
c.mu.Unlock()
c.cacheExpire()
c.mu.Lock()
assert.Equal(t, 1, len(c.cache))
c.mu.Unlock()
// Unpin the entry and check it does get expired now
c.Unpin("/")
// Reset last used
c.mu.Lock()
entry.lastUsed = time.Now().Add(-c.expireDuration - 60*time.Second)
c.mu.Unlock()
c.cacheExpire()
c.mu.Lock()
assert.Equal(t, 0, len(c.cache))
c.mu.Unlock()
}
func TestClear(t *testing.T) {
c, create := setup(t)
assert.Equal(t, 0, len(c.cache))
_, err := c.Get("/", create)
require.NoError(t, err)
assert.Equal(t, 1, len(c.cache))
c.Clear()
assert.Equal(t, 0, len(c.cache))
}
func TestEntries(t *testing.T) {
c, create := setup(t)
assert.Equal(t, 0, c.Entries())
_, err := c.Get("/", create)
require.NoError(t, err)
assert.Equal(t, 1, c.Entries())
c.Clear()
assert.Equal(t, 0, c.Entries())
}
func TestGetMaybe(t *testing.T) {
c, create := setup(t)
value, found := c.GetMaybe("/")
assert.Equal(t, false, found)
assert.Nil(t, value)
f, err := c.Get("/", create)
require.NoError(t, err)
value, found = c.GetMaybe("/")
assert.Equal(t, true, found)
assert.Equal(t, f, value)
c.Clear()
value, found = c.GetMaybe("/")
assert.Equal(t, false, found)
assert.Nil(t, value)
}
func TestCacheRename(t *testing.T) {
c := New()
create := func(path string) (interface{}, bool, error) {
return path, true, nil
}
existing1, err := c.Get("existing1", create)
require.NoError(t, err)
_, err = c.Get("existing2", create)
require.NoError(t, err)
assert.Equal(t, 2, c.Entries())
// rename to non existent
value, found := c.Rename("existing1", "EXISTING1")
assert.Equal(t, true, found)
assert.Equal(t, existing1, value)
assert.Equal(t, 2, c.Entries())
// rename to existent and check existing value is returned
value, found = c.Rename("existing2", "EXISTING1")
assert.Equal(t, true, found)
assert.Equal(t, existing1, value)
assert.Equal(t, 1, c.Entries())
// rename non existent
value, found = c.Rename("notfound", "NOTFOUND")
assert.Equal(t, false, found)
assert.Nil(t, value)
assert.Equal(t, 1, c.Entries())
}