github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/datastore/common/changes.go (about) 1 package common 2 3 import ( 4 "context" 5 "sort" 6 7 "golang.org/x/exp/maps" 8 9 log "github.com/authzed/spicedb/internal/logging" 10 "github.com/authzed/spicedb/pkg/datastore" 11 core "github.com/authzed/spicedb/pkg/proto/core/v1" 12 "github.com/authzed/spicedb/pkg/spiceerrors" 13 "github.com/authzed/spicedb/pkg/tuple" 14 ) 15 16 const ( 17 nsPrefix = "n$" 18 caveatPrefix = "c$" 19 ) 20 21 // Changes represents a set of datastore mutations that are kept self-consistent 22 // across one or more transaction revisions. 23 type Changes[R datastore.Revision, K comparable] struct { 24 records map[K]changeRecord[R] 25 keyFunc func(R) K 26 content datastore.WatchContent 27 } 28 29 type changeRecord[R datastore.Revision] struct { 30 rev R 31 tupleTouches map[string]*core.RelationTuple 32 tupleDeletes map[string]*core.RelationTuple 33 definitionsChanged map[string]datastore.SchemaDefinition 34 namespacesDeleted map[string]struct{} 35 caveatsDeleted map[string]struct{} 36 } 37 38 // NewChanges creates a new Changes object for change tracking and de-duplication. 39 func NewChanges[R datastore.Revision, K comparable](keyFunc func(R) K, content datastore.WatchContent) *Changes[R, K] { 40 return &Changes[R, K]{ 41 records: make(map[K]changeRecord[R], 0), 42 keyFunc: keyFunc, 43 content: content, 44 } 45 } 46 47 // IsEmpty returns if the change set is empty. 48 func (ch *Changes[R, K]) IsEmpty() bool { 49 return len(ch.records) == 0 50 } 51 52 // AddRelationshipChange adds a specific change to the complete list of tracked changes 53 func (ch *Changes[R, K]) AddRelationshipChange( 54 ctx context.Context, 55 rev R, 56 tpl *core.RelationTuple, 57 op core.RelationTupleUpdate_Operation, 58 ) error { 59 if ch.content&datastore.WatchRelationships != datastore.WatchRelationships { 60 return nil 61 } 62 63 record := ch.recordForRevision(rev) 64 tplKey := tuple.StringWithoutCaveat(tpl) 65 66 switch op { 67 case core.RelationTupleUpdate_TOUCH: 68 // If there was a delete for the same tuple at the same revision, drop it 69 delete(record.tupleDeletes, tplKey) 70 record.tupleTouches[tplKey] = tpl 71 72 case core.RelationTupleUpdate_DELETE: 73 _, alreadyTouched := record.tupleTouches[tplKey] 74 if !alreadyTouched { 75 record.tupleDeletes[tplKey] = tpl 76 } 77 default: 78 log.Ctx(ctx).Warn().Stringer("operation", op).Msg("unknown change operation") 79 return spiceerrors.MustBugf("unknown change operation") 80 } 81 return nil 82 } 83 84 func (ch *Changes[R, K]) recordForRevision(rev R) changeRecord[R] { 85 k := ch.keyFunc(rev) 86 revisionChanges, ok := ch.records[k] 87 if !ok { 88 revisionChanges = changeRecord[R]{ 89 rev, 90 make(map[string]*core.RelationTuple), 91 make(map[string]*core.RelationTuple), 92 make(map[string]datastore.SchemaDefinition), 93 make(map[string]struct{}), 94 make(map[string]struct{}), 95 } 96 ch.records[k] = revisionChanges 97 } 98 99 return revisionChanges 100 } 101 102 // AddDeletedNamespace adds a change indicating that the namespace with the name was deleted. 103 func (ch *Changes[R, K]) AddDeletedNamespace( 104 _ context.Context, 105 rev R, 106 namespaceName string, 107 ) { 108 if ch.content&datastore.WatchSchema != datastore.WatchSchema { 109 return 110 } 111 112 record := ch.recordForRevision(rev) 113 delete(record.definitionsChanged, nsPrefix+namespaceName) 114 115 record.namespacesDeleted[namespaceName] = struct{}{} 116 } 117 118 // AddDeletedCaveat adds a change indicating that the caveat with the name was deleted. 119 func (ch *Changes[R, K]) AddDeletedCaveat( 120 _ context.Context, 121 rev R, 122 caveatName string, 123 ) { 124 if ch.content&datastore.WatchSchema != datastore.WatchSchema { 125 return 126 } 127 128 record := ch.recordForRevision(rev) 129 delete(record.definitionsChanged, caveatPrefix+caveatName) 130 131 record.caveatsDeleted[caveatName] = struct{}{} 132 } 133 134 // AddChangedDefinition adds a change indicating that the schema definition (namespace or caveat) 135 // was changed to the definition given. 136 func (ch *Changes[R, K]) AddChangedDefinition( 137 ctx context.Context, 138 rev R, 139 def datastore.SchemaDefinition, 140 ) { 141 if ch.content&datastore.WatchSchema != datastore.WatchSchema { 142 return 143 } 144 145 record := ch.recordForRevision(rev) 146 147 switch t := def.(type) { 148 case *core.NamespaceDefinition: 149 delete(record.namespacesDeleted, t.Name) 150 record.definitionsChanged[nsPrefix+t.Name] = t 151 152 case *core.CaveatDefinition: 153 delete(record.caveatsDeleted, t.Name) 154 record.definitionsChanged[caveatPrefix+t.Name] = t 155 default: 156 log.Ctx(ctx).Fatal().Msg("unknown schema definition kind") 157 } 158 } 159 160 // AsRevisionChanges returns the list of changes processed so far as a datastore watch 161 // compatible, ordered, changelist. 162 func (ch *Changes[R, K]) AsRevisionChanges(lessThanFunc func(lhs, rhs K) bool) []datastore.RevisionChanges { 163 return ch.revisionChanges(lessThanFunc, *new(R), false) 164 } 165 166 // FilterAndRemoveRevisionChanges filters a list of changes processed up to the bound revision from the changes list, removing them 167 // and returning the filtered changes. 168 func (ch *Changes[R, K]) FilterAndRemoveRevisionChanges(lessThanFunc func(lhs, rhs K) bool, boundRev R) []datastore.RevisionChanges { 169 changes := ch.revisionChanges(lessThanFunc, boundRev, true) 170 ch.removeAllChangesBefore(boundRev) 171 return changes 172 } 173 174 func (ch *Changes[R, K]) revisionChanges(lessThanFunc func(lhs, rhs K) bool, boundRev R, withBound bool) []datastore.RevisionChanges { 175 if ch.IsEmpty() { 176 return nil 177 } 178 179 revisionsWithChanges := make([]K, 0, len(ch.records)) 180 for rk, cr := range ch.records { 181 if !withBound || boundRev.GreaterThan(cr.rev) { 182 revisionsWithChanges = append(revisionsWithChanges, rk) 183 } 184 } 185 186 if len(revisionsWithChanges) == 0 { 187 return nil 188 } 189 190 sort.Slice(revisionsWithChanges, func(i int, j int) bool { 191 return lessThanFunc(revisionsWithChanges[i], revisionsWithChanges[j]) 192 }) 193 194 changes := make([]datastore.RevisionChanges, len(revisionsWithChanges)) 195 for i, k := range revisionsWithChanges { 196 revisionChangeRecord := ch.records[k] 197 changes[i].Revision = revisionChangeRecord.rev 198 for _, tpl := range revisionChangeRecord.tupleTouches { 199 changes[i].RelationshipChanges = append(changes[i].RelationshipChanges, &core.RelationTupleUpdate{ 200 Operation: core.RelationTupleUpdate_TOUCH, 201 Tuple: tpl, 202 }) 203 } 204 for _, tpl := range revisionChangeRecord.tupleDeletes { 205 changes[i].RelationshipChanges = append(changes[i].RelationshipChanges, &core.RelationTupleUpdate{ 206 Operation: core.RelationTupleUpdate_DELETE, 207 Tuple: tpl, 208 }) 209 } 210 changes[i].ChangedDefinitions = maps.Values(revisionChangeRecord.definitionsChanged) 211 changes[i].DeletedNamespaces = maps.Keys(revisionChangeRecord.namespacesDeleted) 212 changes[i].DeletedCaveats = maps.Keys(revisionChangeRecord.caveatsDeleted) 213 } 214 215 return changes 216 } 217 218 func (ch *Changes[R, K]) removeAllChangesBefore(boundRev R) { 219 for rk, cr := range ch.records { 220 if boundRev.GreaterThan(cr.rev) { 221 delete(ch.records, rk) 222 } 223 } 224 }