mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-14 11:58:02 +00:00
25dc7bfe72
# Which Problems Are Solved Cache implementation using a PGX connection pool. # How the Problems Are Solved Defines a new schema `cache` in the zitadel database. A table for string keys and a table for objects is defined. For postgreSQL, tables are unlogged and partitioned by cache name for performance. Cockroach does not have unlogged tables and partitioning is an enterprise feature that uses alternative syntax combined with sharding. Regular tables are used here. # Additional Changes - `postgres.Config` can return a pxg pool. See following discussion # Additional Context - Part of https://github.com/zitadel/zitadel/issues/8648 - Closes https://github.com/zitadel/zitadel/issues/8647 --------- Co-authored-by: Silvan <silvan.reusser@gmail.com>
201 lines
4.7 KiB
Go
201 lines
4.7 KiB
Go
package gomap
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"log/slog"
|
|
"maps"
|
|
"os"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/zitadel/zitadel/internal/cache"
|
|
)
|
|
|
|
type mapCache[I, K comparable, V cache.Entry[I, K]] struct {
|
|
config *cache.CacheConfig
|
|
indexMap map[I]*index[K, V]
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// NewCache returns an in-memory Cache implementation based on the builtin go map type.
|
|
// Object values are stored as-is and there is no encoding or decoding involved.
|
|
func NewCache[I, K comparable, V cache.Entry[I, K]](background context.Context, indices []I, config cache.CacheConfig) cache.PrunerCache[I, K, V] {
|
|
m := &mapCache[I, K, V]{
|
|
config: &config,
|
|
indexMap: make(map[I]*index[K, V], len(indices)),
|
|
logger: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
|
|
AddSource: true,
|
|
Level: slog.LevelError,
|
|
})),
|
|
}
|
|
if config.Log != nil {
|
|
m.logger = config.Log.Slog()
|
|
}
|
|
m.logger.InfoContext(background, "map cache logging enabled")
|
|
|
|
for _, name := range indices {
|
|
m.indexMap[name] = &index[K, V]{
|
|
config: m.config,
|
|
entries: make(map[K]*entry[V]),
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (c *mapCache[I, K, V]) Get(ctx context.Context, index I, key K) (value V, ok bool) {
|
|
i, ok := c.indexMap[index]
|
|
if !ok {
|
|
c.logger.ErrorContext(ctx, "map cache get", "err", cache.NewIndexUnknownErr(index), "index", index, "key", key)
|
|
return value, false
|
|
}
|
|
entry, err := i.Get(key)
|
|
if err == nil {
|
|
c.logger.DebugContext(ctx, "map cache get", "index", index, "key", key)
|
|
return entry.value, true
|
|
}
|
|
if errors.Is(err, cache.ErrCacheMiss) {
|
|
c.logger.InfoContext(ctx, "map cache get", "err", err, "index", index, "key", key)
|
|
return value, false
|
|
}
|
|
c.logger.ErrorContext(ctx, "map cache get", "err", cache.NewIndexUnknownErr(index), "index", index, "key", key)
|
|
return value, false
|
|
}
|
|
|
|
func (c *mapCache[I, K, V]) Set(ctx context.Context, value V) {
|
|
now := time.Now()
|
|
entry := &entry[V]{
|
|
value: value,
|
|
created: now,
|
|
}
|
|
entry.lastUse.Store(now.UnixMicro())
|
|
|
|
for name, i := range c.indexMap {
|
|
keys := value.Keys(name)
|
|
i.Set(keys, entry)
|
|
c.logger.DebugContext(ctx, "map cache set", "index", name, "keys", keys)
|
|
}
|
|
}
|
|
|
|
func (c *mapCache[I, K, V]) Invalidate(ctx context.Context, index I, keys ...K) error {
|
|
i, ok := c.indexMap[index]
|
|
if !ok {
|
|
return cache.NewIndexUnknownErr(index)
|
|
}
|
|
i.Invalidate(keys)
|
|
c.logger.DebugContext(ctx, "map cache invalidate", "index", index, "keys", keys)
|
|
return nil
|
|
}
|
|
|
|
func (c *mapCache[I, K, V]) Delete(ctx context.Context, index I, keys ...K) error {
|
|
i, ok := c.indexMap[index]
|
|
if !ok {
|
|
return cache.NewIndexUnknownErr(index)
|
|
}
|
|
i.Delete(keys)
|
|
c.logger.DebugContext(ctx, "map cache delete", "index", index, "keys", keys)
|
|
return nil
|
|
}
|
|
|
|
func (c *mapCache[I, K, V]) Prune(ctx context.Context) error {
|
|
for name, index := range c.indexMap {
|
|
index.Prune()
|
|
c.logger.DebugContext(ctx, "map cache prune", "index", name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *mapCache[I, K, V]) Truncate(ctx context.Context) error {
|
|
for name, index := range c.indexMap {
|
|
index.Truncate()
|
|
c.logger.DebugContext(ctx, "map cache truncate", "index", name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type index[K comparable, V any] struct {
|
|
mutex sync.RWMutex
|
|
config *cache.CacheConfig
|
|
entries map[K]*entry[V]
|
|
}
|
|
|
|
func (i *index[K, V]) Get(key K) (*entry[V], error) {
|
|
i.mutex.RLock()
|
|
entry, ok := i.entries[key]
|
|
i.mutex.RUnlock()
|
|
if ok && entry.isValid(i.config) {
|
|
return entry, nil
|
|
}
|
|
return nil, cache.ErrCacheMiss
|
|
}
|
|
|
|
func (c *index[K, V]) Set(keys []K, entry *entry[V]) {
|
|
c.mutex.Lock()
|
|
for _, key := range keys {
|
|
c.entries[key] = entry
|
|
}
|
|
c.mutex.Unlock()
|
|
}
|
|
|
|
func (i *index[K, V]) Invalidate(keys []K) {
|
|
i.mutex.RLock()
|
|
for _, key := range keys {
|
|
if entry, ok := i.entries[key]; ok {
|
|
entry.invalid.Store(true)
|
|
}
|
|
}
|
|
i.mutex.RUnlock()
|
|
}
|
|
|
|
func (c *index[K, V]) Delete(keys []K) {
|
|
c.mutex.Lock()
|
|
for _, key := range keys {
|
|
delete(c.entries, key)
|
|
}
|
|
c.mutex.Unlock()
|
|
}
|
|
|
|
func (c *index[K, V]) Prune() {
|
|
c.mutex.Lock()
|
|
maps.DeleteFunc(c.entries, func(_ K, entry *entry[V]) bool {
|
|
return !entry.isValid(c.config)
|
|
})
|
|
c.mutex.Unlock()
|
|
}
|
|
|
|
func (c *index[K, V]) Truncate() {
|
|
c.mutex.Lock()
|
|
c.entries = make(map[K]*entry[V])
|
|
c.mutex.Unlock()
|
|
}
|
|
|
|
type entry[V any] struct {
|
|
value V
|
|
created time.Time
|
|
invalid atomic.Bool
|
|
lastUse atomic.Int64 // UnixMicro time
|
|
}
|
|
|
|
func (e *entry[V]) isValid(c *cache.CacheConfig) bool {
|
|
if e.invalid.Load() {
|
|
return false
|
|
}
|
|
now := time.Now()
|
|
if c.MaxAge > 0 {
|
|
if e.created.Add(c.MaxAge).Before(now) {
|
|
e.invalid.Store(true)
|
|
return false
|
|
}
|
|
}
|
|
if c.LastUseAge > 0 {
|
|
lastUse := e.lastUse.Load()
|
|
if time.UnixMicro(lastUse).Add(c.LastUseAge).Before(now) {
|
|
e.invalid.Store(true)
|
|
return false
|
|
}
|
|
e.lastUse.CompareAndSwap(lastUse, now.UnixMicro())
|
|
}
|
|
return true
|
|
}
|