1
0

Key-Value Storages (#674)

Fix lint

Add docs for keyvalue config options

Use keyvalue store to cache unsplash photo results

Cleanup

Use keyvalue store for upload avatar

Use keyvalue store for initials avatar

Fix initializing metrics

Use keyvalue for metrics

Add IncryBy and DecrBy methods to increase or decrease a value

Fix lint

Return custom error if a key does not exist

Init keyvalue storage

Follow the keyvalue storage setting for things like cache and other

Add docs

Add configuration of the storage backend

Add redis keyvalue storage implementation

Add doc comments

Add methods to use storage through the package itself

Add memory implementation for keyvalue store

Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/api/pulls/674
Co-Authored-By: konrad <konrad@kola-entertainments.de>
Co-Committed-By: konrad <konrad@kola-entertainments.de>
This commit is contained in:
konrad
2020-10-10 16:53:59 +00:00
parent bf5d8af3f6
commit d56a611be7
16 changed files with 469 additions and 122 deletions

View File

@ -17,14 +17,12 @@
package initials
import (
"bytes"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/modules/keyvalue"
e "code.vikunja.io/api/pkg/modules/keyvalue/error"
"code.vikunja.io/api/pkg/user"
"github.com/disintegration/imaging"
"strconv"
"strings"
"sync"
"bytes"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font"
"golang.org/x/image/font/gofont/goregular"
@ -33,6 +31,8 @@ import (
"image/color"
"image/draw"
"image/png"
"strconv"
"strings"
)
// Provider represents the provider implementation of the initials provider
@ -51,19 +51,8 @@ var (
{121, 134, 203, 255},
{241, 185, 29, 255},
}
// Contain the created avatars with a size of defaultSize
cache = map[int64]*image.RGBA64{}
cacheLock = sync.Mutex{}
cacheResized = map[string][]byte{}
cacheResizedLock = sync.Mutex{}
)
func init() {
cache = make(map[int64]*image.RGBA64)
cacheResized = make(map[string][]byte)
}
const (
dpi = 72
defaultSize = 1024
@ -124,10 +113,26 @@ func drawImage(text rune, bg *color.RGBA) (img *image.RGBA64, err error) {
return img, err
}
func getCacheKey(prefix string, keys ...int64) string {
result := "avatar_initials_" + prefix
for i, key := range keys {
result += strconv.Itoa(int(key))
if i < len(keys) {
result += "_"
}
}
return result
}
func getAvatarForUser(u *user.User) (fullSizeAvatar *image.RGBA64, err error) {
var cached bool
fullSizeAvatar, cached = cache[u.ID]
if !cached {
cacheKey := getCacheKey("full", u.ID)
a, err := keyvalue.Get(cacheKey)
if err != nil && !e.IsErrValueNotFoundForKey(err) {
return nil, err
}
if err != nil && e.IsErrValueNotFoundForKey(err) {
log.Debugf("Initials avatar for user %d not cached, creating...", u.ID)
firstRune := []rune(strings.ToUpper(u.Username))[0]
bg := avatarBgColors[int(u.ID)%len(avatarBgColors)] // Random color based on the user id
@ -136,21 +141,27 @@ func getAvatarForUser(u *user.User) (fullSizeAvatar *image.RGBA64, err error) {
if err != nil {
return nil, err
}
cacheLock.Lock()
cache[u.ID] = fullSizeAvatar
cacheLock.Unlock()
err = keyvalue.Put(cacheKey, fullSizeAvatar)
if err != nil {
return nil, err
}
} else {
fullSizeAvatar = a.(*image.RGBA64)
}
return fullSizeAvatar, err
return fullSizeAvatar, nil
}
// GetAvatar returns an initials avatar for a user
func (p *Provider) GetAvatar(u *user.User, size int64) (avatar []byte, mimeType string, err error) {
cacheKey := getCacheKey("resized", u.ID, size)
var cached bool
cacheKey := strconv.Itoa(int(u.ID)) + "_" + strconv.Itoa(int(size))
avatar, cached = cacheResized[cacheKey]
if !cached {
a, err := keyvalue.Get(cacheKey)
if err != nil && !e.IsErrValueNotFoundForKey(err) {
return nil, "", err
}
if err != nil && e.IsErrValueNotFoundForKey(err) {
log.Debugf("Initials avatar for user %d and size %d not cached, creating...", u.ID, size)
fullAvatar, err := getAvatarForUser(u)
if err != nil {
@ -164,12 +175,14 @@ func (p *Provider) GetAvatar(u *user.User, size int64) (avatar []byte, mimeType
return nil, "", err
}
avatar = buf.Bytes()
cacheResizedLock.Lock()
cacheResized[cacheKey] = avatar
cacheResizedLock.Unlock()
err = keyvalue.Put(cacheKey, avatar)
if err != nil {
return nil, "", err
}
} else {
avatar = a.([]byte)
log.Debugf("Serving initials avatar for user %d and size %d from cache", u.ID, size)
}
return avatar, "image/png", err
return avatar, "image/png", nil
}

View File

@ -20,25 +20,16 @@ import (
"bytes"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/modules/keyvalue"
e "code.vikunja.io/api/pkg/modules/keyvalue/error"
"code.vikunja.io/api/pkg/user"
"github.com/disintegration/imaging"
"image"
"image/png"
"io/ioutil"
"sync"
"strconv"
)
var (
// This is a map with a map so we're able to clear all cached avatar (in all sizes) for one user at once
// The first map has as key the user id, the second one has the size as key
resizedCache = map[int64]map[int64][]byte{}
resizedCacheLock = sync.Mutex{}
)
func init() {
resizedCache = make(map[int64]map[int64][]byte)
}
// Provider represents the upload avatar provider
type Provider struct {
}
@ -46,19 +37,32 @@ type Provider struct {
// GetAvatar returns an uploaded user avatar
func (p *Provider) GetAvatar(u *user.User, size int64) (avatar []byte, mimeType string, err error) {
a, cached := resizedCache[u.ID]
if cached {
cacheKey := "avatar_upload_" + strconv.Itoa(int(u.ID))
ai, err := keyvalue.Get(cacheKey)
if err != nil && !e.IsErrValueNotFoundForKey(err) {
return nil, "", err
}
var cached map[int64][]byte
if ai != nil {
cached = ai.(map[int64][]byte)
}
if err != nil && e.IsErrValueNotFoundForKey(err) {
// Nothing ever cached for this user so we need to create the size map to avoid panics
cached = make(map[int64][]byte)
} else {
a := ai.(map[int64][]byte)
if a != nil && a[size] != nil {
log.Debugf("Serving uploaded avatar for user %d and size %d from cache.", u.ID, size)
return a[size], "", nil
}
// This means we have a map for the user, but nothing in it.
if a == nil {
resizedCache[u.ID] = make(map[int64][]byte)
cached = make(map[int64][]byte)
}
} else {
// Nothing ever cached for this user so we need to create the size map to avoid panics
resizedCache[u.ID] = make(map[int64][]byte)
}
log.Debugf("Uploaded avatar for user %d and size %d not cached, resizing and caching.", u.ID, size)
@ -84,15 +88,17 @@ func (p *Provider) GetAvatar(u *user.User, size int64) (avatar []byte, mimeType
}
avatar, err = ioutil.ReadAll(buf)
resizedCacheLock.Lock()
resizedCache[u.ID][size] = avatar
resizedCacheLock.Unlock()
if err != nil {
return nil, "", err
}
cached[size] = avatar
err = keyvalue.Put(cacheKey, cached)
return avatar, f.Mime, err
}
// InvalidateCache invalidates the avatar cache for a user
func InvalidateCache(u *user.User) {
resizedCacheLock.Lock()
delete(resizedCache, u.ID)
resizedCacheLock.Unlock()
if err := keyvalue.Del("avatar_upload_" + strconv.Itoa(int(u.ID))); err != nil {
log.Errorf("Could not invalidate upload avatar cache for user %d, error was %s", u.ID, err)
}
}