Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Memdb datastore MVCC improvements #319

Merged
merged 5 commits into from Dec 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
101 changes: 101 additions & 0 deletions internal/datastore/common/changes.go
@@ -0,0 +1,101 @@
package common

import (
"context"
"sort"

v0 "github.com/authzed/authzed-go/proto/authzed/api/v0"
"github.com/rs/zerolog/log"
"github.com/shopspring/decimal"

"github.com/authzed/spicedb/internal/datastore"
"github.com/authzed/spicedb/pkg/tuple"
)

// Changes represents a set of tuple mutations that are kept self-consistent
// across one or more transaction revisions.
type Changes map[uint64]*changeRecord

type changeRecord struct {
tupleTouches map[string]*v0.RelationTuple
tupleDeletes map[string]*v0.RelationTuple
}

// NewChanges creates a new Changes object for change tracking and de-duplication.
func NewChanges() Changes {
return make(Changes)
}

// AddChange adds a specific change to the complete list of tracked changes
func (ch Changes) AddChange(
ctx context.Context,
revTxID uint64,
tpl *v0.RelationTuple,
op v0.RelationTupleUpdate_Operation,
) {
revisionChanges, ok := ch[revTxID]
if !ok {
revisionChanges = &changeRecord{
tupleTouches: make(map[string]*v0.RelationTuple),
tupleDeletes: make(map[string]*v0.RelationTuple),
}
ch[revTxID] = revisionChanges
}

tplKey := tuple.String(tpl)

switch op {
case v0.RelationTupleUpdate_TOUCH:
// If there was a delete for the same tuple at the same revision, drop it
delete(revisionChanges.tupleDeletes, tplKey)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the difference between if I have a delete and then touch, we're left with just the touch, but if I have touch and then delete, both will be emitted?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The won't, the test cases confirm this.


revisionChanges.tupleTouches[tplKey] = tpl

case v0.RelationTupleUpdate_DELETE:
_, alreadyTouched := revisionChanges.tupleTouches[tplKey]
if !alreadyTouched {
revisionChanges.tupleDeletes[tplKey] = tpl
}
default:
log.Ctx(ctx).Fatal().Stringer("operation", op).Msg("unknown change operation")
}
}

// AsRevisionChanges returns the list of changes processed so far as a datastore watch
// compatible, ordered, changelist.
func (ch Changes) AsRevisionChanges() (changes []*datastore.RevisionChanges) {
revisionsWithChanges := make([]uint64, 0, len(ch))
for revTxID := range ch {
revisionsWithChanges = append(revisionsWithChanges, revTxID)
}
sort.Slice(revisionsWithChanges, func(i int, j int) bool {
return revisionsWithChanges[i] < revisionsWithChanges[j]
})

for _, revTxID := range revisionsWithChanges {
revisionChange := &datastore.RevisionChanges{
Revision: revisionFromTransactionID(revTxID),
}

revisionChangeRecord := ch[revTxID]
for _, tpl := range revisionChangeRecord.tupleTouches {
revisionChange.Changes = append(revisionChange.Changes, &v0.RelationTupleUpdate{
Operation: v0.RelationTupleUpdate_TOUCH,
Tuple: tpl,
})
}
for _, tpl := range revisionChangeRecord.tupleDeletes {
revisionChange.Changes = append(revisionChange.Changes, &v0.RelationTupleUpdate{
Operation: v0.RelationTupleUpdate_DELETE,
Tuple: tpl,
})
}
changes = append(changes, revisionChange)
}

return
}

func revisionFromTransactionID(txID uint64) datastore.Revision {
return decimal.NewFromInt(int64(txID))
}