Skip to content

Commit 89fbe30

Browse files
authored
feat: multiple consistency levels for authz checks (#832)
So far frontier was either using no consistency level from spicedb which defaults to prioritise minimum latency with minimal consistent results as well. Other was fully consistent calls which provide the most accurate authz resolution but takes substential latency hit. A middle ground is introduced now that caches the zookie generated during the last fully consistent check and uses that in all but Check calls. This should speed up Lookup/List calls for authz. Existing config `fully_consistent` is now deprecated in favor of new consistency level. ``` spicedb: # consistency ensures Authz server consistency guarantees for various operations # Possible values are: # - "full": Guarantees that the data is always fresh although API calls might be slower than usual # - "best_effort": Guarantees that the data is the best effort fresh [default] # - "minimize_latency": Tries to prioritise minimal latency consistency: "best_effort" ``` Signed-off-by: Kush Sharma <thekushsharma@gmail.com>
1 parent b5e5d67 commit 89fbe30

File tree

7 files changed

+111
-39
lines changed

7 files changed

+111
-39
lines changed

cmd/serve.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"syscall"
1313
"time"
1414

15+
"golang.org/x/exp/slices"
16+
1517
"github.com/jackc/pgx/v4"
1618
"github.com/stripe/stripe-go/v79"
1719

@@ -331,7 +333,17 @@ func buildAPIDependencies(
331333
namespaceService := namespace.NewService(namespaceRepository)
332334

333335
authzSchemaRepository := spicedb.NewSchemaRepository(logger, sdb)
334-
authzRelationRepository := spicedb.NewRelationRepository(sdb, cfg.SpiceDB.FullyConsistent, cfg.SpiceDB.CheckTrace)
336+
consistencyLevel := spicedb.ConsistencyLevel(cfg.SpiceDB.Consistency)
337+
if cfg.SpiceDB.FullyConsistent {
338+
consistencyLevel = spicedb.ConsistencyLevelFull
339+
}
340+
if !slices.Contains([]spicedb.ConsistencyLevel{
341+
spicedb.ConsistencyLevelFull,
342+
spicedb.ConsistencyLevelBestEffort,
343+
spicedb.ConsistencyLevelMinimizeLatency}, consistencyLevel) {
344+
return api.Deps{}, fmt.Errorf("invalid consistency level: %s", consistencyLevel)
345+
}
346+
authzRelationRepository := spicedb.NewRelationRepository(sdb, consistencyLevel, cfg.SpiceDB.CheckTrace)
335347

336348
permissionRepository := postgres.NewPermissionRepository(dbc)
337349
permissionService := permission.NewService(permissionRepository)

config/config_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ func TestLoad(t *testing.T) {
3838
Host: "spicedb.localhost",
3939
Port: "50051",
4040
PreSharedKey: "randomkey",
41+
Consistency: spicedb.ConsistencyLevelBestEffort.String(),
4142
},
4243
},
4344
wantErr: false,

config/sample.config.yaml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,12 +160,15 @@ spicedb:
160160
host: spicedb.localhost
161161
pre_shared_key: randomkey
162162
port: 50051
163-
# fully_consistent ensures APIs although slower than usual will result in responses always most consistent
164-
# suggested to keep it false for performance
165-
fully_consistent: false
166163
# check_trace enables tracing in check api for spicedb, it adds considerable
167164
# latency to the check calls and shouldn't be enabled in production
168165
check_trace: false
166+
# consistency ensures Authz server consistency guarantees for various operations
167+
# Possible values are:
168+
# - "full": Guarantees that the data is always fresh although API calls might be slower than usual
169+
# - "best_effort": Guarantees that the data is the best effort fresh [default]
170+
# - "minimize_latency": Tries to prioritise minimal latency
171+
consistency: "best_effort"
169172

170173
billing:
171174
# stripe key to be used for billing

docs/docs/reference/configurations.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,9 +155,12 @@ spicedb:
155155
host: spicedb.localhost
156156
pre_shared_key: randomkey
157157
port: 50051
158-
# fully_consistent ensures APIs although slower than usual will result in responses always most consistent
159-
# suggested to keep it false for performance
160-
fully_consistent: false
158+
# consistency ensures Authz server consistency guarantees for various operations
159+
# Possible values are:
160+
# - "full": Guarantees that the data is always fresh although API calls might be slower than usual
161+
# - "best_effort": Guarantees that the data is the best effort fresh [default]
162+
# - "minimize_latency": Tries to prioritise minimal latency
163+
consistency: "best_effort"
161164
# check_trace enables tracing in check api for spicedb, it adds considerable
162165
# latency to the check calls and shouldn't be enabled in production
163166
check_trace: false

internal/store/spicedb/config.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,17 @@ type Config struct {
55
Port string `yaml:"port" default:"50051"`
66
PreSharedKey string `yaml:"pre_shared_key" mapstructure:"pre_shared_key"`
77

8+
// Deprecated: Use Consistency instead
89
// FullyConsistent ensures APIs although slower than usual will result in responses always most consistent
910
FullyConsistent bool `yaml:"fully_consistent" mapstructure:"fully_consistent" default:"false"`
1011

12+
// Consistency ensures Authz server consistency guarantees for various operations
13+
// Possible values are:
14+
// - "full": Guarantees that the data is always fresh
15+
// - "best_effort": Guarantees that the data is the best effort fresh
16+
// - "minimize_latency": Tries to prioritise minimal latency
17+
Consistency string `yaml:"consistency" mapstructure:"consistency" default:"best_effort"`
18+
1119
// CheckTrace enables tracing in check api for spicedb, it adds considerable
1220
// latency to the check calls and shouldn't be enabled in production
1321
CheckTrace bool `yaml:"check_trace" mapstructure:"check_trace" default:"false"`

internal/store/spicedb/relation_repository.go

Lines changed: 73 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"errors"
77
"fmt"
88
"io"
9+
"sync/atomic"
910

1011
grpczap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
1112
"go.uber.org/zap"
@@ -18,29 +19,48 @@ import (
1819
type RelationRepository struct {
1920
spiceDB *SpiceDB
2021

21-
// fullyConsistent makes sure all APIs are highly consistent on their responses
22-
// turning it on will result in slower API calls but useful in tests
23-
fullyConsistent bool
22+
// Consistency ensures Authz server consistency guarantees for various operations
23+
// Possible values are:
24+
// - "full": Guarantees that the data is always fresh
25+
// - "best_effort": Guarantees that the data is the best effort fresh
26+
// - "minimize_latency": Tries to prioritise minimal latency
27+
consistency ConsistencyLevel
2428

2529
// tracing enables debug traces for check calls
2630
tracing bool
2731

28-
// TODO(kushsharma): after every call, check if the response returns a relationship
29-
// snapshot(zedtoken/zookie), if it does, store it in a cache/db, and use it for subsequent calls
30-
// this will make the calls faster and avoid the use of fully consistent spiceDB
32+
// lastToken is the last zookie returned by the server, this is cached at instance level and
33+
// maybe not be consistent across multiple instances but that is fine in most cases as
34+
// the token is only used in lookup or list calls, for permission checks we always use the
35+
// consistency level. Storing it in a shared db/cache will make it consistent across instances.
36+
// We can also store multiple tokens in the cache based on what kind of resource we are dealing with
37+
// but that adds complexity.
38+
lastToken atomic.Pointer[authzedpb.ZedToken]
3139
}
3240

41+
type ConsistencyLevel string
42+
43+
func (c ConsistencyLevel) String() string {
44+
return string(c)
45+
}
46+
47+
const (
48+
ConsistencyLevelFull ConsistencyLevel = "full"
49+
ConsistencyLevelBestEffort ConsistencyLevel = "best_effort"
50+
ConsistencyLevelMinimizeLatency ConsistencyLevel = "minimize_latency"
51+
)
52+
3353
const nrProductName = "spicedb"
3454

35-
func NewRelationRepository(spiceDB *SpiceDB, fullyConsistent bool, tracing bool) *RelationRepository {
55+
func NewRelationRepository(spiceDB *SpiceDB, consistency ConsistencyLevel, tracing bool) *RelationRepository {
3656
return &RelationRepository{
37-
spiceDB: spiceDB,
38-
fullyConsistent: fullyConsistent,
39-
tracing: tracing,
57+
spiceDB: spiceDB,
58+
consistency: consistency,
59+
tracing: tracing,
4060
}
4161
}
4262

43-
func (r RelationRepository) Add(ctx context.Context, rel relation.Relation) error {
63+
func (r *RelationRepository) Add(ctx context.Context, rel relation.Relation) error {
4464
relationship := &authzedpb.Relationship{
4565
Resource: &authzedpb.ObjectReference{
4666
ObjectType: rel.Object.Namespace,
@@ -79,16 +99,18 @@ func (r RelationRepository) Add(ctx context.Context, rel relation.Relation) erro
7999
defer nr.End()
80100
}
81101

82-
if _, err := r.spiceDB.client.WriteRelationships(ctx, request); err != nil {
102+
resp, err := r.spiceDB.client.WriteRelationships(ctx, request)
103+
if err != nil {
83104
return err
84105
}
85106

107+
r.lastToken.Store(resp.GetWrittenAt())
86108
return nil
87109
}
88110

89-
func (r RelationRepository) Check(ctx context.Context, rel relation.Relation) (bool, error) {
111+
func (r *RelationRepository) Check(ctx context.Context, rel relation.Relation) (bool, error) {
90112
request := &authzedpb.CheckPermissionRequest{
91-
Consistency: r.getConsistency(),
113+
Consistency: r.getConsistencyForCheck(),
92114
Resource: &authzedpb.ObjectReference{
93115
ObjectId: rel.Object.ID,
94116
ObjectType: rel.Object.Namespace,
@@ -124,10 +146,11 @@ func (r RelationRepository) Check(ctx context.Context, rel relation.Relation) (b
124146
grpczap.Extract(ctx).Info("CheckPermission", zap.String("trace", string(str)))
125147
}
126148

149+
r.lastToken.Store(response.GetCheckedAt())
127150
return response.GetPermissionship() == authzedpb.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, nil
128151
}
129152

130-
func (r RelationRepository) Delete(ctx context.Context, rel relation.Relation) error {
153+
func (r *RelationRepository) Delete(ctx context.Context, rel relation.Relation) error {
131154
if rel.Object.Namespace == "" {
132155
return errors.New("object namespace is required to delete a relation")
133156
}
@@ -160,15 +183,16 @@ func (r RelationRepository) Delete(ctx context.Context, rel relation.Relation) e
160183
}
161184
defer nr.End()
162185
}
163-
_, err := r.spiceDB.client.DeleteRelationships(ctx, request)
186+
resp, err := r.spiceDB.client.DeleteRelationships(ctx, request)
164187
if err != nil {
165188
return err
166189
}
167190

191+
r.lastToken.Store(resp.GetDeletedAt())
168192
return nil
169193
}
170194

171-
func (r RelationRepository) LookupSubjects(ctx context.Context, rel relation.Relation) ([]string, error) {
195+
func (r *RelationRepository) LookupSubjects(ctx context.Context, rel relation.Relation) ([]string, error) {
172196
resp, err := r.spiceDB.client.LookupSubjects(ctx, &authzedpb.LookupSubjectsRequest{
173197
Consistency: r.getConsistency(),
174198
Resource: &authzedpb.ObjectReference{
@@ -195,7 +219,7 @@ func (r RelationRepository) LookupSubjects(ctx context.Context, rel relation.Rel
195219
return subjects, nil
196220
}
197221

198-
func (r RelationRepository) LookupResources(ctx context.Context, rel relation.Relation) ([]string, error) {
222+
func (r *RelationRepository) LookupResources(ctx context.Context, rel relation.Relation) ([]string, error) {
199223
resp, err := r.spiceDB.client.LookupResources(ctx, &authzedpb.LookupResourcesRequest{
200224
Consistency: r.getConsistency(),
201225
ResourceObjectType: rel.Object.Namespace,
@@ -226,7 +250,7 @@ func (r RelationRepository) LookupResources(ctx context.Context, rel relation.Re
226250
}
227251

228252
// ListRelations shouldn't be used in high TPS flows as consistency requirements are set high
229-
func (r RelationRepository) ListRelations(ctx context.Context, rel relation.Relation) ([]relation.Relation, error) {
253+
func (r *RelationRepository) ListRelations(ctx context.Context, rel relation.Relation) ([]relation.Relation, error) {
230254
resp, err := r.spiceDB.client.ReadRelationships(ctx, &authzedpb.ReadRelationshipsRequest{
231255
Consistency: r.getConsistency(),
232256
RelationshipFilter: &authzedpb.RelationshipFilter{
@@ -268,14 +292,7 @@ func (r RelationRepository) ListRelations(ctx context.Context, rel relation.Rela
268292
return rels, nil
269293
}
270294

271-
func (r RelationRepository) getConsistency() *authzedpb.Consistency {
272-
if !r.fullyConsistent {
273-
return nil
274-
}
275-
return &authzedpb.Consistency{Requirement: &authzedpb.Consistency_FullyConsistent{FullyConsistent: true}}
276-
}
277-
278-
func (r RelationRepository) BatchCheck(ctx context.Context, relations []relation.Relation) ([]relation.CheckPair, error) {
295+
func (r *RelationRepository) BatchCheck(ctx context.Context, relations []relation.Relation) ([]relation.CheckPair, error) {
279296
result := make([]relation.CheckPair, len(relations))
280297
items := make([]*authzedpb.BulkCheckPermissionRequestItem, 0, len(relations))
281298
for _, rel := range relations {
@@ -295,7 +312,7 @@ func (r RelationRepository) BatchCheck(ctx context.Context, relations []relation
295312
})
296313
}
297314
request := &authzedpb.BulkCheckPermissionRequest{
298-
Consistency: r.getConsistency(),
315+
Consistency: r.getConsistencyForCheck(),
299316
Items: items,
300317
}
301318

@@ -329,5 +346,33 @@ func (r RelationRepository) BatchCheck(ctx context.Context, relations []relation
329346
result[itemIdx].Status = item.GetItem().GetPermissionship() == authzedpb.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION
330347
}
331348
}
349+
350+
r.lastToken.Store(response.GetCheckedAt())
332351
return result, respErr
333352
}
353+
354+
func (r *RelationRepository) getConsistency() *authzedpb.Consistency {
355+
switch r.consistency {
356+
case ConsistencyLevelMinimizeLatency:
357+
return &authzedpb.Consistency{Requirement: &authzedpb.Consistency_MinimizeLatency{MinimizeLatency: true}}
358+
case ConsistencyLevelFull:
359+
return &authzedpb.Consistency{Requirement: &authzedpb.Consistency_FullyConsistent{FullyConsistent: true}}
360+
}
361+
362+
lastToken := r.lastToken.Load()
363+
if lastToken == nil {
364+
return &authzedpb.Consistency{Requirement: &authzedpb.Consistency_FullyConsistent{FullyConsistent: true}}
365+
}
366+
return &authzedpb.Consistency{
367+
Requirement: &authzedpb.Consistency_AtLeastAsFresh{
368+
AtLeastAsFresh: lastToken,
369+
},
370+
}
371+
}
372+
373+
func (r *RelationRepository) getConsistencyForCheck() *authzedpb.Consistency {
374+
if r.consistency == ConsistencyLevelMinimizeLatency {
375+
return &authzedpb.Consistency{Requirement: &authzedpb.Consistency_MinimizeLatency{MinimizeLatency: true}}
376+
}
377+
return &authzedpb.Consistency{Requirement: &authzedpb.Consistency_FullyConsistent{FullyConsistent: true}}
378+
}

test/e2e/testbench/testbench.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,10 @@ func Init(appConfig *config.Frontier) (*TestBench, error) {
9090
MaxQueryTimeout: time.Second * 30,
9191
}
9292
appConfig.SpiceDB = spicedb.Config{
93-
Host: "localhost",
94-
Port: spiceDBPort,
95-
PreSharedKey: preSharedKey,
96-
FullyConsistent: true,
93+
Host: "localhost",
94+
Port: spiceDBPort,
95+
PreSharedKey: preSharedKey,
96+
Consistency: spicedb.ConsistencyLevelBestEffort.String(),
9797
}
9898
appConfig.App.Admin.Users = []string{OrgAdminEmail}
9999
appConfig.App.Webhook.EncryptionKey = "kmm4ECoWU21K2ZoyTcYLd6w7DfhoUoap"

0 commit comments

Comments
 (0)