Skip to content

[server] SpiceDB: Add request-level caching based on AsyncLocalStorage+ZedTokens #18893

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

Merged
merged 2 commits into from
Oct 10, 2023

Conversation

geropl
Copy link
Member

@geropl geropl commented Oct 9, 2023

Description

This is a first attempt to address EXP-738, to come up with the least-invasive caching strategy for SpiceDB using ZedTokens:

  • we do store the ZedToken per API request, in the context (starting out blank)
  • we are using it for all subsequent requests + update it with every call

In parallel, we are looking to visualize the cache metrics SpiceDB emits. ✔️

This PR also comes with two tests. To make them work with the context-based caching, all relevant methods are executed with runWithContext to simulate API calls.

Follow-up cleanup PR: #18898

Summary generated by Copilot

🤖 Generated by Copilot at 6a92654

Refactor and cache the SpiceDB authorizer in the server component. Use ZedTokens to ensure consistency and avoid stale reads and writes. Simplify the Authorizer class methods. Add unit tests for the caching logic.

Related Issue(s)

Part of EXP-738

How to test

Documentation

Preview status

Gitpod was successfully deployed to your preview environment.

Build Options

Build
  • /werft with-werft
    Run the build with werft instead of GHA
  • leeway-no-cache
  • /werft no-test
    Run Leeway with --dont-test
Publish
  • /werft publish-to-npm
  • /werft publish-to-jb-marketplace
Installer
  • analytics=segment
  • with-dedicated-emulation
  • workspace-feature-flags
    Add desired feature flags to the end of the line above, space separated
Preview Environment / Integration Tests
  • /werft with-local-preview
    If enabled this will build install/preview
  • /werft with-preview
  • /werft with-large-vm
  • /werft with-gce-vm
    If enabled this will create the environment on GCE infra
  • with-integration-tests=all
    Valid options are all, workspace, webapp, ide, jetbrains, vscode, ssh. If enabled, with-preview and with-large-vm will be enabled.
  • with-monitoring

/hold

export function setZedTokenToContext(zedToken: StoredZedToken) {
const ctx = asyncLocalStorage.getStore();
if (ctx) {
ctx.zedToken = zedToken;
Copy link
Member

Choose a reason for hiding this comment

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

does it really work? I thought the only way to modify the storage is to use run?

Copy link
Member Author

Choose a reason for hiding this comment

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

My understanding was that this will work when called from code that runs inside runWithContext. As we run all API calls inside, I thought it should.
Might be wrong, though. 🙃

/cc @svenefftinge

Copy link
Member

Choose a reason for hiding this comment

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

just checking, I don't know whether you can mutate, or only read

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, it would work. Just not super cool to hijack the "log"-context here. Do we want to move the code that sets the asyncLocalStorage out to something more general and make use of that from logging?

Copy link
Member Author

Choose a reason for hiding this comment

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

Just not super cool to hijack the "log"-context here. Do we want to move the code that sets the asyncLocalStorage out to something more general and make use of that from logging?

Totally agree. Would it be ok to merge this now (so we get some data), and me working on this in parallel?

* Works as a caching decorator for SpiceDBAuthorizerImpl. Delegates the actual caching strategy to ZedTokenCache.
*/
@injectable()
export class CachingSpiceDBAuthorizer implements SpiceDBAuthorizer {
Copy link
Contributor

Choose a reason for hiding this comment

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

would we use the cache directly in the original implmentation?
Using a delegation pattern introduces an abstraction that we don't really need but adds additional complexity with the inversify bindings but also requires to expose the token bit on the API level.

Copy link
Member Author

Choose a reason for hiding this comment

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

As discussed in sync: I wanted to focus on getting results first, before fine-tuning the implementation.

That being said, I agree on the inversify bindings and exposing impl details in the interface. 💯 Not so much on the "decorator is complexity" but oh well 🙃

Here is a branch where addressed all the review comments from this PR, given that the underlying idea works out: #18898

}

async readRelationships(req: v1.ReadRelationshipsRequest): Promise<v1.ReadRelationshipsResponse[]> {
// pass through with given consistency/caching for now
Copy link
Contributor

Choose a reason for hiding this comment

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

would be good to also use the token here. Is that somehow different or more complicated than in other cases?

Copy link
Member Author

Choose a reason for hiding this comment

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

No. But as the only use of the API is for introspection, I thought we always want fullyConsistent. 👍

* To make this work we make the "assumption" that ZedTokens string (meant to be opaque) represent a timestamp which we can order. This is at least true for the MySQL datastore we are using.
*/
@injectable()
export class RequestLocalZedTokenCache implements ZedTokenCache {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could the functions in here all be sync?

Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need an interface for this?

Copy link
Member Author

@geropl geropl Oct 9, 2023

Choose a reason for hiding this comment

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


export interface StoredZedToken {
token: string;
timestamp: number;
Copy link
Contributor

Choose a reason for hiding this comment

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

why store both elements?

Copy link
Member Author

Choose a reason for hiding this comment

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

Performance considerations: I don't want to always decode/encode the cached token (in fact, I did not even implement encode).

export type ZedTokenCacheKV = [objectRef: v1.ObjectReference | undefined, token: string | undefined];
export const ZedTokenCache = Symbol("ZedTokenCache");
export interface ZedTokenCache {
get(objectRef: v1.ObjectReference): Promise<string | undefined>;
Copy link
Contributor

Choose a reason for hiding this comment

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

I understood we would ignore the resources entirely with this simple approach, but just globally store a single token that we use when it exists

Copy link
Member Author

Choose a reason for hiding this comment

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

}
}

async consistency(resourceRef: v1.ObjectReference | undefined): Promise<v1.Consistency> {
Copy link
Contributor

Choose a reason for hiding this comment

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

this functionality could be the responsibility of the spicedb-authorizer not the cache itself.
I believe the cache really only needs get and update. Both being sync and very simple, update would only update if the given token is higher than the current one.

Copy link
Member Author

Choose a reason for hiding this comment

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

export function setZedTokenToContext(zedToken: StoredZedToken) {
const ctx = asyncLocalStorage.getStore();
if (ctx) {
ctx.zedToken = zedToken;
Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, it would work. Just not super cool to hijack the "log"-context here. Do we want to move the code that sets the asyncLocalStorage out to something more general and make use of that from logging?

@geropl
Copy link
Member Author

geropl commented Oct 9, 2023

Server currently logs invalid revision requested. Will look into this next.

@geropl geropl marked this pull request as draft October 9, 2023 12:04
@geropl geropl force-pushed the gpl/738-zedtokens-3 branch from 6a92654 to 088cb73 Compare October 9, 2023 13:41
@roboquat roboquat added size/XXL and removed size/XL labels Oct 9, 2023
@geropl geropl force-pushed the gpl/738-zedtokens-3 branch from 88dc424 to e96f061 Compare October 9, 2023 15:04
@geropl geropl mentioned this pull request Oct 9, 2023
14 tasks
@geropl
Copy link
Member Author

geropl commented Oct 9, 2023

@svenefftinge The preview works now. It was this line I missed in the Go implementation... I ended up generating SpiceDB internal proto implementations for DecodedZedToken, which blows up the LOC count quite a bit. 🙈 We could also looking into finding a shorter solution, but this is correct and works for now.

@geropl geropl marked this pull request as ready for review October 9, 2023 15:38
Copy link
Contributor

@svenefftinge svenefftinge left a comment

Choose a reason for hiding this comment

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

Let's see how well this works

@geropl
Copy link
Member Author

geropl commented Oct 10, 2023

/unhold

@roboquat roboquat merged commit e28a756 into main Oct 10, 2023
@roboquat roboquat deleted the gpl/738-zedtokens-3 branch October 10, 2023 06:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants