-
Notifications
You must be signed in to change notification settings - Fork 278
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
Change from using protos to structs for relationships, ONRs and RRs #2081
Conversation
068372e
to
54a0bd6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
two minor comments while scrolling through
err := set.AddConcreteSubject(tuple.MustParseONR("document:foo#viewer")) | ||
require.NoError(t, err) | ||
|
||
err = set.AddConcreteSubject(tuple.ParseONR("document:bar#viewer")) | ||
err = set.AddConcreteSubject(tuple.MustParseONR("document:bar#viewer")) | ||
require.NoError(t, err) | ||
|
||
err = set.AddConcreteSubject(tuple.ParseONR("team:something#member")) | ||
err = set.AddConcreteSubject(tuple.MustParseONR("team:something#member")) | ||
require.NoError(t, err) | ||
|
||
err = set.AddConcreteSubject(tuple.ParseONR("team:other#member")) | ||
err = set.AddConcreteSubject(tuple.MustParseONR("team:other#member")) | ||
require.NoError(t, err) | ||
|
||
err = set.AddConcreteSubject(tuple.ParseONR("team:other#manager")) | ||
err = set.AddConcreteSubject(tuple.MustParseONR("team:other#manager")) | ||
require.NoError(t, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Easier to maintain over time if you write these with a loop:
for _, rel := range []string{
"document:foo#viewer",
"document:bar#viewer",
"team:something#member",
"team:other#member",
"team:other#manager",
} {
require.NoError(t, set.AddConcreteSubject(tuple.MustParseONR(rel)))
}
internal/datastore/common/tuple.go
Outdated
func MustIteratorBeClosed(iter *sliceRelationshipIterator) { | ||
if !iter.closed { | ||
panic("Tuple iterator garbage collected before Close() was called") | ||
func NewSliceRelationshipIterator(rels []tuple.Relationship, order options.SortOrder) datastore.RelationshipIterator { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if a generic Relationship Iterator constructor with ordering should be in the tuple package (probably also needs a rename) instead of datastore
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're only really using the slice iterator now in testing
ae2f1a3
to
1ec152f
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM - no blockers. I've got a bunch of questions but they're mostly understanding questions.
@@ -1461,7 +1411,9 @@ func StrictReadModeTest(t *testing.T, ds datastore.Datastore) { | |||
OptionalResourceType: "resource", | |||
}) | |||
require.NoError(err) | |||
it.Close() | |||
|
|||
_, err = datastore.IteratorToSlice(it) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this basically asserting that the thing is an iterator?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its ensuring it can read everything
for range tempIterator { | ||
break | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the advantage of this idiom?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It closes the iterator
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how is the iterator getting closed this way? It would be doing 1 iteration, unless there is some magic happening here? If that's the case, can you document it (e.g. pointer to go docs describing the behavior?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The loop invokes the iterator and break
causes it to load a single item. Once the loop has completed, Go terminates the function being invoked, which via its defer
, closes the underlying iterator
@@ -12,13 +12,13 @@ import ( | |||
// NOTE: This is designed solely for the developer API and testing and should *not* be used in any | |||
// performance sensitive code. | |||
type TrackingSubjectSet struct { | |||
setByType map[string]datasets.BaseSubjectSet[FoundSubject] | |||
setByType map[tuple.RelationReference]datasets.BaseSubjectSet[FoundSubject] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For my own curiosity - when you've got an object as the key in a map, is it using the pointer to the object as the hashmap key, or does the object itself have to be hashable? Are map
s even hashmaps?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if a reference (pointer), it uses the "pointer" itself. Otherwise, it compares structurally. Here, it is comparing structurally
spiceerrors.DebugAssert(func() bool { | ||
return tuple.OnrEqualOrWildcard(tpl.Subject, crc.parentReq.Subject) | ||
}, "somehow got invalid ONR for direct check matching") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did this check get moved or is it no longer necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed it because I didn't think it was necessary anymore
func Leaf(start *tuple.ObjectAndRelation, subjects ...*core.DirectSubject) *core.RelationTupleTreeNode { | ||
var startONR *core.ObjectAndRelation | ||
if start != nil { | ||
startONR = start.ToCoreONR() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious about this boundary - is this a task for future refactoring or is there a reason that this part of the codebase is still speaking in terms of the proto objects?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its generating protos for external consumption and our testing
func TestCanonicalBytes(t *testing.T) { | ||
foundBytes := make(map[string]string) | ||
|
||
for _, tc := range testCases { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where are these testCases defined?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for _, tc := range testCases { | |
// These test cases are defined in parsing_test.go | |
for _, tc := range testCases { |
Relation string | ||
} | ||
|
||
const onrStructSize = 48 /* size of the struct itself */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As in the number of bytes that an empty struct occupies?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep; i.e. its size without any other data to which it points
UpdateOperationTouch UpdateOperation = iota | ||
UpdateOperationCreate | ||
UpdateOperationDelete |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does it mean that there are two identifiers to the left of the assignment here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Golang allows you to elide the type in var ( ... )
and const (...)
expressions.
iota
is a special keyword that says "start at 0 and increment for each value below"
This reads as:
UpdateOperationTouch UpdateOperation = 0
UpdateOperationCreate UpdateOperation = 1
UpdateOperationDelete UpdateOperation = 2
) | ||
|
||
func TestONRStructSize(t *testing.T) { | ||
size := int(unsafe.Sizeof(ObjectAndRelation{})) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious - will this potentially depend on the Go runtime?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, hence the test
1ec152f
to
7e1cd74
Compare
Rebased |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Halfway through this, looking good so far
return s.AddSubject(subject, nil) | ||
} | ||
|
||
// AddSubject adds the specified subject to the set. | ||
func (s *SubjectByTypeSet) AddSubject(subject *core.ObjectAndRelation, caveat *core.ContextualizedCaveat) error { | ||
key := tuple.JoinRelRef(subject.Namespace, subject.Relation) | ||
func (s *SubjectByTypeSet) AddSubject(subject tuple.ObjectAndRelation, caveat *core.ContextualizedCaveat) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why were the contextualized caveat and integrity bits left as protos? Will it be addressed in a follow-up?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few reasons:
- They aren't used nearly as often, so the space savings are minimal
- Context can be quite large, so copying it around on the stack seems unwise
- Context is often
nil
, so there won't be any overhead in the common case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the context. It just feels odd because it won't be clear to the reader why the codebase is left in this odd mixture of structs and protos, and what pattern should be followed as the codebase evolves. From the arguments laid, context being quite large seems like something that would be equally problematic for the heap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Context being large is better to be on the heap: it ensures we're not copying it around; granted, context is not usually super large, but either way, its not used as much
ObjectId: userID, | ||
Relation: datastore.Ellipsis, | ||
func docViewer(documentID, userID string) tuple.Relationship { | ||
return tuple.Relationship{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would have been great if we had a fluent API to create struct values
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can add it, but it runs the risk of having "partial state" Relationships or needing a builder struct. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The risk is the same if you are creating a struct value directly. The struct builder will be in the stack and the overhead is minimal, and the impact on readability would be great - we do this a lot in the codebase. At the very least we could add the helper to use it in tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Followup?
@@ -75,7 +74,7 @@ type CreateRelationshipExistsError struct { | |||
error | |||
|
|||
// Relationship is the relationship that caused the error. May be nil, depending on the datastore. | |||
Relationship *core.RelationTuple | |||
Relationship *tuple.Relationship |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why was this guy left as a pointer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because it can be nil
; see comment above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. Just for the sake of the argument, you know you can achieve the same by comparing against a zero-value struct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer the explicit nil
to indicate its not present
// NOTE: The ordering of these columns can affect query performance, be aware when changing. | ||
columnsAndValues := map[options.SortOrder][]nameAndValue{ | ||
options.ByResource: { | ||
{ | ||
sqf.schema.colNamespace, cursor.ResourceAndRelation.Namespace, | ||
sqf.schema.colNamespace, cursor.Resource.ObjectType, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: since you took the time to make the refactor, this could have been less verbose. ObjectType
could have been Type
and ObjectID
could have been ID
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I explicitly put Object
in there: I wanted it to read very clearly that it was the Type (and ID) of the object
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's unnecesary. See how this reads: cursor.Resource.Type
It clearly states that it's the Type
of the Resource
. Most accesses need to go via Resource
or Subject
.
Anyway it was just a nit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still prefer it being explicit, but I get it
This is in prep for changing all internal references to relationships to use the structs, rather than proto-generated references, which are always on the heap and thus impact GC
92e53fb
to
7337008
Compare
) | ||
|
||
// ObjectAndRelation represents an object and its relation. | ||
type ObjectAndRelation struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
makes me wonder if we are running fieldalignment
linter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I doubt it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ran it locally and will address
This significantly reduces memory allocation in the datastore and dispatcher and should make other improvements easier down the road
Also changes to use a Go 1.23-style iterator for the relationship iterator rather than the custom one previously used