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  }