mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-07-16 14:52:35 -05:00
A GetWithValue deserialization error in RememberFor was returned as fatal. On a Redis upgrade the metrics counters live under the same keys as before but were stored as plain int64, so the first decode into the new envelope would fail and the metric would break permanently. Treat such errors as a miss and recompute/overwrite so the cache self-heals.
160 lines
4.0 KiB
Go
160 lines
4.0 KiB
Go
// Vikunja is a to-do list application to facilitate your life.
|
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package keyvalue
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"code.vikunja.io/api/pkg/modules/keyvalue/memory"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestRememberReturnsExisting(t *testing.T) {
|
|
store = memory.NewStorage()
|
|
err := Put("foo", "bar")
|
|
require.NoError(t, err)
|
|
|
|
called := false
|
|
val, err := Remember("foo", func() (interface{}, error) {
|
|
called = true
|
|
return "baz", nil
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "bar", val)
|
|
assert.False(t, called)
|
|
}
|
|
|
|
func TestRememberComputesAndStores(t *testing.T) {
|
|
store = memory.NewStorage()
|
|
|
|
called := 0
|
|
val, err := Remember("foo", func() (interface{}, error) {
|
|
called++
|
|
return "bar", nil
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "bar", val)
|
|
assert.Equal(t, 1, called)
|
|
|
|
v, exists, err := Get("foo")
|
|
require.NoError(t, err)
|
|
assert.True(t, exists)
|
|
assert.Equal(t, "bar", v)
|
|
|
|
val, err = Remember("foo", func() (interface{}, error) {
|
|
called++
|
|
return "baz", nil
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "bar", val)
|
|
assert.Equal(t, 1, called)
|
|
}
|
|
|
|
func TestRememberErrorDoesNotStore(t *testing.T) {
|
|
store = memory.NewStorage()
|
|
|
|
_, err := Remember("foo", func() (interface{}, error) {
|
|
return nil, errors.New("fail")
|
|
})
|
|
|
|
require.Error(t, err)
|
|
_, exists, err2 := Get("foo")
|
|
require.NoError(t, err2)
|
|
assert.False(t, exists)
|
|
}
|
|
|
|
func TestRememberForReturnsCachedWithinTTL(t *testing.T) {
|
|
store = memory.NewStorage()
|
|
|
|
called := 0
|
|
fn := func() (int64, error) {
|
|
called++
|
|
return int64(called), nil
|
|
}
|
|
|
|
val, err := RememberFor("foo", time.Hour, fn)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(1), val)
|
|
|
|
// Still within the TTL, so fn must not be called again.
|
|
val, err = RememberFor("foo", time.Hour, fn)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(1), val)
|
|
assert.Equal(t, 1, called)
|
|
}
|
|
|
|
func TestRememberForRecomputesAfterExpiry(t *testing.T) {
|
|
store = memory.NewStorage()
|
|
|
|
// Seed an already-expired value.
|
|
require.NoError(t, Put("foo", expiringValue[int64]{Value: 1, ExpiresAt: time.Now().Add(-time.Minute)}))
|
|
|
|
called := 0
|
|
val, err := RememberFor("foo", time.Hour, func() (int64, error) {
|
|
called++
|
|
return 2, nil
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(2), val)
|
|
assert.Equal(t, 1, called)
|
|
}
|
|
|
|
func TestRememberForErrorDoesNotStore(t *testing.T) {
|
|
store = memory.NewStorage()
|
|
|
|
_, err := RememberFor("foo", time.Hour, func() (int64, error) {
|
|
return 0, errors.New("fail")
|
|
})
|
|
|
|
require.Error(t, err)
|
|
_, exists, err2 := Get("foo")
|
|
require.NoError(t, err2)
|
|
assert.False(t, exists)
|
|
}
|
|
|
|
// getWithValueErrorStore simulates a backend that cannot deserialize an existing value
|
|
// into the requested type, e.g. a key that held a plain int64 before the cache started
|
|
// storing a struct (the pre-refactor metrics counters in Redis).
|
|
type getWithValueErrorStore struct {
|
|
*memory.Storage
|
|
}
|
|
|
|
func (s *getWithValueErrorStore) GetWithValue(string, interface{}) (bool, error) {
|
|
return false, errors.New("decode error")
|
|
}
|
|
|
|
func TestRememberForRecomputesWhenStoredValueCannotBeDeserialized(t *testing.T) {
|
|
store = &getWithValueErrorStore{memory.NewStorage()}
|
|
|
|
called := 0
|
|
val, err := RememberFor("foo", time.Hour, func() (int64, error) {
|
|
called++
|
|
return 42, nil
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(42), val)
|
|
assert.Equal(t, 1, called)
|
|
}
|