mirror of
https://github.com/rclone/rclone.git
synced 2026-10-09 14:39:28 -04:00
An entry is expired when it hasn't been used for a while, but the user
of the cache may know it is still doing something in the background.
SetCanExpire sets a function which is asked before expiring an entry
and can keep it in the cache until it is ready to go.
The function is called without the cache locked as it may take a
while, so an entry which is used in the meantime isn't expired.
(cherry picked from commit 49d8566ec5)
447 lines
8.8 KiB
Go
447 lines
8.8 KiB
Go
package cache
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var (
|
|
called = 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) (any, bool, error) {
|
|
assert.Equal(t, 0, called)
|
|
called++
|
|
switch path {
|
|
case "/":
|
|
return "/", true, nil
|
|
case "/file.txt":
|
|
return "/file.txt", true, errCached
|
|
case "/error":
|
|
return nil, false, errSentinel
|
|
case "/err":
|
|
return nil, false, errSentinel
|
|
}
|
|
panic(fmt.Sprintf("Unknown path %q", path))
|
|
}
|
|
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 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 TestPutErr(t *testing.T) {
|
|
c, create := setup(t)
|
|
|
|
assert.Equal(t, 0, len(c.cache))
|
|
|
|
c.PutErr("/alien", "slime", errSentinel)
|
|
|
|
assert.Equal(t, 1, len(c.cache))
|
|
|
|
fNew, err := c.Get("/alien", create)
|
|
require.Equal(t, errSentinel, err)
|
|
require.Equal(t, "slime", fNew)
|
|
|
|
assert.Equal(t, 1, 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.SetExpireInterval(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 TestCacheNoExpire(t *testing.T) {
|
|
c, create := setup(t)
|
|
|
|
assert.False(t, c.noCache())
|
|
|
|
c.SetExpireDuration(0)
|
|
assert.Equal(t, false, c.expireRunning)
|
|
|
|
assert.True(t, c.noCache())
|
|
|
|
f, err := c.Get("/", create)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, f)
|
|
|
|
c.mu.Lock()
|
|
assert.Equal(t, 0, len(c.cache))
|
|
c.mu.Unlock()
|
|
|
|
c.Put("/alien", "slime")
|
|
|
|
c.mu.Lock()
|
|
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 nonexistent 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 TestDelete(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))
|
|
|
|
assert.Equal(t, false, c.Delete("notfound"))
|
|
assert.Equal(t, 1, len(c.cache))
|
|
|
|
assert.Equal(t, true, c.Delete("/"))
|
|
assert.Equal(t, 0, len(c.cache))
|
|
|
|
assert.Equal(t, false, c.Delete("/"))
|
|
assert.Equal(t, 0, len(c.cache))
|
|
}
|
|
|
|
func TestDeletePrefix(t *testing.T) {
|
|
create := func(path string) (any, bool, error) {
|
|
return path, true, nil
|
|
}
|
|
c := New()
|
|
|
|
_, err := c.Get("remote:path", create)
|
|
require.NoError(t, err)
|
|
_, err = c.Get("remote:path2", create)
|
|
require.NoError(t, err)
|
|
_, err = c.Get("remote:", create)
|
|
require.NoError(t, err)
|
|
_, err = c.Get("remote", create)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 4, len(c.cache))
|
|
|
|
assert.Equal(t, 3, c.DeletePrefix("remote:"))
|
|
assert.Equal(t, 1, len(c.cache))
|
|
|
|
assert.Equal(t, 1, c.DeletePrefix(""))
|
|
assert.Equal(t, 0, len(c.cache))
|
|
|
|
assert.Equal(t, 0, c.DeletePrefix(""))
|
|
assert.Equal(t, 0, len(c.cache))
|
|
}
|
|
|
|
func TestCacheRename(t *testing.T) {
|
|
c := New()
|
|
create := func(path string) (any, 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 nonexistent
|
|
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 nonexistent
|
|
value, found = c.Rename("notfound", "NOTFOUND")
|
|
assert.Equal(t, false, found)
|
|
assert.Nil(t, value)
|
|
|
|
assert.Equal(t, 1, c.Entries())
|
|
}
|
|
|
|
func TestCacheFinalize(t *testing.T) {
|
|
c := New()
|
|
numCalled := 0
|
|
c.SetFinalizer(func(v any) {
|
|
numCalled++
|
|
})
|
|
create := func(path string) (any, bool, error) {
|
|
return path, true, nil
|
|
}
|
|
_, _ = c.Get("ok", create)
|
|
assert.Equal(t, 0, numCalled)
|
|
c.Clear()
|
|
assert.Equal(t, 1, numCalled)
|
|
|
|
_, _ = c.Get("ok", create)
|
|
c.Delete("ok")
|
|
assert.Equal(t, 2, numCalled)
|
|
|
|
_, _ = c.Get("ok", create)
|
|
c.DeletePrefix("ok")
|
|
assert.Equal(t, 3, numCalled)
|
|
|
|
_, _ = c.Get("old", create)
|
|
_, _ = c.Get("new", create)
|
|
c.Rename("old", "new")
|
|
assert.Equal(t, 4, numCalled)
|
|
|
|
c.expireDuration = 1 * time.Millisecond
|
|
_, _ = c.Get("ok", create)
|
|
time.Sleep(2 * time.Millisecond)
|
|
c.cacheExpire() // "ok" and "new" fall out of cache
|
|
assert.Equal(t, 6, numCalled)
|
|
}
|
|
|
|
func TestCacheCanExpire(t *testing.T) {
|
|
c := New()
|
|
finalized := 0
|
|
c.SetFinalizer(func(v any) {
|
|
finalized++
|
|
})
|
|
busy := true
|
|
c.SetCanExpire(func(v any) bool {
|
|
// Must be callable without the lock
|
|
assert.Equal(t, 2, c.Entries())
|
|
return v != "busy" || !busy
|
|
})
|
|
create := func(path string) (any, bool, error) {
|
|
return path, true, nil
|
|
}
|
|
c.expireDuration = 1 * time.Millisecond
|
|
_, _ = c.Get("busy", create)
|
|
_, _ = c.Get("pinned", create)
|
|
c.Pin("pinned")
|
|
time.Sleep(2 * time.Millisecond)
|
|
|
|
// Neither the busy nor the pinned entry expire
|
|
c.cacheExpire()
|
|
assert.Equal(t, 2, c.Entries())
|
|
assert.Equal(t, 0, finalized)
|
|
assert.Equal(t, true, c.expireRunning)
|
|
|
|
// The busy entry expires as soon as it is no longer busy
|
|
busy = false
|
|
c.cacheExpire()
|
|
assert.Equal(t, 1, c.Entries())
|
|
assert.Equal(t, 1, finalized)
|
|
_, found := c.GetMaybe("pinned")
|
|
assert.True(t, found)
|
|
}
|
|
|
|
func TestCacheCanExpireUsed(t *testing.T) {
|
|
c := New()
|
|
finalized := 0
|
|
c.SetFinalizer(func(v any) {
|
|
finalized++
|
|
})
|
|
create := func(path string) (any, bool, error) {
|
|
return path, true, nil
|
|
}
|
|
// Use the entry while it is being asked about
|
|
c.SetCanExpire(func(v any) bool {
|
|
_, _ = c.Get("/", create)
|
|
return true
|
|
})
|
|
c.expireDuration = 1 * time.Second
|
|
_, _ = c.Get("/", create)
|
|
c.mu.Lock()
|
|
c.cache["/"].lastUsed = time.Now().Add(-time.Minute)
|
|
c.mu.Unlock()
|
|
|
|
c.cacheExpire()
|
|
assert.Equal(t, 1, c.Entries())
|
|
assert.Equal(t, 0, finalized)
|
|
}
|