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:
Tim Möhlmann 2024-11-21 08:05:03 +02:00 committed by GitHub
parent 041c3d9b9e
commit c165ed07f4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 191 additions and 20 deletions

View File

@ -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.

View File

@ -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
```
----

View File

@ -16,6 +16,7 @@ const (
PurposeUnspecified Purpose = iota
PurposeAuthzInstance
PurposeMilestones
PurposeOrganization
)
// Cache stores objects with a value of type `V`.

View File

@ -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 {

View File

@ -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.

View File

@ -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:

View File

@ -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
}

View File

@ -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) {

View File

@ -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)
}

View 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
}