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

Cache canonicalization #485

Merged
merged 13 commits into from
Apr 14, 2022
Merged

Conversation

josephschorr
Copy link
Member

@josephschorr josephschorr commented Mar 17, 2022

Add support for aliasing and cache canonicalization on dispatch.

Aliasing means that any permission which refers directly to another permission or relation will be marked as an "alias" of that permission/relation, and dispatch will skip the unnecessary intermediate step(s).

Cache canonicalization generates a canonical key for each permission, where the key is shared amongst permissions on the same namespace that have the same expression. The key (if available) is then used for the cache, to ensure that permissions referencing the same expressions share the same cache.

Fixes AUTHZ-459

@github-actions github-actions bot added area/api v0 Affects the v0 API area/api v1 Affects the v1 API area/dependencies Affects dependencies area/dispatch Affects dispatching of requests area/tooling Affects the dev or user toolchain (e.g. tests, ci, build tools) labels Mar 17, 2022
@josephschorr josephschorr force-pushed the cache-canonicalization branch 3 times, most recently from 6a68f48 to 35014ab Compare March 17, 2022 22:35
@jzelinskie jzelinskie linked an issue Mar 17, 2022 that may be closed by this pull request
requestKey := dispatch.CheckRequestToKey(req)
if cd.nsm != nil && req.ObjectAndRelation.Namespace != req.Subject.Namespace {
Copy link
Contributor

Choose a reason for hiding this comment

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

instead of checking if the nsm is nil, can there just be variants of the dispatcher that can do canonicalization?

Copy link
Member Author

Choose a reason for hiding this comment

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

I suppose, although the only reason we have this is to remove the need for a mock namespace manager in the caching testing. Do you prefer I just mock out the nsm there?

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we not just use a real namespace manager in the tests?

Copy link
Member Author

Choose a reason for hiding this comment

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

We could, but then we also need a real datastore

Copy link
Contributor

@ecordell ecordell Mar 22, 2022

Choose a reason for hiding this comment

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

Maybe there's value to a "KeyProvider" abstraction, and the disptacher just takes a reference to a key provider.

When there's a datastore available, we use a key provider backed w/ a nsm so that we get the pre-calculated cache keys, but for testing we can inject a simple static keyer, etc

This would also make it easier to get the canonical keys outside of the dispatcher, though I don't have any particular use case for that in mind.

Copy link
Member Author

Choose a reason for hiding this comment

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

A good idea

internal/dispatch/graph/graph.go Show resolved Hide resolved
internal/namespace/aliasing_test.go Show resolved Hide resolved
internal/namespace/aliasing.go Outdated Show resolved Hide resolved
internal/namespace/aliasing.go Outdated Show resolved Hide resolved
internal/namespace/canonicalization.go Outdated Show resolved Hide resolved
requestKey := dispatch.CheckRequestToKey(req)
if cd.nsm != nil && req.ObjectAndRelation.Namespace != req.Subject.Namespace {
Copy link
Contributor

Choose a reason for hiding this comment

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

discussed in slack, but we should determine if this branch needs to happen for any checks to the same namespace, or if we can only worry about checks with relations to the same object (or ideally avoid it entirely, though I'm not sure that's possible)


// NOTE: canonical cache keys are only unique *within* a version of a namespace, so they used
// essentially as a new relation name in the cache key.
return fmt.Sprintf("check//canonical/%s:%s#%s@%s@%s", req.ObjectAndRelation.Namespace, req.ObjectAndRelation.ObjectId, possibleCanonicalKey, tuple.StringONR(req.Subject), req.Metadata.AtRevision)
Copy link
Contributor

Choose a reason for hiding this comment

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

this is arguably a separate issue entirely, but why not share cache with lookup/expand?

Copy link
Member Author

Choose a reason for hiding this comment

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

Because it would require a significant amount of reworking of the results from those caches (not to mention that lookup is not necessary going to have the full results set since it supports limits)

internal/dispatch/dispatch.go Outdated Show resolved Hide resolved

// CheckRequestToKeyWithPossibleCanonical converts a check request into a cache key based possibly
// on the canonical key, or the relation name if the canonical key is empty
func CheckRequestToKeyWithPossibleCanonical(req *v1.DispatchCheckRequest, possibleCanonicalKey string) string {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we unify this with CheckRequestToKey?

i.e. pass in the relation name as the canonical key if we don't have a real canonical key, and then compute the cache keys the same?

Copy link
Member Author

Choose a reason for hiding this comment

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

They can't have the same key as there is a non-zero possibility someone creates a relation with a name that matches the computed hash, which would result in the cache keys overlapping incorrectly. I explicitly have different prefixes to prevent that from occurring

Copy link
Contributor

Choose a reason for hiding this comment

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

We always compute the cache keys ourselves ahead of time, if that's a concern why not just detect it and either reject the schema or change the canonical key?

Copy link
Member Author

Choose a reason for hiding this comment

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

Two reasons:
(1) Its not backwards compatible and limiting the names of relations to prevent overlap is a user facing error for an internal implementation
(2) Why do so when we can simply just construct a different key to guarantee no overlap? The cache itself is still shared

Copy link
Contributor

Choose a reason for hiding this comment

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

Changing the canonical key if there's an existing relation with that hash (which I would be surprised if anyone, ever, hits) would also guarantee no overlap

we can pick a deterministic way to avoid conflicts: if hash(rudd) is the same as some user-provided name, calculate hash(hash(rudd)), etc

Copy link
Contributor

Choose a reason for hiding this comment

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

We're hashing the bdd output ourselves, and it's fnv which is much more likely to collide than a crypto hash fn

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, but it is isolated to the BDD input, vs the relation and BDD. I very much do not want to mix them. Why the push to have a single cache key?

Copy link
Contributor

@ecordell ecordell Mar 28, 2022

Choose a reason for hiding this comment

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

I'm just trying to push runtime decisions into compile-time (since we're making exactly this type of decision at compile time already)

Copy link
Member

Choose a reason for hiding this comment

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

I'm inclined to agree. We have perfect information at schema compile time. We can just straight up check if there are any collisions if that's a real concern.

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 "upside" of the design is to remove a conditional (?) and yet the downsides are:

  1. If we ever mess it up, the cache will return incorrect results
  2. If there is a collision, we either have to issue an error to the user (bad) or pick a new revision (which could completely cascade forward)

I really don't see any upside of real value and major downsides in additional complexity at annotation time OR user error... plus, we'll still need the conditional anyway because we won't necessarily have a cache key for older schemas anyway

@josephschorr
Copy link
Member Author

Updated

jakedt
jakedt previously requested changes Mar 22, 2022
// We begin by assigning a unique integer index to each relation and arrow found for all
// expressions in the namespace:
// definition somenamespace {
// relation first: ...
Copy link
Member

Choose a reason for hiding this comment

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

Indentation.

Copy link
Member Author

Choose a reason for hiding this comment

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

What's wrong?

Copy link
Member

Choose a reason for hiding this comment

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

There are extra spaces before relation and the arrow points to the middle of the word relation.

// ^ index 0
// relation second: ...
// ^ index 1
// permission someperm = second + (first - third->something)
Copy link
Member

Choose a reason for hiding this comment

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

third should probably be shown as a permission or a relation.

// }
//
// These indexes are then used with the rudd library to build the expression:
// someperm => `bdd.And(bdd.Ithvar(1), bdd.Or(bdd.Ithvar(0), bdd.NIthvar(2)))`
Copy link
Member

Choose a reason for hiding this comment

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

These ops seem wrong for the example. + shouldn't be And, right?

}, varMap)

case *core.UsersetRewrite_Exclusion:
return convertToBdd(relation, bdd, rw.Exclusion, bdd.Or, func(childIndex int, varIndex int) rudd.Node {
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't Exclusion be bdd.And, i.e. it's A and not B?

return CheckRequestToKey(req)
}

// NOTE: canonical cache keys are only unique *within* a version of a namespace, so they used
Copy link
Member

Choose a reason for hiding this comment

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

grammar

return nil, err
}

requestKey = dispatch.CheckRequestToKeyWithPossibleCanonical(req, relation.CanonicalCacheKey)
Copy link
Member

Choose a reason for hiding this comment

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

If there is no canonical cache key, this method calls requestKey to re-compute the key for the check request. Seems wasteful to do it twice...

// computePermissionAliases computes a map of aliases between the various permissions in a
// namespace. A permission is considered an alias if it *directly* refers to another permission
// or relation without any other form of expression.
func computePermissionAliases(typeSystem *ValidatedNamespaceTypeSystem) (map[string]string, error) {
Copy link
Member

Choose a reason for hiding this comment

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

Do we expect this to save us enough time to be worth it? If there were more than one alias of the same relation or permission, they would share a cache key and only 1/n calls would actually be invoked. Am I missing something that makes this worth adding?

Copy link
Member Author

Choose a reason for hiding this comment

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

In the case where there isn't any caching yet, and where there is a chain of aliasing (2-3 at least), it absolutely could save time because we'd be redispatching from node to node for each aliasing.

}

// Otherwise, add the permission to the working set.
workingSet[rel.Name] = aliasedPermOrRel
Copy link
Member

Choose a reason for hiding this comment

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

Can we call this unresolvedAliases or something? workingSet could mean anything.

@josephschorr josephschorr force-pushed the cache-canonicalization branch 3 times, most recently from b89560f to 9bb0db2 Compare March 23, 2022 09:31
@josephschorr
Copy link
Member Author

Rebased and updated for nil support

@josephschorr
Copy link
Member Author

@ecordell Updated as discussed

ecordell
ecordell previously approved these changes Apr 12, 2022
Copy link
Contributor

@ecordell ecordell left a comment

Choose a reason for hiding this comment

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

LGTM

As discussed offline: we'll want this in a release before issuing a migration that re-writes existing namespaces.

@josephschorr josephschorr force-pushed the cache-canonicalization branch 2 times, most recently from 56c142c to 59f53ea Compare April 14, 2022 16:26
@authzed authzed deleted a comment from github-actions bot Apr 14, 2022
//
// For example, for the namespace:
// definition somenamespace {
// relation first: ...
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: this line has different spacing

ecordell
ecordell previously approved these changes Apr 14, 2022
Copy link
Contributor

@ecordell ecordell left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Contributor

@ecordell ecordell left a comment

Choose a reason for hiding this comment

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

LGTM

@josephschorr josephschorr merged commit d9bd4b0 into authzed:main Apr 14, 2022
@github-actions github-actions bot locked and limited conversation to collaborators Apr 14, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area/api v0 Affects the v0 API area/api v1 Affects the v1 API area/dependencies Affects dependencies area/dispatch Affects dispatching of requests area/tooling Affects the dev or user toolchain (e.g. tests, ci, build tools)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Canonicalize cache keys for shared subproblems
3 participants