mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-04 15:35:10 +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
|
||||
Formatter:
|
||||
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:
|
||||
# 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.
|
||||
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
|
||||
|
||||
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:
|
||||
Connector: "memory"
|
||||
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:
|
||||
@ -206,6 +219,9 @@ Caches:
|
||||
Connector: "postgres"
|
||||
MaxAge: 1h
|
||||
LastUsage: 10m
|
||||
Organization:
|
||||
Connector: "memory"
|
||||
MaxAge: 1s
|
||||
```
|
||||
|
||||
When running many instances on multiple servers:
|
||||
@ -225,6 +241,10 @@ Caches:
|
||||
Connector: "redis"
|
||||
MaxAge: 1h
|
||||
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
|
||||
PurposeAuthzInstance
|
||||
PurposeMilestones
|
||||
PurposeOrganization
|
||||
)
|
||||
|
||||
// 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
|
||||
Redis redis.Config
|
||||
}
|
||||
Instance *cache.Config
|
||||
Milestones *cache.Config
|
||||
Instance *cache.Config
|
||||
Milestones *cache.Config
|
||||
Organization *cache.Config
|
||||
}
|
||||
|
||||
type Connectors struct {
|
||||
|
12
internal/cache/purpose_enumer.go
vendored
12
internal/cache/purpose_enumer.go
vendored
@ -7,11 +7,11 @@ import (
|
||||
"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 {
|
||||
if i < 0 || i >= Purpose(len(_PurposeIndex)-1) {
|
||||
@ -27,9 +27,10 @@ func _PurposeNoOp() {
|
||||
_ = x[PurposeUnspecified-(0)]
|
||||
_ = x[PurposeAuthzInstance-(1)]
|
||||
_ = x[PurposeMilestones-(2)]
|
||||
_ = x[PurposeOrganization-(3)]
|
||||
}
|
||||
|
||||
var _PurposeValues = []Purpose{PurposeUnspecified, PurposeAuthzInstance, PurposeMilestones}
|
||||
var _PurposeValues = []Purpose{PurposeUnspecified, PurposeAuthzInstance, PurposeMilestones, PurposeOrganization}
|
||||
|
||||
var _PurposeNameToValueMap = map[string]Purpose{
|
||||
_PurposeName[0:11]: PurposeUnspecified,
|
||||
@ -38,12 +39,15 @@ var _PurposeNameToValueMap = map[string]Purpose{
|
||||
_PurposeLowerName[11:25]: PurposeAuthzInstance,
|
||||
_PurposeName[25:35]: PurposeMilestones,
|
||||
_PurposeLowerName[25:35]: PurposeMilestones,
|
||||
_PurposeName[35:47]: PurposeOrganization,
|
||||
_PurposeLowerName[35:47]: PurposeOrganization,
|
||||
}
|
||||
|
||||
var _PurposeNames = []string{
|
||||
_PurposeName[0:11],
|
||||
_PurposeName[11:25],
|
||||
_PurposeName[25:35],
|
||||
_PurposeName[35:47],
|
||||
}
|
||||
|
||||
// PurposeString retrieves an enum value from the enum constants string name.
|
||||
|
@ -8,28 +8,30 @@ TLS:
|
||||
|
||||
Caches:
|
||||
Connectors:
|
||||
Memory:
|
||||
Enabled: true
|
||||
Postgres:
|
||||
Enabled: true
|
||||
Redis:
|
||||
Enabled: true
|
||||
Instance:
|
||||
Connector: "redis"
|
||||
MaxAge: 1h
|
||||
LastUsage: 10m
|
||||
Connector: "memory"
|
||||
MaxAge: 5m
|
||||
LastUsage: 1m
|
||||
Log:
|
||||
Level: info
|
||||
AddSource: true
|
||||
Formatter:
|
||||
Format: text
|
||||
Milestones:
|
||||
Connector: "postgres"
|
||||
MaxAge: 1h
|
||||
LastUsage: 10m
|
||||
MaxAge: 5m
|
||||
LastUsage: 1m
|
||||
Log:
|
||||
Level: info
|
||||
Organization:
|
||||
Connector: "redis"
|
||||
MaxAge: 5m
|
||||
LastUsage: 1m
|
||||
Log:
|
||||
Level: info
|
||||
AddSource: true
|
||||
Formatter:
|
||||
Format: text
|
||||
|
||||
Quotas:
|
||||
Access:
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
|
||||
type Caches struct {
|
||||
instance cache.Cache[instanceIndex, string, *authzInstance]
|
||||
org cache.Cache[orgIndex, string, *Org]
|
||||
}
|
||||
|
||||
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 {
|
||||
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.registerOrgInvalidation()
|
||||
return caches, nil
|
||||
}
|
||||
|
||||
|
@ -522,9 +522,9 @@ func (i *authzInstance) Keys(index instanceIndex) []string {
|
||||
return []string{i.ID}
|
||||
case instanceIndexByHost:
|
||||
return i.ExternalDomains
|
||||
default:
|
||||
return nil
|
||||
case instanceIndexUnspecified:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func scanAuthzInstance() (*authzInstance, func(row *sql.Row) error) {
|
||||
|
@ -107,10 +107,19 @@ func (q *OrgSearchQueries) toQuery(query sq.SelectBuilder) sq.SelectBuilder {
|
||||
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)
|
||||
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) {
|
||||
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)
|
||||
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)
|
||||
query, args, err := stmt.Where(sq.Eq{
|
||||
OrgColumnDomain.identifier(): domain,
|
||||
@ -189,6 +203,9 @@ func (q *Queries) OrgByPrimaryDomain(ctx context.Context, domain string) (org *O
|
||||
org, err = scan(row)
|
||||
return err
|
||||
}, query, args...)
|
||||
if err == nil {
|
||||
q.caches.org.Set(ctx, org)
|
||||
}
|
||||
return org, err
|
||||
}
|
||||
|
||||
@ -476,3 +493,30 @@ func prepareOrgUniqueQuery(ctx context.Context, db prepareDatabase) (sq.SelectBu
|
||||
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…
Reference in New Issue
Block a user