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

OpenFGA: Add request cache to the OpenFGA datastore #14557

Merged
merged 6 commits into from
Dec 11, 2024

Conversation

gabrielmougard
Copy link
Contributor

This PR decrease the latency of API calls that are going through the OpenFGA fine-grained authorizer. Thanks to a per-request cache mechanism, we avoid calling the database when the cache key is present.

This work has been benchmarked and has been used in the following PR: #14476

openfga_benchmark.pdf

The benchmarking script can be found at: https://paste.ubuntu.com/p/WCwsk6gSSK/

lxd/daemon.go Outdated Show resolved Hide resolved
Copy link
Contributor

@markylaing markylaing left a comment

Choose a reason for hiding this comment

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

General LGTM, just a couple of nits :)

@gabrielmougard
Copy link
Contributor Author

Thanks! Fixing this right away

@gabrielmougard gabrielmougard force-pushed the feat/openfga-request-cache branch 2 times, most recently from a4814ce to 9c61530 Compare December 2, 2024 08:50
@gabrielmougard gabrielmougard force-pushed the feat/openfga-request-cache branch from 9c61530 to 2f78972 Compare December 4, 2024 11:28
@gabrielmougard
Copy link
Contributor Author

@tomponline @markylaing updated (thanks Mark for helping documenting this)

Copy link
Contributor

@markylaing markylaing left a comment

Choose a reason for hiding this comment

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

LGTM, thanks!

@markylaing markylaing requested a review from tomponline December 4, 2024 11:35
@@ -248,10 +345,93 @@ func (o *openfgaStore) ReadUsersetTuples(ctx context.Context, store string, filt
return nil, fmt.Errorf("ReadUsersetTuples: Failed to parse entity URL %q: %w", entityURL, err)
}

var groupNames []string
// Get cache from context. If it is not present, we'll fall back to calling the database directly.
cache, err := request.GetCtxValue[*RequestCache](ctx, request.CtxOpenFGARequestCache)
Copy link
Member

Choose a reason for hiding this comment

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

why do we need this section, if the cache is not in the context we have a bug right? Why would we mask over that here?

Copy link
Contributor

Choose a reason for hiding this comment

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

I asked @gabrielmougard to do this in the original PR; which performed caching slightly differently, but the reasoning is the same.

The reason is that for long running requests we may not want to use the cache. For example, currently have an open issue to improve authorization for the events API. I anticipate that this will check can_view on a per-resource basis. If we use this cache for this kind of request, we'll have a snapshot of permissions at the time of the original request, but this may change while the web socket is still connected.

So this logic is in anticipation of setting the value of the request cache to nil in the request context, in the edge cases where we don't want to use a cache.

@@ -84,10 +84,6 @@ func (e *embeddedOpenFGA) load(ctx context.Context, identityCache *identity.Cach
server.WithDatastore(opts.openfgaDatastore),
// Use our logger.
server.WithLogger(openfgaLogger{l: e.logger}),
// Set the max concurrency to 1 for both read and check requests.
Copy link
Member

Choose a reason for hiding this comment

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

why did we need this before?

What changed?

The commit message would be improved with a why rather than just a what.

Copy link
Contributor

Choose a reason for hiding this comment

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

The OpenFGADatastore implementation when first implemented wasn't explicitly safe for concurrency. (i.e. I wasn't really thinking about it on the first pass, I was just trying to get something working 😂). So I added these configs initially for a bit of safety.

I think we could have removed the restrictions before this, because it is safe to have concurrent transactions (even though they will be serialised by DQLite).

With caching there is now a significant benefit to concurrency. Since pre-fetching the cache on the first encountered call means that subsequent calls with the same cache may not need to hit the DB.

Copy link
Member

Choose a reason for hiding this comment

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

With caching there is now a significant benefit to concurrency. Since pre-fetching the cache on the first encountered call means that subsequent calls with the same cache may not need to hit the DB.

So let me check I understand this.

There is a single long-lived openfga embedded instance, so enabling concurrency allows for that openfga server to check access for multiple API requests concurrently right?

But within a single request, the cache exists only for the duration of the request, but what is concurrently happening in the openga server then for a single request? Are you saying that we are pushing down the locking from openga server-level to request-level inside the request cache itself?

Copy link
Member

Choose a reason for hiding this comment

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

BTW the reason I'm asking is im trying to understand whether we actually need the complexity of 2 rw locks + an atomic variable or whether we can use a single mutex and check for whether the variables themselves are initialized.

Copy link
Member

Choose a reason for hiding this comment

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

@markylaing explained in a call that the openfga server will traverse the module and make concurrent calls to the DB driver (and thus the cache) even for a single API request.

Copy link
Member

@tomponline tomponline left a comment

Choose a reason for hiding this comment

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

I have some questions about the concurrency aspect.

  • Why did we restrict openfga concurrency before and why dont we need to now?
  • For the per-request cache in the context, in what scenarios will openfga concurrently read or write to the cache for a single request?

@tomponline tomponline merged commit 93764d9 into canonical:main Dec 11, 2024
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants