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 (
1819type 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+
3353const 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+ }
0 commit comments