mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-11 20:37:30 +00:00
use current cache implementation
This commit is contained in:
56
backend/repository/cache/instance.go
vendored
56
backend/repository/cache/instance.go
vendored
@@ -1,56 +0,0 @@
|
|||||||
package cache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"log"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/backend/repository"
|
|
||||||
"github.com/zitadel/zitadel/backend/storage/cache"
|
|
||||||
"github.com/zitadel/zitadel/backend/storage/cache/gomap"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Instance struct {
|
|
||||||
mu *sync.RWMutex
|
|
||||||
byID cache.Cache[string, *repository.Instance]
|
|
||||||
byDomain cache.Cache[string, *repository.Instance]
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewInstance() *Instance {
|
|
||||||
return &Instance{
|
|
||||||
mu: &sync.RWMutex{},
|
|
||||||
byID: gomap.New[string, *repository.Instance](),
|
|
||||||
byDomain: gomap.New[string, *repository.Instance](),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Instance) Set(ctx context.Context, instance *repository.Instance) (*repository.Instance, error) {
|
|
||||||
log.Println("cache.instance.set")
|
|
||||||
i.set(instance, "")
|
|
||||||
return instance, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Instance) ByID(ctx context.Context, id string) (*repository.Instance, error) {
|
|
||||||
i.mu.RLock()
|
|
||||||
defer i.mu.RUnlock()
|
|
||||||
log.Println("cache.instance.byID")
|
|
||||||
instance, _ := i.byID.Get(id)
|
|
||||||
return instance, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Instance) ByDomain(ctx context.Context, domain string) (*repository.Instance, error) {
|
|
||||||
i.mu.RLock()
|
|
||||||
defer i.mu.RUnlock()
|
|
||||||
log.Println("cache.instance.byDomain")
|
|
||||||
instance, _ := i.byDomain.Get(domain)
|
|
||||||
return instance, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (i *Instance) set(instance *repository.Instance, domain string) {
|
|
||||||
i.mu.Lock()
|
|
||||||
defer i.mu.Unlock()
|
|
||||||
if domain != "" {
|
|
||||||
i.byDomain.Set(domain, instance)
|
|
||||||
}
|
|
||||||
i.byID.Set(instance.ID, instance)
|
|
||||||
}
|
|
38
backend/repository/cache/user.go
vendored
38
backend/repository/cache/user.go
vendored
@@ -1,38 +0,0 @@
|
|||||||
package cache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/backend/repository"
|
|
||||||
"github.com/zitadel/zitadel/backend/storage/cache"
|
|
||||||
"github.com/zitadel/zitadel/backend/storage/cache/gomap"
|
|
||||||
)
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
cache.Cache[string, *repository.User]
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUser() *User {
|
|
||||||
return &User{
|
|
||||||
Cache: gomap.New[string, *repository.User](),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ByID implements repository.UserRepository.
|
|
||||||
func (u *User) ByID(ctx context.Context, id string) (*repository.User, error) {
|
|
||||||
log.Println("cache.user.byid")
|
|
||||||
user, _ := u.Get(id)
|
|
||||||
return user, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) Set(ctx context.Context, user *repository.User) (*repository.User, error) {
|
|
||||||
log.Println("cache.user.set")
|
|
||||||
u.set(user)
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *User) set(user *repository.User) {
|
|
||||||
u.Cache.Set(user.ID, user)
|
|
||||||
}
|
|
@@ -1,10 +1,37 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
|
import "github.com/zitadel/zitadel/backend/storage/cache"
|
||||||
|
|
||||||
type Instance struct {
|
type Instance struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InstanceIndex uint8
|
||||||
|
|
||||||
|
var InstanceIndices = []InstanceIndex{
|
||||||
|
InstanceByID,
|
||||||
|
InstanceByDomain,
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
InstanceByID InstanceIndex = iota
|
||||||
|
InstanceByDomain
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ cache.Entry[InstanceIndex, string] = (*Instance)(nil)
|
||||||
|
|
||||||
|
// Keys implements [cache.Entry].
|
||||||
|
func (i *Instance) Keys(index InstanceIndex) (key []string) {
|
||||||
|
switch index {
|
||||||
|
case InstanceByID:
|
||||||
|
return []string{i.ID}
|
||||||
|
case InstanceByDomain:
|
||||||
|
return []string{i.Name}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type ListRequest struct {
|
type ListRequest struct {
|
||||||
Limit uint16
|
Limit uint16
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,8 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/backend/storage/cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handler is a function that handles the request.
|
// Handler is a function that handles the request.
|
||||||
@@ -65,7 +67,7 @@ func SkipNext[Req, Res any](handle Handler[Req, Res], next Handler[Req, Res]) Ha
|
|||||||
|
|
||||||
// SkipNilHandler skips the handle function if the handler is nil.
|
// SkipNilHandler skips the handle function if the handler is nil.
|
||||||
// The function is safe to call with nil handler.
|
// The function is safe to call with nil handler.
|
||||||
func SkipNilHandler[O, R any](handler *O, handle Handler[R, R]) Handler[R, R] {
|
func SkipNilHandler[R any](handler any, handle Handler[R, R]) Handler[R, R] {
|
||||||
return func(ctx context.Context, request R) (res R, err error) {
|
return func(ctx context.Context, request R) (res R, err error) {
|
||||||
if handler == nil {
|
if handler == nil {
|
||||||
return request, nil
|
return request, nil
|
||||||
@@ -73,3 +75,27 @@ func SkipNilHandler[O, R any](handler *O, handle Handler[R, R]) Handler[R, R] {
|
|||||||
return handle(ctx, request)
|
return handle(ctx, request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ErrFuncToHandle[R any](fn func(context.Context, R) error) Handler[R, R] {
|
||||||
|
return func(ctx context.Context, request R) (res R, err error) {
|
||||||
|
err = fn(ctx, request)
|
||||||
|
if err != nil {
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
return request, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NoReturnToHandle[R any](fn func(context.Context, R)) Handler[R, R] {
|
||||||
|
return func(ctx context.Context, request R) (res R, err error) {
|
||||||
|
fn(ctx, request)
|
||||||
|
return request, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CacheGetToHandle[I, K comparable, E cache.Entry[I, K]](fn func(context.Context, I, K) (E, bool), index I) Handler[K, E] {
|
||||||
|
return func(ctx context.Context, request K) (res E, err error) {
|
||||||
|
res, _ = fn(ctx, index, request)
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -4,18 +4,19 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/backend/repository"
|
"github.com/zitadel/zitadel/backend/repository"
|
||||||
"github.com/zitadel/zitadel/backend/repository/cache"
|
|
||||||
"github.com/zitadel/zitadel/backend/repository/event"
|
"github.com/zitadel/zitadel/backend/repository/event"
|
||||||
"github.com/zitadel/zitadel/backend/repository/orchestrate/handler"
|
"github.com/zitadel/zitadel/backend/repository/orchestrate/handler"
|
||||||
"github.com/zitadel/zitadel/backend/repository/sql"
|
"github.com/zitadel/zitadel/backend/repository/sql"
|
||||||
"github.com/zitadel/zitadel/backend/repository/telemetry/logged"
|
"github.com/zitadel/zitadel/backend/repository/telemetry/logged"
|
||||||
"github.com/zitadel/zitadel/backend/repository/telemetry/traced"
|
"github.com/zitadel/zitadel/backend/repository/telemetry/traced"
|
||||||
|
"github.com/zitadel/zitadel/backend/storage/cache"
|
||||||
|
"github.com/zitadel/zitadel/backend/storage/cache/connector/noop"
|
||||||
"github.com/zitadel/zitadel/backend/storage/database"
|
"github.com/zitadel/zitadel/backend/storage/database"
|
||||||
"github.com/zitadel/zitadel/backend/telemetry/tracing"
|
"github.com/zitadel/zitadel/backend/telemetry/tracing"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InstanceOptions struct {
|
type InstanceOptions struct {
|
||||||
cache *cache.Instance
|
cache cache.Cache[repository.InstanceIndex, string, *repository.Instance]
|
||||||
}
|
}
|
||||||
|
|
||||||
type instance struct {
|
type instance struct {
|
||||||
@@ -24,17 +25,17 @@ type instance struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Instance(opts ...Option[InstanceOptions]) *instance {
|
func Instance(opts ...Option[InstanceOptions]) *instance {
|
||||||
i := instance{
|
i := new(instance)
|
||||||
options: newOptions[InstanceOptions](),
|
i.InstanceOptions = &i.options.custom
|
||||||
}
|
i.cache = noop.NewCache[repository.InstanceIndex, string, *repository.Instance]()
|
||||||
i.InstanceOptions = i.options.custom
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt.apply(&i.options)
|
opt.apply(&i.options)
|
||||||
}
|
}
|
||||||
return &i
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithInstanceCache(cache *cache.Instance) Option[InstanceOptions] {
|
func WithInstanceCache(cache cache.Cache[repository.InstanceIndex, string, *repository.Instance]) Option[InstanceOptions] {
|
||||||
return func(opts *options[InstanceOptions]) {
|
return func(opts *options[InstanceOptions]) {
|
||||||
opts.custom.cache = cache
|
opts.custom.cache = cache
|
||||||
}
|
}
|
||||||
@@ -55,7 +56,7 @@ func (i *instance) Create(ctx context.Context, tx database.Transaction, instance
|
|||||||
),
|
),
|
||||||
handler.SkipNilHandler(i.cache,
|
handler.SkipNilHandler(i.cache,
|
||||||
handler.Decorates(
|
handler.Decorates(
|
||||||
i.cache.Set,
|
handler.NoReturnToHandle(i.cache.Set),
|
||||||
traced.Decorate[*repository.Instance, *repository.Instance](i.tracer, tracing.WithSpanName("instance.cache.SetUp")),
|
traced.Decorate[*repository.Instance, *repository.Instance](i.tracer, tracing.WithSpanName("instance.cache.SetUp")),
|
||||||
logged.Decorate[*repository.Instance, *repository.Instance](i.logger, "instance.cache.SetUp"),
|
logged.Decorate[*repository.Instance, *repository.Instance](i.logger, "instance.cache.SetUp"),
|
||||||
),
|
),
|
||||||
@@ -66,26 +67,26 @@ func (i *instance) Create(ctx context.Context, tx database.Transaction, instance
|
|||||||
|
|
||||||
func (i *instance) ByID(ctx context.Context, querier database.Querier, id string) (*repository.Instance, error) {
|
func (i *instance) ByID(ctx context.Context, querier database.Querier, id string) (*repository.Instance, error) {
|
||||||
return handler.SkipNext(
|
return handler.SkipNext(
|
||||||
i.cache.ByID,
|
handler.CacheGetToHandle(i.cache.Get, repository.InstanceByID),
|
||||||
handler.Chain(
|
handler.Chain(
|
||||||
handler.Decorate(
|
handler.Decorate(
|
||||||
sql.Query(querier).InstanceByID,
|
sql.Query(querier).InstanceByID,
|
||||||
traced.Decorate[string, *repository.Instance](i.tracer, tracing.WithSpanName("instance.sql.ByID")),
|
traced.Decorate[string, *repository.Instance](i.tracer, tracing.WithSpanName("instance.sql.ByID")),
|
||||||
),
|
),
|
||||||
handler.SkipNilHandler(i.cache, i.cache.Set),
|
handler.SkipNilHandler(i.cache, handler.NoReturnToHandle(i.cache.Set)),
|
||||||
),
|
),
|
||||||
)(ctx, id)
|
)(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *instance) ByDomain(ctx context.Context, querier database.Querier, domain string) (*repository.Instance, error) {
|
func (i *instance) ByDomain(ctx context.Context, querier database.Querier, domain string) (*repository.Instance, error) {
|
||||||
return handler.SkipNext(
|
return handler.SkipNext(
|
||||||
i.cache.ByDomain,
|
handler.CacheGetToHandle(i.cache.Get, repository.InstanceByDomain),
|
||||||
handler.Chain(
|
handler.Chain(
|
||||||
handler.Decorate(
|
handler.Decorate(
|
||||||
sql.Query(querier).InstanceByDomain,
|
sql.Query(querier).InstanceByDomain,
|
||||||
traced.Decorate[string, *repository.Instance](i.tracer, tracing.WithSpanName("instance.sql.ByDomain")),
|
traced.Decorate[string, *repository.Instance](i.tracer, tracing.WithSpanName("instance.sql.ByDomain")),
|
||||||
),
|
),
|
||||||
handler.SkipNilHandler(i.cache, i.cache.Set),
|
handler.SkipNilHandler(i.cache, handler.NoReturnToHandle(i.cache.Set)),
|
||||||
),
|
),
|
||||||
)(ctx, domain)
|
)(ctx, domain)
|
||||||
}
|
}
|
||||||
|
@@ -8,8 +8,9 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/backend/repository"
|
"github.com/zitadel/zitadel/backend/repository"
|
||||||
"github.com/zitadel/zitadel/backend/repository/cache"
|
|
||||||
"github.com/zitadel/zitadel/backend/repository/orchestrate"
|
"github.com/zitadel/zitadel/backend/repository/orchestrate"
|
||||||
|
"github.com/zitadel/zitadel/backend/storage/cache"
|
||||||
|
"github.com/zitadel/zitadel/backend/storage/cache/connector/gomap"
|
||||||
"github.com/zitadel/zitadel/backend/storage/database"
|
"github.com/zitadel/zitadel/backend/storage/database"
|
||||||
"github.com/zitadel/zitadel/backend/storage/database/mock"
|
"github.com/zitadel/zitadel/backend/storage/database/mock"
|
||||||
"github.com/zitadel/zitadel/backend/telemetry/logging"
|
"github.com/zitadel/zitadel/backend/telemetry/logging"
|
||||||
@@ -34,7 +35,9 @@ func Test_instance_SetUp(t *testing.T) {
|
|||||||
opts: []orchestrate.Option[orchestrate.InstanceOptions]{
|
opts: []orchestrate.Option[orchestrate.InstanceOptions]{
|
||||||
orchestrate.WithTracer[orchestrate.InstanceOptions](tracing.NewTracer("test")),
|
orchestrate.WithTracer[orchestrate.InstanceOptions](tracing.NewTracer("test")),
|
||||||
orchestrate.WithLogger[orchestrate.InstanceOptions](logging.New(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})))),
|
orchestrate.WithLogger[orchestrate.InstanceOptions](logging.New(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})))),
|
||||||
orchestrate.WithInstanceCache(cache.NewInstance()),
|
orchestrate.WithInstanceCache(
|
||||||
|
gomap.NewCache[repository.InstanceIndex, string, *repository.Instance](context.Background(), repository.InstanceIndices, cache.Config{}),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
args: args{
|
args: args{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
|
@@ -7,21 +7,15 @@ import (
|
|||||||
|
|
||||||
// options are the default options for orchestrators.
|
// options are the default options for orchestrators.
|
||||||
type options[T any] struct {
|
type options[T any] struct {
|
||||||
custom *T
|
custom T
|
||||||
|
defaultOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
type defaultOptions struct {
|
||||||
tracer *tracing.Tracer
|
tracer *tracing.Tracer
|
||||||
logger *logging.Logger
|
logger *logging.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func newOptions[T any]() options[T] {
|
|
||||||
return options[T]{
|
|
||||||
custom: new(T),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type applier interface {
|
|
||||||
apply()
|
|
||||||
}
|
|
||||||
|
|
||||||
type Option[T any] func(*options[T])
|
type Option[T any] func(*options[T])
|
||||||
|
|
||||||
func WithTracer[T any](tracer *tracing.Tracer) Option[T] {
|
func WithTracer[T any](tracer *tracing.Tracer) Option[T] {
|
||||||
|
@@ -4,17 +4,18 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/backend/repository"
|
"github.com/zitadel/zitadel/backend/repository"
|
||||||
"github.com/zitadel/zitadel/backend/repository/cache"
|
|
||||||
"github.com/zitadel/zitadel/backend/repository/event"
|
"github.com/zitadel/zitadel/backend/repository/event"
|
||||||
"github.com/zitadel/zitadel/backend/repository/orchestrate/handler"
|
"github.com/zitadel/zitadel/backend/repository/orchestrate/handler"
|
||||||
"github.com/zitadel/zitadel/backend/repository/sql"
|
"github.com/zitadel/zitadel/backend/repository/sql"
|
||||||
"github.com/zitadel/zitadel/backend/repository/telemetry/traced"
|
"github.com/zitadel/zitadel/backend/repository/telemetry/traced"
|
||||||
|
"github.com/zitadel/zitadel/backend/storage/cache"
|
||||||
|
"github.com/zitadel/zitadel/backend/storage/cache/connector/noop"
|
||||||
"github.com/zitadel/zitadel/backend/storage/database"
|
"github.com/zitadel/zitadel/backend/storage/database"
|
||||||
"github.com/zitadel/zitadel/backend/telemetry/tracing"
|
"github.com/zitadel/zitadel/backend/telemetry/tracing"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserOptions struct {
|
type UserOptions struct {
|
||||||
cache *cache.User
|
cache cache.Cache[repository.UserIndex, string, *repository.User]
|
||||||
}
|
}
|
||||||
|
|
||||||
type user struct {
|
type user struct {
|
||||||
@@ -23,17 +24,17 @@ type user struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func User(opts ...Option[UserOptions]) *user {
|
func User(opts ...Option[UserOptions]) *user {
|
||||||
i := user{
|
i := new(user)
|
||||||
options: newOptions[UserOptions](),
|
i.UserOptions = &i.options.custom
|
||||||
}
|
i.cache = noop.NewCache[repository.UserIndex, string, *repository.User]()
|
||||||
i.UserOptions = i.options.custom
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(&i.options)
|
opt(&i.options)
|
||||||
}
|
}
|
||||||
return &i
|
return i
|
||||||
}
|
}
|
||||||
|
|
||||||
func WithUserCache(cache *cache.User) Option[UserOptions] {
|
func WithUserCache(cache cache.Cache[repository.UserIndex, string, *repository.User]) Option[UserOptions] {
|
||||||
return func(i *options[UserOptions]) {
|
return func(i *options[UserOptions]) {
|
||||||
i.custom.cache = cache
|
i.custom.cache = cache
|
||||||
}
|
}
|
||||||
@@ -56,13 +57,13 @@ func (i *user) Create(ctx context.Context, tx database.Transaction, user *reposi
|
|||||||
|
|
||||||
func (i *user) ByID(ctx context.Context, querier database.Querier, id string) (*repository.User, error) {
|
func (i *user) ByID(ctx context.Context, querier database.Querier, id string) (*repository.User, error) {
|
||||||
return handler.SkipNext(
|
return handler.SkipNext(
|
||||||
i.custom.cache.ByID,
|
handler.CacheGetToHandle(i.cache.Get, repository.UserByID),
|
||||||
handler.Chain(
|
handler.Chain(
|
||||||
handler.Decorate(
|
handler.Decorate(
|
||||||
sql.Query(querier).UserByID,
|
sql.Query(querier).UserByID,
|
||||||
traced.Decorate[string, *repository.User](i.tracer, tracing.WithSpanName("user.sql.ByID")),
|
traced.Decorate[string, *repository.User](i.tracer, tracing.WithSpanName("user.sql.ByID")),
|
||||||
),
|
),
|
||||||
handler.SkipNilHandler(i.custom.cache, i.custom.cache.Set),
|
handler.SkipNilHandler(i.custom.cache, handler.NoReturnToHandle(i.cache.Set)),
|
||||||
),
|
),
|
||||||
)(ctx, id)
|
)(ctx, id)
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,33 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
|
import "github.com/zitadel/zitadel/backend/storage/cache"
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID string
|
ID string
|
||||||
Username string
|
Username string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserIndex uint8
|
||||||
|
|
||||||
|
var UserIndices = []UserIndex{
|
||||||
|
UserByID,
|
||||||
|
UserByUsername,
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
UserByID UserIndex = iota
|
||||||
|
UserByUsername
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ cache.Entry[UserIndex, string] = (*User)(nil)
|
||||||
|
|
||||||
|
// Keys implements [cache.Entry].
|
||||||
|
func (u *User) Keys(index UserIndex) (key []string) {
|
||||||
|
switch index {
|
||||||
|
case UserByID:
|
||||||
|
return []string{u.ID}
|
||||||
|
case UserByUsername:
|
||||||
|
return []string{u.Username}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
114
backend/storage/cache/cache.go
vendored
114
backend/storage/cache/cache.go
vendored
@@ -1,8 +1,112 @@
|
|||||||
|
// Package cache provides abstraction of cache implementations that can be used by zitadel.
|
||||||
package cache
|
package cache
|
||||||
|
|
||||||
type Cache[K comparable, V any] interface {
|
import (
|
||||||
Get(key K) (V, bool)
|
"context"
|
||||||
Set(key K, value V)
|
"time"
|
||||||
Delete(key K)
|
|
||||||
Clear()
|
"github.com/zitadel/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Purpose describes which object types are stored by a cache.
|
||||||
|
type Purpose int
|
||||||
|
|
||||||
|
//go:generate enumer -type Purpose -transform snake -trimprefix Purpose
|
||||||
|
const (
|
||||||
|
PurposeUnspecified Purpose = iota
|
||||||
|
PurposeAuthzInstance
|
||||||
|
PurposeMilestones
|
||||||
|
PurposeOrganization
|
||||||
|
PurposeIdPFormCallback
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cache stores objects with a value of type `V`.
|
||||||
|
// Objects may be referred to by one or more indices.
|
||||||
|
// Implementations may encode the value for storage.
|
||||||
|
// This means non-exported fields may be lost and objects
|
||||||
|
// with function values may fail to encode.
|
||||||
|
// See https://pkg.go.dev/encoding/json#Marshal for example.
|
||||||
|
//
|
||||||
|
// `I` is the type by which indices are identified,
|
||||||
|
// typically an enum for type-safe access.
|
||||||
|
// Indices are defined when calling the constructor of an implementation of this interface.
|
||||||
|
// It is illegal to refer to an idex not defined during construction.
|
||||||
|
//
|
||||||
|
// `K` is the type used as key in each index.
|
||||||
|
// Due to the limitations in type constraints, all indices use the same key type.
|
||||||
|
//
|
||||||
|
// Implementations are free to use stricter type constraints or fixed typing.
|
||||||
|
type Cache[I, K comparable, V Entry[I, K]] interface {
|
||||||
|
// Get an object through specified index.
|
||||||
|
// An [IndexUnknownError] may be returned if the index is unknown.
|
||||||
|
// [ErrCacheMiss] is returned if the key was not found in the index,
|
||||||
|
// or the object is not valid.
|
||||||
|
Get(ctx context.Context, index I, key K) (V, bool)
|
||||||
|
|
||||||
|
// Set an object.
|
||||||
|
// Keys are created on each index based in the [Entry.Keys] method.
|
||||||
|
// If any key maps to an existing object, the object is invalidated,
|
||||||
|
// regardless if the object has other keys defined in the new entry.
|
||||||
|
// This to prevent ghost objects when an entry reduces the amount of keys
|
||||||
|
// for a given index.
|
||||||
|
Set(ctx context.Context, value V)
|
||||||
|
|
||||||
|
// Invalidate an object through specified index.
|
||||||
|
// Implementations may choose to instantly delete the object,
|
||||||
|
// defer until prune or a separate cleanup routine.
|
||||||
|
// Invalidated object are no longer returned from Get.
|
||||||
|
// It is safe to call Invalidate multiple times or on non-existing entries.
|
||||||
|
Invalidate(ctx context.Context, index I, key ...K) error
|
||||||
|
|
||||||
|
// Delete one or more keys from a specific index.
|
||||||
|
// An [IndexUnknownError] may be returned if the index is unknown.
|
||||||
|
// The referred object is not invalidated and may still be accessible though
|
||||||
|
// other indices and keys.
|
||||||
|
// It is safe to call Delete multiple times or on non-existing entries
|
||||||
|
Delete(ctx context.Context, index I, key ...K) error
|
||||||
|
|
||||||
|
// Truncate deletes all cached objects.
|
||||||
|
Truncate(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entry contains a value of type `V` to be cached.
|
||||||
|
//
|
||||||
|
// `I` is the type by which indices are identified,
|
||||||
|
// typically an enum for type-safe access.
|
||||||
|
//
|
||||||
|
// `K` is the type used as key in an index.
|
||||||
|
// Due to the limitations in type constraints, all indices use the same key type.
|
||||||
|
type Entry[I, K comparable] interface {
|
||||||
|
// Keys returns which keys map to the object in a specified index.
|
||||||
|
// May return nil if the index in unknown or when there are no keys.
|
||||||
|
Keys(index I) (key []K)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Connector int
|
||||||
|
|
||||||
|
//go:generate enumer -type Connector -transform snake -trimprefix Connector -linecomment -text
|
||||||
|
const (
|
||||||
|
// Empty line comment ensures empty string for unspecified value
|
||||||
|
ConnectorUnspecified Connector = iota //
|
||||||
|
ConnectorMemory
|
||||||
|
ConnectorPostgres
|
||||||
|
ConnectorRedis
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Connector Connector
|
||||||
|
|
||||||
|
// Age since an object was added to the cache,
|
||||||
|
// after which the object is considered invalid.
|
||||||
|
// 0 disables max age checks.
|
||||||
|
MaxAge time.Duration
|
||||||
|
|
||||||
|
// Age since last use (Get) of an object,
|
||||||
|
// after which the object is considered invalid.
|
||||||
|
// 0 disables last use age checks.
|
||||||
|
LastUseAge time.Duration
|
||||||
|
|
||||||
|
// Log allows logging of the specific cache.
|
||||||
|
// By default only errors are logged to stdout.
|
||||||
|
Log *logging.Config
|
||||||
}
|
}
|
||||||
|
49
backend/storage/cache/connector/connector.go
vendored
Normal file
49
backend/storage/cache/connector/connector.go
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// Package connector provides glue between the [cache.Cache] interface and implementations from the connector sub-packages.
|
||||||
|
package connector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/backend/storage/cache"
|
||||||
|
"github.com/zitadel/zitadel/backend/storage/cache/connector/gomap"
|
||||||
|
"github.com/zitadel/zitadel/backend/storage/cache/connector/noop"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CachesConfig struct {
|
||||||
|
Connectors struct {
|
||||||
|
Memory gomap.Config
|
||||||
|
}
|
||||||
|
Instance *cache.Config
|
||||||
|
Milestones *cache.Config
|
||||||
|
Organization *cache.Config
|
||||||
|
IdPFormCallbacks *cache.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
type Connectors struct {
|
||||||
|
Config CachesConfig
|
||||||
|
Memory *gomap.Connector
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartConnectors(conf *CachesConfig) (Connectors, error) {
|
||||||
|
if conf == nil {
|
||||||
|
return Connectors{}, nil
|
||||||
|
}
|
||||||
|
return Connectors{
|
||||||
|
Config: *conf,
|
||||||
|
Memory: gomap.NewConnector(conf.Connectors.Memory),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartCache[I ~int, K ~string, V cache.Entry[I, K]](background context.Context, indices []I, purpose cache.Purpose, conf *cache.Config, connectors Connectors) (cache.Cache[I, K, V], error) {
|
||||||
|
if conf == nil || conf.Connector == cache.ConnectorUnspecified {
|
||||||
|
return noop.NewCache[I, K, V](), nil
|
||||||
|
}
|
||||||
|
if conf.Connector == cache.ConnectorMemory && connectors.Memory != nil {
|
||||||
|
c := gomap.NewCache[I, K, V](background, indices, *conf)
|
||||||
|
connectors.Memory.Config.StartAutoPrune(background, c, purpose)
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("cache connector %q not enabled", conf.Connector)
|
||||||
|
}
|
23
backend/storage/cache/connector/gomap/connector.go
vendored
Normal file
23
backend/storage/cache/connector/gomap/connector.go
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package gomap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/zitadel/zitadel/backend/storage/cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Enabled bool
|
||||||
|
AutoPrune cache.AutoPruneConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type Connector struct {
|
||||||
|
Config cache.AutoPruneConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConnector(config Config) *Connector {
|
||||||
|
if !config.Enabled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &Connector{
|
||||||
|
Config: config.AutoPrune,
|
||||||
|
}
|
||||||
|
}
|
200
backend/storage/cache/connector/gomap/gomap.go
vendored
Normal file
200
backend/storage/cache/connector/gomap/gomap.go
vendored
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
package gomap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
|
"maps"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/backend/storage/cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mapCache[I, K comparable, V cache.Entry[I, K]] struct {
|
||||||
|
config *cache.Config
|
||||||
|
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.Config) 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.Config
|
||||||
|
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.Config) 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
|
||||||
|
}
|
329
backend/storage/cache/connector/gomap/gomap_test.go
vendored
Normal file
329
backend/storage/cache/connector/gomap/gomap_test.go
vendored
Normal file
@@ -0,0 +1,329 @@
|
|||||||
|
package gomap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/zitadel/logging"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/backend/storage/cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testIndex int
|
||||||
|
|
||||||
|
const (
|
||||||
|
testIndexID testIndex = iota
|
||||||
|
testIndexName
|
||||||
|
)
|
||||||
|
|
||||||
|
var testIndices = []testIndex{
|
||||||
|
testIndexID,
|
||||||
|
testIndexName,
|
||||||
|
}
|
||||||
|
|
||||||
|
type testObject struct {
|
||||||
|
id string
|
||||||
|
names []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *testObject) Keys(index testIndex) []string {
|
||||||
|
switch index {
|
||||||
|
case testIndexID:
|
||||||
|
return []string{o.id}
|
||||||
|
case testIndexName:
|
||||||
|
return o.names
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_mapCache_Get(t *testing.T) {
|
||||||
|
c := NewCache[testIndex, string, *testObject](context.Background(), testIndices, cache.Config{
|
||||||
|
MaxAge: time.Second,
|
||||||
|
LastUseAge: time.Second / 4,
|
||||||
|
Log: &logging.Config{
|
||||||
|
Level: "debug",
|
||||||
|
AddSource: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
obj := &testObject{
|
||||||
|
id: "id",
|
||||||
|
names: []string{"foo", "bar"},
|
||||||
|
}
|
||||||
|
c.Set(context.Background(), obj)
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
index testIndex
|
||||||
|
key string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want *testObject
|
||||||
|
wantOk bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ok",
|
||||||
|
args: args{
|
||||||
|
index: testIndexID,
|
||||||
|
key: "id",
|
||||||
|
},
|
||||||
|
want: obj,
|
||||||
|
wantOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "miss",
|
||||||
|
args: args{
|
||||||
|
index: testIndexID,
|
||||||
|
key: "spanac",
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantOk: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown index",
|
||||||
|
args: args{
|
||||||
|
index: 99,
|
||||||
|
key: "id",
|
||||||
|
},
|
||||||
|
want: nil,
|
||||||
|
wantOk: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, ok := c.Get(context.Background(), tt.args.index, tt.args.key)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
assert.Equal(t, tt.wantOk, ok)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_mapCache_Invalidate(t *testing.T) {
|
||||||
|
c := NewCache[testIndex, string, *testObject](context.Background(), testIndices, cache.Config{
|
||||||
|
MaxAge: time.Second,
|
||||||
|
LastUseAge: time.Second / 4,
|
||||||
|
Log: &logging.Config{
|
||||||
|
Level: "debug",
|
||||||
|
AddSource: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
obj := &testObject{
|
||||||
|
id: "id",
|
||||||
|
names: []string{"foo", "bar"},
|
||||||
|
}
|
||||||
|
c.Set(context.Background(), obj)
|
||||||
|
err := c.Invalidate(context.Background(), testIndexName, "bar")
|
||||||
|
require.NoError(t, err)
|
||||||
|
got, ok := c.Get(context.Background(), testIndexID, "id")
|
||||||
|
assert.Nil(t, got)
|
||||||
|
assert.False(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_mapCache_Delete(t *testing.T) {
|
||||||
|
c := NewCache[testIndex, string, *testObject](context.Background(), testIndices, cache.Config{
|
||||||
|
MaxAge: time.Second,
|
||||||
|
LastUseAge: time.Second / 4,
|
||||||
|
Log: &logging.Config{
|
||||||
|
Level: "debug",
|
||||||
|
AddSource: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
obj := &testObject{
|
||||||
|
id: "id",
|
||||||
|
names: []string{"foo", "bar"},
|
||||||
|
}
|
||||||
|
c.Set(context.Background(), obj)
|
||||||
|
err := c.Delete(context.Background(), testIndexName, "bar")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Shouldn't find object by deleted name
|
||||||
|
got, ok := c.Get(context.Background(), testIndexName, "bar")
|
||||||
|
assert.Nil(t, got)
|
||||||
|
assert.False(t, ok)
|
||||||
|
|
||||||
|
// Should find object by other name
|
||||||
|
got, ok = c.Get(context.Background(), testIndexName, "foo")
|
||||||
|
assert.Equal(t, obj, got)
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
// Should find object by id
|
||||||
|
got, ok = c.Get(context.Background(), testIndexID, "id")
|
||||||
|
assert.Equal(t, obj, got)
|
||||||
|
assert.True(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_mapCache_Prune(t *testing.T) {
|
||||||
|
c := NewCache[testIndex, string, *testObject](context.Background(), testIndices, cache.Config{
|
||||||
|
MaxAge: time.Second,
|
||||||
|
LastUseAge: time.Second / 4,
|
||||||
|
Log: &logging.Config{
|
||||||
|
Level: "debug",
|
||||||
|
AddSource: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
objects := []*testObject{
|
||||||
|
{
|
||||||
|
id: "id1",
|
||||||
|
names: []string{"foo", "bar"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "id2",
|
||||||
|
names: []string{"hello"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, obj := range objects {
|
||||||
|
c.Set(context.Background(), obj)
|
||||||
|
}
|
||||||
|
// invalidate one entry
|
||||||
|
err := c.Invalidate(context.Background(), testIndexName, "bar")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = c.(cache.Pruner).Prune(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Other object should still be found
|
||||||
|
got, ok := c.Get(context.Background(), testIndexID, "id2")
|
||||||
|
assert.Equal(t, objects[1], got)
|
||||||
|
assert.True(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_mapCache_Truncate(t *testing.T) {
|
||||||
|
c := NewCache[testIndex, string, *testObject](context.Background(), testIndices, cache.Config{
|
||||||
|
MaxAge: time.Second,
|
||||||
|
LastUseAge: time.Second / 4,
|
||||||
|
Log: &logging.Config{
|
||||||
|
Level: "debug",
|
||||||
|
AddSource: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
objects := []*testObject{
|
||||||
|
{
|
||||||
|
id: "id1",
|
||||||
|
names: []string{"foo", "bar"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "id2",
|
||||||
|
names: []string{"hello"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, obj := range objects {
|
||||||
|
c.Set(context.Background(), obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := c.Truncate(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
mc := c.(*mapCache[testIndex, string, *testObject])
|
||||||
|
for _, index := range mc.indexMap {
|
||||||
|
index.mutex.RLock()
|
||||||
|
assert.Len(t, index.entries, 0)
|
||||||
|
index.mutex.RUnlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_entry_isValid(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
created time.Time
|
||||||
|
invalid bool
|
||||||
|
lastUse time.Time
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
config *cache.Config
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "invalid",
|
||||||
|
fields: fields{
|
||||||
|
created: time.Now(),
|
||||||
|
invalid: true,
|
||||||
|
lastUse: time.Now(),
|
||||||
|
},
|
||||||
|
config: &cache.Config{
|
||||||
|
MaxAge: time.Minute,
|
||||||
|
LastUseAge: time.Second,
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "max age exceeded",
|
||||||
|
fields: fields{
|
||||||
|
created: time.Now().Add(-(time.Minute + time.Second)),
|
||||||
|
invalid: false,
|
||||||
|
lastUse: time.Now(),
|
||||||
|
},
|
||||||
|
config: &cache.Config{
|
||||||
|
MaxAge: time.Minute,
|
||||||
|
LastUseAge: time.Second,
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "max age disabled",
|
||||||
|
fields: fields{
|
||||||
|
created: time.Now().Add(-(time.Minute + time.Second)),
|
||||||
|
invalid: false,
|
||||||
|
lastUse: time.Now(),
|
||||||
|
},
|
||||||
|
config: &cache.Config{
|
||||||
|
LastUseAge: time.Second,
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "last use age exceeded",
|
||||||
|
fields: fields{
|
||||||
|
created: time.Now().Add(-(time.Minute / 2)),
|
||||||
|
invalid: false,
|
||||||
|
lastUse: time.Now().Add(-(time.Second * 2)),
|
||||||
|
},
|
||||||
|
config: &cache.Config{
|
||||||
|
MaxAge: time.Minute,
|
||||||
|
LastUseAge: time.Second,
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "last use age disabled",
|
||||||
|
fields: fields{
|
||||||
|
created: time.Now().Add(-(time.Minute / 2)),
|
||||||
|
invalid: false,
|
||||||
|
lastUse: time.Now().Add(-(time.Second * 2)),
|
||||||
|
},
|
||||||
|
config: &cache.Config{
|
||||||
|
MaxAge: time.Minute,
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid",
|
||||||
|
fields: fields{
|
||||||
|
created: time.Now(),
|
||||||
|
invalid: false,
|
||||||
|
lastUse: time.Now(),
|
||||||
|
},
|
||||||
|
config: &cache.Config{
|
||||||
|
MaxAge: time.Minute,
|
||||||
|
LastUseAge: time.Second,
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
e := &entry[any]{
|
||||||
|
created: tt.fields.created,
|
||||||
|
}
|
||||||
|
e.invalid.Store(tt.fields.invalid)
|
||||||
|
e.lastUse.Store(tt.fields.lastUse.UnixMicro())
|
||||||
|
got := e.isValid(tt.config)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
21
backend/storage/cache/connector/noop/noop.go
vendored
Normal file
21
backend/storage/cache/connector/noop/noop.go
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package noop
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/zitadel/zitadel/backend/storage/cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
type noop[I, K comparable, V cache.Entry[I, K]] struct{}
|
||||||
|
|
||||||
|
// NewCache returns a cache that does nothing
|
||||||
|
func NewCache[I, K comparable, V cache.Entry[I, K]]() cache.Cache[I, K, V] {
|
||||||
|
return noop[I, K, V]{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (noop[I, K, V]) Set(context.Context, V) {}
|
||||||
|
func (noop[I, K, V]) Get(context.Context, I, K) (value V, ok bool) { return }
|
||||||
|
func (noop[I, K, V]) Invalidate(context.Context, I, ...K) (err error) { return }
|
||||||
|
func (noop[I, K, V]) Delete(context.Context, I, ...K) (err error) { return }
|
||||||
|
func (noop[I, K, V]) Prune(context.Context) (err error) { return }
|
||||||
|
func (noop[I, K, V]) Truncate(context.Context) (err error) { return }
|
98
backend/storage/cache/connector_enumer.go
vendored
Normal file
98
backend/storage/cache/connector_enumer.go
vendored
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
// Code generated by "enumer -type Connector -transform snake -trimprefix Connector -linecomment -text"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const _ConnectorName = "memorypostgresredis"
|
||||||
|
|
||||||
|
var _ConnectorIndex = [...]uint8{0, 0, 6, 14, 19}
|
||||||
|
|
||||||
|
const _ConnectorLowerName = "memorypostgresredis"
|
||||||
|
|
||||||
|
func (i Connector) String() string {
|
||||||
|
if i < 0 || i >= Connector(len(_ConnectorIndex)-1) {
|
||||||
|
return fmt.Sprintf("Connector(%d)", i)
|
||||||
|
}
|
||||||
|
return _ConnectorName[_ConnectorIndex[i]:_ConnectorIndex[i+1]]
|
||||||
|
}
|
||||||
|
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
func _ConnectorNoOp() {
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[ConnectorUnspecified-(0)]
|
||||||
|
_ = x[ConnectorMemory-(1)]
|
||||||
|
_ = x[ConnectorPostgres-(2)]
|
||||||
|
_ = x[ConnectorRedis-(3)]
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ConnectorValues = []Connector{ConnectorUnspecified, ConnectorMemory, ConnectorPostgres, ConnectorRedis}
|
||||||
|
|
||||||
|
var _ConnectorNameToValueMap = map[string]Connector{
|
||||||
|
_ConnectorName[0:0]: ConnectorUnspecified,
|
||||||
|
_ConnectorLowerName[0:0]: ConnectorUnspecified,
|
||||||
|
_ConnectorName[0:6]: ConnectorMemory,
|
||||||
|
_ConnectorLowerName[0:6]: ConnectorMemory,
|
||||||
|
_ConnectorName[6:14]: ConnectorPostgres,
|
||||||
|
_ConnectorLowerName[6:14]: ConnectorPostgres,
|
||||||
|
_ConnectorName[14:19]: ConnectorRedis,
|
||||||
|
_ConnectorLowerName[14:19]: ConnectorRedis,
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ConnectorNames = []string{
|
||||||
|
_ConnectorName[0:0],
|
||||||
|
_ConnectorName[0:6],
|
||||||
|
_ConnectorName[6:14],
|
||||||
|
_ConnectorName[14:19],
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectorString retrieves an enum value from the enum constants string name.
|
||||||
|
// Throws an error if the param is not part of the enum.
|
||||||
|
func ConnectorString(s string) (Connector, error) {
|
||||||
|
if val, ok := _ConnectorNameToValueMap[s]; ok {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, ok := _ConnectorNameToValueMap[strings.ToLower(s)]; ok {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("%s does not belong to Connector values", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectorValues returns all values of the enum
|
||||||
|
func ConnectorValues() []Connector {
|
||||||
|
return _ConnectorValues
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectorStrings returns a slice of all String values of the enum
|
||||||
|
func ConnectorStrings() []string {
|
||||||
|
strs := make([]string, len(_ConnectorNames))
|
||||||
|
copy(strs, _ConnectorNames)
|
||||||
|
return strs
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAConnector returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||||
|
func (i Connector) IsAConnector() bool {
|
||||||
|
for _, v := range _ConnectorValues {
|
||||||
|
if i == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText implements the encoding.TextMarshaler interface for Connector
|
||||||
|
func (i Connector) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(i.String()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText implements the encoding.TextUnmarshaler interface for Connector
|
||||||
|
func (i *Connector) UnmarshalText(text []byte) error {
|
||||||
|
var err error
|
||||||
|
*i, err = ConnectorString(string(text))
|
||||||
|
return err
|
||||||
|
}
|
29
backend/storage/cache/error.go
vendored
Normal file
29
backend/storage/cache/error.go
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IndexUnknownError[I comparable] struct {
|
||||||
|
index I
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIndexUnknownErr[I comparable](index I) error {
|
||||||
|
return IndexUnknownError[I]{index}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i IndexUnknownError[I]) Error() string {
|
||||||
|
return fmt.Sprintf("index %v unknown", i.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a IndexUnknownError[I]) Is(err error) bool {
|
||||||
|
if b, ok := err.(IndexUnknownError[I]); ok {
|
||||||
|
return a.index == b.index
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrCacheMiss = errors.New("cache miss")
|
||||||
|
)
|
54
backend/storage/cache/gomap/map.go
vendored
54
backend/storage/cache/gomap/map.go
vendored
@@ -1,54 +0,0 @@
|
|||||||
package gomap
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/zitadel/zitadel/backend/storage/cache"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Map[K comparable, V any] struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
items map[K]V
|
|
||||||
}
|
|
||||||
|
|
||||||
func New[K comparable, V any]() *Map[K, V] {
|
|
||||||
return &Map[K, V]{
|
|
||||||
items: make(map[K]V),
|
|
||||||
mu: sync.RWMutex{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear implements cache.Cache.
|
|
||||||
func (m *Map[K, V]) Clear() {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
m.items = make(map[K]V, len(m.items))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete implements cache.Cache.
|
|
||||||
func (m *Map[K, V]) Delete(key K) {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
delete(m.items, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get implements cache.Cache.
|
|
||||||
func (m *Map[K, V]) Get(key K) (V, bool) {
|
|
||||||
m.mu.RLock()
|
|
||||||
defer m.mu.RUnlock()
|
|
||||||
|
|
||||||
value, exists := m.items[key]
|
|
||||||
return value, exists
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set implements cache.Cache.
|
|
||||||
func (m *Map[K, V]) Set(key K, value V) {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
m.items[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ cache.Cache[string, string] = &Map[string, string]{}
|
|
76
backend/storage/cache/pruner.go
vendored
Normal file
76
backend/storage/cache/pruner.go
vendored
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jonboulle/clockwork"
|
||||||
|
"github.com/zitadel/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pruner is an optional [Cache] interface.
|
||||||
|
type Pruner interface {
|
||||||
|
// Prune deletes all invalidated or expired objects.
|
||||||
|
Prune(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type PrunerCache[I, K comparable, V Entry[I, K]] interface {
|
||||||
|
Cache[I, K, V]
|
||||||
|
Pruner
|
||||||
|
}
|
||||||
|
|
||||||
|
type AutoPruneConfig struct {
|
||||||
|
// Interval at which the cache is automatically pruned.
|
||||||
|
// 0 or lower disables automatic pruning.
|
||||||
|
Interval time.Duration
|
||||||
|
|
||||||
|
// Timeout for an automatic prune.
|
||||||
|
// It is recommended to keep the value shorter than AutoPruneInterval
|
||||||
|
// 0 or lower disables automatic pruning.
|
||||||
|
Timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c AutoPruneConfig) StartAutoPrune(background context.Context, pruner Pruner, purpose Purpose) (close func()) {
|
||||||
|
return c.startAutoPrune(background, pruner, purpose, clockwork.NewRealClock())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AutoPruneConfig) startAutoPrune(background context.Context, pruner Pruner, purpose Purpose, clock clockwork.Clock) (close func()) {
|
||||||
|
if c.Interval <= 0 {
|
||||||
|
return func() {}
|
||||||
|
}
|
||||||
|
background, cancel := context.WithCancel(background)
|
||||||
|
// randomize the first interval
|
||||||
|
timer := clock.NewTimer(time.Duration(rand.Int63n(int64(c.Interval))))
|
||||||
|
go c.pruneTimer(background, pruner, purpose, timer)
|
||||||
|
return cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AutoPruneConfig) pruneTimer(background context.Context, pruner Pruner, purpose Purpose, timer clockwork.Timer) {
|
||||||
|
defer func() {
|
||||||
|
if !timer.Stop() {
|
||||||
|
<-timer.Chan()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-background.Done():
|
||||||
|
return
|
||||||
|
case <-timer.Chan():
|
||||||
|
err := c.doPrune(background, pruner)
|
||||||
|
logging.OnError(err).WithField("purpose", purpose).Error("cache auto prune")
|
||||||
|
timer.Reset(c.Interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AutoPruneConfig) doPrune(background context.Context, pruner Pruner) error {
|
||||||
|
ctx, cancel := context.WithCancel(background)
|
||||||
|
defer cancel()
|
||||||
|
if c.Timeout > 0 {
|
||||||
|
ctx, cancel = context.WithTimeout(background, c.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
}
|
||||||
|
return pruner.Prune(ctx)
|
||||||
|
}
|
43
backend/storage/cache/pruner_test.go
vendored
Normal file
43
backend/storage/cache/pruner_test.go
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jonboulle/clockwork"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
type testPruner struct {
|
||||||
|
called chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *testPruner) Prune(context.Context) error {
|
||||||
|
p.called <- struct{}{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAutoPruneConfig_startAutoPrune(t *testing.T) {
|
||||||
|
c := AutoPruneConfig{
|
||||||
|
Interval: time.Second,
|
||||||
|
Timeout: time.Millisecond,
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
pruner := testPruner{
|
||||||
|
called: make(chan struct{}),
|
||||||
|
}
|
||||||
|
clock := clockwork.NewFakeClock()
|
||||||
|
close := c.startAutoPrune(ctx, &pruner, PurposeAuthzInstance, clock)
|
||||||
|
defer close()
|
||||||
|
clock.Advance(time.Second)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case _, ok := <-pruner.called:
|
||||||
|
assert.True(t, ok)
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.Fatal(ctx.Err())
|
||||||
|
}
|
||||||
|
}
|
90
backend/storage/cache/purpose_enumer.go
vendored
Normal file
90
backend/storage/cache/purpose_enumer.go
vendored
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
// Code generated by "enumer -type Purpose -transform snake -trimprefix Purpose"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const _PurposeName = "unspecifiedauthz_instancemilestonesorganizationid_p_form_callback"
|
||||||
|
|
||||||
|
var _PurposeIndex = [...]uint8{0, 11, 25, 35, 47, 65}
|
||||||
|
|
||||||
|
const _PurposeLowerName = "unspecifiedauthz_instancemilestonesorganizationid_p_form_callback"
|
||||||
|
|
||||||
|
func (i Purpose) String() string {
|
||||||
|
if i < 0 || i >= Purpose(len(_PurposeIndex)-1) {
|
||||||
|
return fmt.Sprintf("Purpose(%d)", i)
|
||||||
|
}
|
||||||
|
return _PurposeName[_PurposeIndex[i]:_PurposeIndex[i+1]]
|
||||||
|
}
|
||||||
|
|
||||||
|
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||||
|
// Re-run the stringer command to generate them again.
|
||||||
|
func _PurposeNoOp() {
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[PurposeUnspecified-(0)]
|
||||||
|
_ = x[PurposeAuthzInstance-(1)]
|
||||||
|
_ = x[PurposeMilestones-(2)]
|
||||||
|
_ = x[PurposeOrganization-(3)]
|
||||||
|
_ = x[PurposeIdPFormCallback-(4)]
|
||||||
|
}
|
||||||
|
|
||||||
|
var _PurposeValues = []Purpose{PurposeUnspecified, PurposeAuthzInstance, PurposeMilestones, PurposeOrganization, PurposeIdPFormCallback}
|
||||||
|
|
||||||
|
var _PurposeNameToValueMap = map[string]Purpose{
|
||||||
|
_PurposeName[0:11]: PurposeUnspecified,
|
||||||
|
_PurposeLowerName[0:11]: PurposeUnspecified,
|
||||||
|
_PurposeName[11:25]: PurposeAuthzInstance,
|
||||||
|
_PurposeLowerName[11:25]: PurposeAuthzInstance,
|
||||||
|
_PurposeName[25:35]: PurposeMilestones,
|
||||||
|
_PurposeLowerName[25:35]: PurposeMilestones,
|
||||||
|
_PurposeName[35:47]: PurposeOrganization,
|
||||||
|
_PurposeLowerName[35:47]: PurposeOrganization,
|
||||||
|
_PurposeName[47:65]: PurposeIdPFormCallback,
|
||||||
|
_PurposeLowerName[47:65]: PurposeIdPFormCallback,
|
||||||
|
}
|
||||||
|
|
||||||
|
var _PurposeNames = []string{
|
||||||
|
_PurposeName[0:11],
|
||||||
|
_PurposeName[11:25],
|
||||||
|
_PurposeName[25:35],
|
||||||
|
_PurposeName[35:47],
|
||||||
|
_PurposeName[47:65],
|
||||||
|
}
|
||||||
|
|
||||||
|
// PurposeString retrieves an enum value from the enum constants string name.
|
||||||
|
// Throws an error if the param is not part of the enum.
|
||||||
|
func PurposeString(s string) (Purpose, error) {
|
||||||
|
if val, ok := _PurposeNameToValueMap[s]; ok {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, ok := _PurposeNameToValueMap[strings.ToLower(s)]; ok {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("%s does not belong to Purpose values", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PurposeValues returns all values of the enum
|
||||||
|
func PurposeValues() []Purpose {
|
||||||
|
return _PurposeValues
|
||||||
|
}
|
||||||
|
|
||||||
|
// PurposeStrings returns a slice of all String values of the enum
|
||||||
|
func PurposeStrings() []string {
|
||||||
|
strs := make([]string, len(_PurposeNames))
|
||||||
|
copy(strs, _PurposeNames)
|
||||||
|
return strs
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAPurpose returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||||
|
func (i Purpose) IsAPurpose() bool {
|
||||||
|
for _, v := range _PurposeValues {
|
||||||
|
if i == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
Reference in New Issue
Block a user