mirror of
https://github.com/zitadel/zitadel.git
synced 2025-06-02 05:18:19 +00:00
feat(cache): organization (#8903)
# Which Problems Are Solved Organizations are ofter searched for by ID or primary domain. This results in many redundant queries, resulting in a performance impact. # How the Problems Are Solved Cache Organizaion objects by ID and primary domain. # Additional Changes - Adjust integration test config to use all types of cache. - Adjust integration test lifetimes so the pruner has something to do while the tests run. # Additional Context - Closes #8865 - After #8902
This commit is contained in:
parent
041c3d9b9e
commit
c165ed07f4
@ -328,6 +328,16 @@ Caches:
|
|||||||
AddSource: true
|
AddSource: true
|
||||||
Formatter:
|
Formatter:
|
||||||
Format: text
|
Format: text
|
||||||
|
# Organization cache, gettable by primary domain or ID.
|
||||||
|
Organization:
|
||||||
|
Connector: ""
|
||||||
|
MaxAge: 1h
|
||||||
|
LastUsage: 10m
|
||||||
|
Log:
|
||||||
|
Level: error
|
||||||
|
AddSource: true
|
||||||
|
Formatter:
|
||||||
|
Format: text
|
||||||
|
|
||||||
Machine:
|
Machine:
|
||||||
# Cloud-hosted VMs need to specify their metadata endpoint so that the machine can be uniquely identified.
|
# Cloud-hosted VMs need to specify their metadata endpoint so that the machine can be uniquely identified.
|
||||||
|
@ -176,6 +176,16 @@ Milestones are reached upon the first time a certain action is performed. For ex
|
|||||||
As an extra optimization, once all milestones are reached by the instance, an in-memory flag is set and the milestone state is never queried again from the database nor cache.
|
As an extra optimization, once all milestones are reached by the instance, an in-memory flag is set and the milestone state is never queried again from the database nor cache.
|
||||||
For single instance setups which fulfilled all milestone (*your next steps* in console) it is not needed to enable this cache. We mainly use it for ZITADEL cloud where there are many instances with *incomplete* milestones.
|
For single instance setups which fulfilled all milestone (*your next steps* in console) it is not needed to enable this cache. We mainly use it for ZITADEL cloud where there are many instances with *incomplete* milestones.
|
||||||
|
|
||||||
|
### Organization
|
||||||
|
|
||||||
|
Most resources like users, project and applications are part of an [organization](/docs/concepts/structure/organizations). Therefore many parts of the ZITADEL logic search for an organization by ID or by their primary domain.
|
||||||
|
Organization objects are quite small and receive infrequent updates after they are created:
|
||||||
|
|
||||||
|
- Change of organization name
|
||||||
|
- Deactivation / Reactivation
|
||||||
|
- Change of primary domain
|
||||||
|
- Removal
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
Currently caches are in beta and disabled by default. However, if you want to give caching a try, the following sections contains some suggested configurations for different setups.
|
Currently caches are in beta and disabled by default. However, if you want to give caching a try, the following sections contains some suggested configurations for different setups.
|
||||||
@ -189,6 +199,9 @@ Caches:
|
|||||||
Instance:
|
Instance:
|
||||||
Connector: "memory"
|
Connector: "memory"
|
||||||
MaxAge: 1h
|
MaxAge: 1h
|
||||||
|
Organization:
|
||||||
|
Connector: "memory"
|
||||||
|
MaxAge: 1h
|
||||||
```
|
```
|
||||||
|
|
||||||
The following configuration is recommended for single instance setups with high traffic on multiple servers, where Redis is not available:
|
The following configuration is recommended for single instance setups with high traffic on multiple servers, where Redis is not available:
|
||||||
@ -206,6 +219,9 @@ Caches:
|
|||||||
Connector: "postgres"
|
Connector: "postgres"
|
||||||
MaxAge: 1h
|
MaxAge: 1h
|
||||||
LastUsage: 10m
|
LastUsage: 10m
|
||||||
|
Organization:
|
||||||
|
Connector: "memory"
|
||||||
|
MaxAge: 1s
|
||||||
```
|
```
|
||||||
|
|
||||||
When running many instances on multiple servers:
|
When running many instances on multiple servers:
|
||||||
@ -225,6 +241,10 @@ Caches:
|
|||||||
Connector: "redis"
|
Connector: "redis"
|
||||||
MaxAge: 1h
|
MaxAge: 1h
|
||||||
LastUsage: 10m
|
LastUsage: 10m
|
||||||
|
Organization:
|
||||||
|
Connector: "redis"
|
||||||
|
MaxAge: 1h
|
||||||
|
LastUsage: 10m
|
||||||
```
|
```
|
||||||
----
|
----
|
||||||
|
|
||||||
|
1
internal/cache/cache.go
vendored
1
internal/cache/cache.go
vendored
@ -16,6 +16,7 @@ const (
|
|||||||
PurposeUnspecified Purpose = iota
|
PurposeUnspecified Purpose = iota
|
||||||
PurposeAuthzInstance
|
PurposeAuthzInstance
|
||||||
PurposeMilestones
|
PurposeMilestones
|
||||||
|
PurposeOrganization
|
||||||
)
|
)
|
||||||
|
|
||||||
// Cache stores objects with a value of type `V`.
|
// Cache stores objects with a value of type `V`.
|
||||||
|
5
internal/cache/connector/connector.go
vendored
5
internal/cache/connector/connector.go
vendored
@ -19,8 +19,9 @@ type CachesConfig struct {
|
|||||||
Postgres pg.Config
|
Postgres pg.Config
|
||||||
Redis redis.Config
|
Redis redis.Config
|
||||||
}
|
}
|
||||||
Instance *cache.Config
|
Instance *cache.Config
|
||||||
Milestones *cache.Config
|
Milestones *cache.Config
|
||||||
|
Organization *cache.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
type Connectors struct {
|
type Connectors struct {
|
||||||
|
12
internal/cache/purpose_enumer.go
vendored
12
internal/cache/purpose_enumer.go
vendored
@ -7,11 +7,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const _PurposeName = "unspecifiedauthz_instancemilestones"
|
const _PurposeName = "unspecifiedauthz_instancemilestonesorganization"
|
||||||
|
|
||||||
var _PurposeIndex = [...]uint8{0, 11, 25, 35}
|
var _PurposeIndex = [...]uint8{0, 11, 25, 35, 47}
|
||||||
|
|
||||||
const _PurposeLowerName = "unspecifiedauthz_instancemilestones"
|
const _PurposeLowerName = "unspecifiedauthz_instancemilestonesorganization"
|
||||||
|
|
||||||
func (i Purpose) String() string {
|
func (i Purpose) String() string {
|
||||||
if i < 0 || i >= Purpose(len(_PurposeIndex)-1) {
|
if i < 0 || i >= Purpose(len(_PurposeIndex)-1) {
|
||||||
@ -27,9 +27,10 @@ func _PurposeNoOp() {
|
|||||||
_ = x[PurposeUnspecified-(0)]
|
_ = x[PurposeUnspecified-(0)]
|
||||||
_ = x[PurposeAuthzInstance-(1)]
|
_ = x[PurposeAuthzInstance-(1)]
|
||||||
_ = x[PurposeMilestones-(2)]
|
_ = x[PurposeMilestones-(2)]
|
||||||
|
_ = x[PurposeOrganization-(3)]
|
||||||
}
|
}
|
||||||
|
|
||||||
var _PurposeValues = []Purpose{PurposeUnspecified, PurposeAuthzInstance, PurposeMilestones}
|
var _PurposeValues = []Purpose{PurposeUnspecified, PurposeAuthzInstance, PurposeMilestones, PurposeOrganization}
|
||||||
|
|
||||||
var _PurposeNameToValueMap = map[string]Purpose{
|
var _PurposeNameToValueMap = map[string]Purpose{
|
||||||
_PurposeName[0:11]: PurposeUnspecified,
|
_PurposeName[0:11]: PurposeUnspecified,
|
||||||
@ -38,12 +39,15 @@ var _PurposeNameToValueMap = map[string]Purpose{
|
|||||||
_PurposeLowerName[11:25]: PurposeAuthzInstance,
|
_PurposeLowerName[11:25]: PurposeAuthzInstance,
|
||||||
_PurposeName[25:35]: PurposeMilestones,
|
_PurposeName[25:35]: PurposeMilestones,
|
||||||
_PurposeLowerName[25:35]: PurposeMilestones,
|
_PurposeLowerName[25:35]: PurposeMilestones,
|
||||||
|
_PurposeName[35:47]: PurposeOrganization,
|
||||||
|
_PurposeLowerName[35:47]: PurposeOrganization,
|
||||||
}
|
}
|
||||||
|
|
||||||
var _PurposeNames = []string{
|
var _PurposeNames = []string{
|
||||||
_PurposeName[0:11],
|
_PurposeName[0:11],
|
||||||
_PurposeName[11:25],
|
_PurposeName[11:25],
|
||||||
_PurposeName[25:35],
|
_PurposeName[25:35],
|
||||||
|
_PurposeName[35:47],
|
||||||
}
|
}
|
||||||
|
|
||||||
// PurposeString retrieves an enum value from the enum constants string name.
|
// PurposeString retrieves an enum value from the enum constants string name.
|
||||||
|
@ -8,28 +8,30 @@ TLS:
|
|||||||
|
|
||||||
Caches:
|
Caches:
|
||||||
Connectors:
|
Connectors:
|
||||||
|
Memory:
|
||||||
|
Enabled: true
|
||||||
Postgres:
|
Postgres:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
Redis:
|
Redis:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
Instance:
|
Instance:
|
||||||
Connector: "redis"
|
Connector: "memory"
|
||||||
MaxAge: 1h
|
MaxAge: 5m
|
||||||
LastUsage: 10m
|
LastUsage: 1m
|
||||||
Log:
|
Log:
|
||||||
Level: info
|
Level: info
|
||||||
AddSource: true
|
|
||||||
Formatter:
|
|
||||||
Format: text
|
|
||||||
Milestones:
|
Milestones:
|
||||||
Connector: "postgres"
|
Connector: "postgres"
|
||||||
MaxAge: 1h
|
MaxAge: 5m
|
||||||
LastUsage: 10m
|
LastUsage: 1m
|
||||||
|
Log:
|
||||||
|
Level: info
|
||||||
|
Organization:
|
||||||
|
Connector: "redis"
|
||||||
|
MaxAge: 5m
|
||||||
|
LastUsage: 1m
|
||||||
Log:
|
Log:
|
||||||
Level: info
|
Level: info
|
||||||
AddSource: true
|
|
||||||
Formatter:
|
|
||||||
Format: text
|
|
||||||
|
|
||||||
Quotas:
|
Quotas:
|
||||||
Access:
|
Access:
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
type Caches struct {
|
type Caches struct {
|
||||||
instance cache.Cache[instanceIndex, string, *authzInstance]
|
instance cache.Cache[instanceIndex, string, *authzInstance]
|
||||||
|
org cache.Cache[orgIndex, string, *Org]
|
||||||
}
|
}
|
||||||
|
|
||||||
func startCaches(background context.Context, connectors connector.Connectors) (_ *Caches, err error) {
|
func startCaches(background context.Context, connectors connector.Connectors) (_ *Caches, err error) {
|
||||||
@ -20,7 +21,13 @@ func startCaches(background context.Context, connectors connector.Connectors) (_
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
caches.org, err = connector.StartCache[orgIndex, string, *Org](background, orgIndexValues(), cache.PurposeOrganization, connectors.Config.Organization, connectors)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
caches.registerInstanceInvalidation()
|
caches.registerInstanceInvalidation()
|
||||||
|
caches.registerOrgInvalidation()
|
||||||
return caches, nil
|
return caches, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -522,9 +522,9 @@ func (i *authzInstance) Keys(index instanceIndex) []string {
|
|||||||
return []string{i.ID}
|
return []string{i.ID}
|
||||||
case instanceIndexByHost:
|
case instanceIndexByHost:
|
||||||
return i.ExternalDomains
|
return i.ExternalDomains
|
||||||
default:
|
case instanceIndexUnspecified:
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func scanAuthzInstance() (*authzInstance, func(row *sql.Row) error) {
|
func scanAuthzInstance() (*authzInstance, func(row *sql.Row) error) {
|
||||||
|
@ -107,10 +107,19 @@ func (q *OrgSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
|
|||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) OrgByID(ctx context.Context, shouldTriggerBulk bool, id string) (_ *Org, err error) {
|
func (q *Queries) OrgByID(ctx context.Context, shouldTriggerBulk bool, id string) (org *Org, err error) {
|
||||||
ctx, span := tracing.NewSpan(ctx)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
|
if org, ok := q.caches.org.Get(ctx, orgIndexByID, id); ok {
|
||||||
|
return org, nil
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err == nil && org != nil {
|
||||||
|
q.caches.org.Set(ctx, org)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if !authz.GetInstance(ctx).Features().ShouldUseImprovedPerformance(feature.ImprovedPerformanceTypeOrgByID) {
|
if !authz.GetInstance(ctx).Features().ShouldUseImprovedPerformance(feature.ImprovedPerformanceTypeOrgByID) {
|
||||||
return q.oldOrgByID(ctx, shouldTriggerBulk, id)
|
return q.oldOrgByID(ctx, shouldTriggerBulk, id)
|
||||||
}
|
}
|
||||||
@ -175,6 +184,11 @@ func (q *Queries) OrgByPrimaryDomain(ctx context.Context, domain string) (org *O
|
|||||||
ctx, span := tracing.NewSpan(ctx)
|
ctx, span := tracing.NewSpan(ctx)
|
||||||
defer func() { span.EndWithError(err) }()
|
defer func() { span.EndWithError(err) }()
|
||||||
|
|
||||||
|
org, ok := q.caches.org.Get(ctx, orgIndexByPrimaryDomain, domain)
|
||||||
|
if ok {
|
||||||
|
return org, nil
|
||||||
|
}
|
||||||
|
|
||||||
stmt, scan := prepareOrgQuery(ctx, q.client)
|
stmt, scan := prepareOrgQuery(ctx, q.client)
|
||||||
query, args, err := stmt.Where(sq.Eq{
|
query, args, err := stmt.Where(sq.Eq{
|
||||||
OrgColumnDomain.identifier(): domain,
|
OrgColumnDomain.identifier(): domain,
|
||||||
@ -189,6 +203,9 @@ func (q *Queries) OrgByPrimaryDomain(ctx context.Context, domain string) (org *O
|
|||||||
org, err = scan(row)
|
org, err = scan(row)
|
||||||
return err
|
return err
|
||||||
}, query, args...)
|
}, query, args...)
|
||||||
|
if err == nil {
|
||||||
|
q.caches.org.Set(ctx, org)
|
||||||
|
}
|
||||||
return org, err
|
return org, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -476,3 +493,30 @@ func prepareOrgUniqueQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu
|
|||||||
return isUnique, err
|
return isUnique, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type orgIndex int
|
||||||
|
|
||||||
|
//go:generate enumer -type orgIndex -linecomment
|
||||||
|
const (
|
||||||
|
// Empty line comment ensures empty string for unspecified value
|
||||||
|
orgIndexUnspecified orgIndex = iota //
|
||||||
|
orgIndexByID
|
||||||
|
orgIndexByPrimaryDomain
|
||||||
|
)
|
||||||
|
|
||||||
|
// Keys implements [cache.Entry]
|
||||||
|
func (o *Org) Keys(index orgIndex) []string {
|
||||||
|
switch index {
|
||||||
|
case orgIndexByID:
|
||||||
|
return []string{o.ID}
|
||||||
|
case orgIndexByPrimaryDomain:
|
||||||
|
return []string{o.Domain}
|
||||||
|
case orgIndexUnspecified:
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Caches) registerOrgInvalidation() {
|
||||||
|
invalidate := cacheInvalidationFunc(c.instance, instanceIndexByID, getAggregateID)
|
||||||
|
projection.OrgProjection.RegisterCacheInvalidation(invalidate)
|
||||||
|
}
|
||||||
|
82
internal/query/orgindex_enumer.go
Normal file
82
internal/query/orgindex_enumer.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
// Code generated by "enumer -type orgIndex -linecomment"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package query
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const _orgIndexName = "orgIndexByIDorgIndexByPrimaryDomain"
|
||||||
|
|
||||||
|
var _orgIndexIndex = [...]uint8{0, 0, 12, 35}
|
||||||
|
|
||||||
|
const _orgIndexLowerName = "orgindexbyidorgindexbyprimarydomain"
|
||||||
|
|
||||||
|
func (i orgIndex) String() string {
|
||||||
|
if i < 0 || i >= orgIndex(len(_orgIndexIndex)-1) {
|
||||||
|
return fmt.Sprintf("orgIndex(%d)", i)
|
||||||
|
}
|
||||||
|
return _orgIndexName[_orgIndexIndex[i]:_orgIndexIndex[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 _orgIndexNoOp() {
|
||||||
|
var x [1]struct{}
|
||||||
|
_ = x[orgIndexUnspecified-(0)]
|
||||||
|
_ = x[orgIndexByID-(1)]
|
||||||
|
_ = x[orgIndexByPrimaryDomain-(2)]
|
||||||
|
}
|
||||||
|
|
||||||
|
var _orgIndexValues = []orgIndex{orgIndexUnspecified, orgIndexByID, orgIndexByPrimaryDomain}
|
||||||
|
|
||||||
|
var _orgIndexNameToValueMap = map[string]orgIndex{
|
||||||
|
_orgIndexName[0:0]: orgIndexUnspecified,
|
||||||
|
_orgIndexLowerName[0:0]: orgIndexUnspecified,
|
||||||
|
_orgIndexName[0:12]: orgIndexByID,
|
||||||
|
_orgIndexLowerName[0:12]: orgIndexByID,
|
||||||
|
_orgIndexName[12:35]: orgIndexByPrimaryDomain,
|
||||||
|
_orgIndexLowerName[12:35]: orgIndexByPrimaryDomain,
|
||||||
|
}
|
||||||
|
|
||||||
|
var _orgIndexNames = []string{
|
||||||
|
_orgIndexName[0:0],
|
||||||
|
_orgIndexName[0:12],
|
||||||
|
_orgIndexName[12:35],
|
||||||
|
}
|
||||||
|
|
||||||
|
// orgIndexString retrieves an enum value from the enum constants string name.
|
||||||
|
// Throws an error if the param is not part of the enum.
|
||||||
|
func orgIndexString(s string) (orgIndex, error) {
|
||||||
|
if val, ok := _orgIndexNameToValueMap[s]; ok {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, ok := _orgIndexNameToValueMap[strings.ToLower(s)]; ok {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("%s does not belong to orgIndex values", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// orgIndexValues returns all values of the enum
|
||||||
|
func orgIndexValues() []orgIndex {
|
||||||
|
return _orgIndexValues
|
||||||
|
}
|
||||||
|
|
||||||
|
// orgIndexStrings returns a slice of all String values of the enum
|
||||||
|
func orgIndexStrings() []string {
|
||||||
|
strs := make([]string, len(_orgIndexNames))
|
||||||
|
copy(strs, _orgIndexNames)
|
||||||
|
return strs
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsAorgIndex returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||||
|
func (i orgIndex) IsAorgIndex() bool {
|
||||||
|
for _, v := range _orgIndexValues {
|
||||||
|
if i == v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user