Skip to content

Conversation

@tstirrat15
Copy link
Contributor

@tstirrat15 tstirrat15 commented Jan 30, 2026

Description

In #2805 we refactored the caching proxies, and a part of that refactor made SchemaReader() return the underlying Reader instance rather than a reference to the current proxy. This meant that the proxy was effectively disabled for anything that interacted with schema reading/writing, because there wasn't a way to get a handle on the proxy that exercised the schema reading logic.

This remedies that by returning a wrapped version of the proxy that adds adapters for the new methods.

It also breaks out the LookupSchemaDefinitions's definition into two other functions that are caveat and typedef specific, which means that we can use the specific version at the callsite and prevent cache misses and overfetching.

Changes

Will annotate.

Testing

Review. Load-test this against the previous commit and see that the schema cache is actually populated and used.

@tstirrat15 tstirrat15 requested a review from a team as a code owner January 30, 2026 16:24
@github-actions github-actions bot added the area/datastore Affects the storage system label Jan 30, 2026
Copy link
Contributor Author

@tstirrat15 tstirrat15 left a comment

Choose a reason for hiding this comment

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

See comments

// that caching logic stays in place.
func (r *definitionCachingReader) SchemaReader() (datastore.SchemaReader, error) {
return r.Reader.SchemaReader()
return schemautil.NewLegacySchemaReaderAdapter(r), nil
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I took the expedient approach in this file under the understanding that this code will be completely discarded once new schema storage is in place.

Copy link
Contributor

Choose a reason for hiding this comment

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

this code will be completely discarded

this entire file?

Copy link
Member

Choose a reason for hiding this comment

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

Yes

p *watchingCachingProxy
}

var _ datastore.SchemaReader = (*watchingCachingReader)(nil)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was slightly more thorough in this file under the understanding that the new schema storage mechanism will reuse some of this logic.


// ListAllTypeDefinitions delegates to the underlying reader, as the cache doesn't currently
// support reading all definitions directly.
// TODO: implement this in the cache
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Question for reviewers: does this seem sane? Listing all will bypass the cache, but I don't have a sense of whether that's actually used in places where caching matters.


// LookupSchemaDefinitionsByNames looks up type and caveat definitions by name.
// TODO: implement this directly in the cache, rather than overfetching from the caches
func (w *watchingCachingReader) LookupSchemaDefinitionsByNames(ctx context.Context, names []string) (map[string]datastore.SchemaDefinition, error) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This function is slightly inefficient because it's always going to make twice as many requests as necessary, but the caches should hold a notFound after their first request so it should be amortized (?).

@codecov
Copy link

codecov bot commented Jan 30, 2026

Codecov Report

❌ Patch coverage is 70.00000% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.39%. Comparing base (c684353) to head (0aabd07).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
internal/datastore/schema/schema.go 70.00% 4 Missing and 2 partials ⚠️
internal/relationships/validation.go 66.67% 4 Missing and 2 partials ⚠️
internal/services/v1/experimental.go 60.00% 4 Missing and 2 partials ⚠️
internal/services/v1/permissions.go 60.00% 4 Missing and 2 partials ⚠️

❌ Your project status has failed because the head coverage (74.39%) is below the target coverage (75.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2868      +/-   ##
==========================================
- Coverage   74.43%   74.39%   -0.04%     
==========================================
  Files         484      484              
  Lines       57764    57792      +28     
==========================================
- Hits        42991    42987       -4     
- Misses      11754    11779      +25     
- Partials     3019     3026       +7     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@tstirrat15 tstirrat15 force-pushed the hook-up-cache-proxies-to-schemareader branch 3 times, most recently from 7396adf to 496d3f6 Compare January 31, 2026 03:07
@github-actions github-actions bot added the area/tooling Affects the dev or user toolchain (e.g. tests, ci, build tools) label Jan 31, 2026
@tstirrat15 tstirrat15 force-pushed the hook-up-cache-proxies-to-schemareader branch from 496d3f6 to 303e311 Compare February 2, 2026 13:33
Copy link
Contributor Author

@tstirrat15 tstirrat15 left a comment

Choose a reason for hiding this comment

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

See comments

}

type testerDef struct {
type oldTesterDef struct {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renaming here was mostly about ensuring that I didn't miss a test.


func TestRWTCaching(t *testing.T) {
for _, tester := range testers {
func TestSnapshotCaching(t *testing.T) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Most of the refactors in these two files look roughly like this - flattening the tables and refactoring to use the new methods.


oneReader := &proxy_test.MockReader{}
dsMock.On("SnapshotReader", one).Return(oneReader)
oneReader.On("LegacyReadNamespaceByName", nsA).Return(nil, old, nil).Once()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note that the mock still references the legacy method - this is because the wrapper is just invoking the legacy method.

dsMock.On("SnapshotReader", one).Return(oneReader)
oneReader.
On("LegacyReadNamespaceByName", nsA).
WaitUntil(time.After(50*time.Millisecond)).
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using synctest here to avoid needing to actually sleep for the given period of time.


// singleflightReader is used to test singleflight context cancellation
// behavior.
type singleflightReader struct {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renaming this to make its use a bit clearer.

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: don't name things based on how they are expected to be used, name them after what they do. in this case, it's a slow reader that reacts to context cancellation

}

// TODO
func TestMixedCaching(t *testing.T) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the test whose semantics differ the most from the test it's based on.

Most of it comes down to the LookupSchemaDefinitionsByNames function taking namespace or caveat names as args, which means that it needs to look up the names as both caveats and namespaces.

Because the caches don't hold negative results, you can't make the assertion that the lookup function doesn't go to the underlying reader, because the lookup function is always going to be looking in the wrong place for a name half of the time.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was able to remedy this by adding separate Lookup methods that return just caveats and namespaces.

}

func (r *watchingCachingReader) LegacyReadNamespaceByName(
func (w *watchingCachingReader) LegacyReadNamespaceByName(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This bugged me so I updated it.

require.NoError(t, err)
require.Equal(t, "somenamespace", nsRevDef.Definition.Name)

// NOTE: we don't read via lookup because we can't retain the semantic of the test
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This test's semantic also differs from the previous one, for the same reason as previously - we can't assert that the Lookup doesn't go to the underlying datastore because it's always going to miss the cache half the time.

@tstirrat15 tstirrat15 force-pushed the hook-up-cache-proxies-to-schemareader branch from 303e311 to f5101b9 Compare February 2, 2026 14:59
@tstirrat15 tstirrat15 changed the title fix: hook up proxies with schemareader fix: hook up schema caching proxies with schemareader Feb 2, 2026
go readCaveat()
go readCaveat()

// Let the timers elapse
Copy link
Contributor

Choose a reason for hiding this comment

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

what timer?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The timers on the singleflight reader mock

dsMock := &proxy_test.MockDatastore{}
ctx1, cancel1 := context.WithCancel(t.Context())
defer cancel1()
// Note that ctx2 will not be cancelled
Copy link
Contributor

Choose a reason for hiding this comment

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

?

t.Context returns a context that is canceled just before Cleanup-registered functions are called.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the context of the test :P I meant that the second context isn't imperatively cancelled during test execution

miparnisari
miparnisari previously approved these changes Feb 2, 2026
@tstirrat15 tstirrat15 force-pushed the hook-up-cache-proxies-to-schemareader branch from fd224db to 502cbd2 Compare February 2, 2026 22:52
@github-actions github-actions bot added the area/api v1 Affects the v1 API label Feb 2, 2026
Copy link
Contributor Author

@tstirrat15 tstirrat15 left a comment

Choose a reason for hiding this comment

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

See comments

return err
}
foundDefs, err := schemaReader.LookupSchemaDefinitionsByNames(ctx, caveatNames.AsSlice())
foundDefs, err := schemaReader.LookupCaveatDefinitionsByNames(ctx, caveatNames.AsSlice())
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using the specific lookup should prevent an NS lookup.

go readCaveat()
go readCaveat()

// Let the timers elapse
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The timers on the singleflight reader mock

dsMock := &proxy_test.MockDatastore{}
ctx1, cancel1 := context.WithCancel(t.Context())
defer cancel1()
// Note that ctx2 will not be cancelled
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the context of the test :P I meant that the second context isn't imperatively cancelled during test execution

}

// TODO
func TestMixedCaching(t *testing.T) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was able to remedy this by adding separate Lookup methods that return just caveats and namespaces.

require.Equal(t, "somenamespace", nsRevDef.Definition.Name)

// Repeat with lookup
nsDefMap, err := schemaReader.LookupTypeDefinitionsByNames(t.Context(), []string{"somenamespace"})
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same in this file - using the new methods means that the new test better reflects the old one.

return err
}
foundDefs, err := schemaReader.LookupSchemaDefinitionsByNames(ctx, nsNames.AsSlice())
foundDefs, err := schemaReader.LookupTypeDefinitionsByNames(ctx, nsNames.AsSlice())
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This only handles typedefs so we can use the more specific function.

var referencedNamespaceMap map[string]*schema.Definition
var referencedCaveatMap map[string]*core.CaveatDefinition

if !referencedNamespaceNames.IsEmpty() || !referencedCaveatNamesWithContext.IsEmpty() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd welcome another pair of eyes on this file.

The old logic combined the namespace and caveat names and then made a single Lookup call. The new one keeps them separate and handles them separately, which should reduce the work and also makes the code slightly easier to follow at the expense of some duplication.

Copy link
Contributor

Choose a reason for hiding this comment

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

This looks good to me

numWritten += streamWritten

// The stream has terminated because we're awaiting namespace and/or caveat information
if len(adapter.awaitingNamespaces) > 0 || len(adapter.awaitingCaveats) > 0 {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the exact same refactor.

numWritten += streamWritten

// The stream has terminated because we're awaiting namespace and/or caveat information
if len(adapter.awaitingNamespaces) > 0 || len(adapter.awaitingCaveats) > 0 {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

And this too.

LookupSchemaDefinitionsByNames(ctx context.Context, names []string) (map[string]SchemaDefinition, error)

// LookupTypeDefinitionsByNames looks up type definitions by name.
LookupTypeDefinitionsByNames(ctx context.Context, names []string) (map[string]SchemaDefinition, error)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These are the new methods added.

josephschorr
josephschorr previously approved these changes Feb 2, 2026
Copy link
Member

@josephschorr josephschorr left a comment

Choose a reason for hiding this comment

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

LGTM

@tstirrat15 tstirrat15 force-pushed the hook-up-cache-proxies-to-schemareader branch from 502cbd2 to 8f3e04b Compare February 2, 2026 23:13
@@ -42,6 +42,12 @@ type SchemaReader interface {

// LookupSchemaDefinitionsByNames looks up type and caveat definitions by name.
LookupSchemaDefinitionsByNames(ctx context.Context, names []string) (map[string]SchemaDefinition, error)
Copy link
Contributor

@miparnisari miparnisari Feb 3, 2026

Choose a reason for hiding this comment

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

LookupSchemaDefinitionsByNames isn't used anymore (well except in tests). Can we mark it as deprecated with pointers to the new ones?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We'll use it when we move to the new schema storage mechanism, because it will make sense when the cost is the same to go look at the entire schema.

}

referencedNamespaceMap = make(map[string]*schema.Definition, len(foundNamespaceDefs))
ts := schema.NewTypeSystem(schema.ResolverForDatastoreReader(reader))
Copy link
Contributor

@miparnisari miparnisari Feb 3, 2026

Choose a reason for hiding this comment

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

this is funny.. if you remove ts and pass schema.NewDefinition(nil,...) the unit tests still pass 💀

not saying you change that but i think the schema.Definition struct needs a review

Co-authored-by: Maria Ines Parnisari <maria.ines.parnisari@authzed.com>
@tstirrat15 tstirrat15 force-pushed the hook-up-cache-proxies-to-schemareader branch from ace7341 to 0aabd07 Compare February 3, 2026 15:46
@tstirrat15 tstirrat15 merged commit 10cc7f7 into main Feb 3, 2026
74 of 78 checks passed
@tstirrat15 tstirrat15 deleted the hook-up-cache-proxies-to-schemareader branch February 3, 2026 16:10
@github-actions github-actions bot locked and limited conversation to collaborators Feb 3, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area/api v1 Affects the v1 API area/datastore Affects the storage system 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.

4 participants