Skip to content

Commit ea6c5f2

Browse files
authored
chore(services): store observed counts in permissionsServer, add LR/LS flags (#2929)
1 parent 0298415 commit ea6c5f2

34 files changed

+1271
-589
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1111
- Updated CI so that Postgres tests run against v18 which is GA and not against v13 which is EOL (https://github.com/authzed/spicedb/pull/2926)
1212
- Added tracing to request validation (https://github.com/authzed/spicedb/pull/2950)
1313
- Query Planner optimization: in Check requests, prune branches that cannot lead to the subject type specified (https://github.com/authzed/spicedb/pull/2968)
14+
- Added `lr` and `ls` to `--experimental-query-plan` for those endpoints, as well as in-memory statistics for optimizing the plans (https://github.com/authzed/spicedb/pull/2929)
1415

1516
### Fixed
1617
- Regression introduced in 1.49.2: missing spans in ReadSchema calls (https://github.com/authzed/spicedb/pull/2947)

docs/spicedb.md

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/services/integrationtesting/consistency_test.go

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,180 @@ func TestConsistency(t *testing.T) {
234234
})
235235
}
236236

237+
// TestQueryPlanConsistencyGRPC verifies that the experimental query plan engine
238+
// produces consistent results for LookupResources and LookupSubjects when those
239+
// operations are routed through the permissionServer gRPC handlers
240+
// (lookupResourcesWithQueryPlan / lookupSubjectsWithQueryPlan).
241+
//
242+
// It iterates the same validation files as TestConsistency, spins up a server
243+
// with both "lr" and "ls" query-plan flags enabled, and then verifies that the
244+
// set of returned resource/subject IDs matches the expected accessibility set.
245+
//
246+
// Note: the query plan handlers do not yet support cursor pagination or wildcard
247+
// exclusions, so this test intentionally uses unpaginated single-shot calls and
248+
// only checks set membership rather than running the full validateLookupResources
249+
// / validateLookupSubjects suite.
250+
func TestQueryPlanConsistencyGRPC(t *testing.T) { // nolint:tparallel
251+
t.Parallel()
252+
253+
consistencyTestFiles, err := consistencytestutil.ListTestConfigs()
254+
require.NoError(t, err)
255+
256+
for _, filePath := range consistencyTestFiles {
257+
t.Run(path.Base(filePath), func(t *testing.T) {
258+
t.Parallel()
259+
runQueryPlanConsistencyGRPCForFile(t, filePath)
260+
})
261+
}
262+
}
263+
264+
// runQueryPlanConsistencyGRPCForFile spins up a full gRPC server with both the
265+
// "lr" and "ls" experimental query plan flags enabled, then validates that
266+
// LookupResources and LookupSubjects return the correct set of IDs for every
267+
// resource-type × subject combination in the validation file.
268+
func runQueryPlanConsistencyGRPCForFile(t *testing.T, filePath string) {
269+
options := []server.ConfigOption{
270+
server.WithDispatchChunkSize(10),
271+
server.WithExperimentalQueryPlan("lr"),
272+
server.WithExperimentalQueryPlan("ls"),
273+
}
274+
275+
cad := consistencytestutil.LoadDataAndCreateClusterForTesting(t, filePath, testTimedelta, options...)
276+
277+
headRevision, err := cad.DataStore.HeadRevision(cad.Ctx)
278+
require.NoError(t, err)
279+
280+
accessibilitySet := consistencytestutil.BuildAccessibilitySet(t, cad.Ctx, cad.Populated, cad.DataStore)
281+
282+
testers := consistencytestutil.ServiceTesters(cad.Conn)
283+
for _, tester := range testers {
284+
t.Run(tester.Name(), func(t *testing.T) {
285+
validateQueryPlanLookupResources(t, cad, tester, headRevision, accessibilitySet)
286+
validateQueryPlanLookupSubjects(t, cad, tester, headRevision, accessibilitySet)
287+
})
288+
}
289+
}
290+
291+
// validateQueryPlanLookupResources checks that for each resource-type × subject
292+
// pair the query plan handler returns exactly the same resource IDs as the
293+
// accessibility set expects. Pagination is not tested here because the query
294+
// plan handlers do not yet implement cursor-based pagination.
295+
func validateQueryPlanLookupResources(
296+
t *testing.T,
297+
cad consistencytestutil.ConsistencyClusterAndData,
298+
tester consistencytestutil.ServiceTester,
299+
revision datastore.Revision,
300+
accessibilitySet *consistencytestutil.AccessibilitySet,
301+
) {
302+
testForEachResourceTypeInPopulated(t, cad.Populated, "qp_lookup_resources",
303+
func(t *testing.T, resourceRelation tuple.RelationReference) {
304+
for _, subject := range accessibilitySet.AllSubjectsNoWildcards() {
305+
t.Run(tuple.StringONR(subject), func(t *testing.T) {
306+
accessibleResources := accessibilitySet.LookupAccessibleResources(resourceRelation, subject)
307+
308+
// Single unpaginated call (limit=0 means no limit).
309+
foundResources, _, err := tester.LookupResources(t.Context(), resourceRelation, subject, revision, nil, 0, nil)
310+
require.NoError(t, err)
311+
312+
resolvedIDs := make([]string, 0, len(foundResources))
313+
for _, r := range foundResources {
314+
resolvedIDs = append(resolvedIDs, r.ResourceObjectId)
315+
}
316+
317+
require.ElementsMatch(t,
318+
slices.Collect(maps.Keys(accessibleResources)),
319+
resolvedIDs,
320+
"query plan LookupResources mismatch for %s#%s / subject %s",
321+
resourceRelation.ObjectType, resourceRelation.Relation, tuple.StringONR(subject),
322+
)
323+
})
324+
}
325+
})
326+
}
327+
328+
// validateQueryPlanLookupSubjects checks that for each resource × subject-type
329+
// pair the query plan handler returns at least the subjects the accessibility
330+
// set defines as directly accessible. Wildcard exclusion checking is omitted
331+
// because the query plan handler does not yet populate ExcludedSubjects.
332+
func validateQueryPlanLookupSubjects(
333+
t *testing.T,
334+
cad consistencytestutil.ConsistencyClusterAndData,
335+
tester consistencytestutil.ServiceTester,
336+
revision datastore.Revision,
337+
accessibilitySet *consistencytestutil.AccessibilitySet,
338+
) {
339+
testForEachResourceInPopulated(t, cad.Populated, accessibilitySet, "qp_lookup_subjects",
340+
func(t *testing.T, resource tuple.ObjectAndRelation) {
341+
for _, subjectType := range accessibilitySet.SubjectTypes() {
342+
t.Run(fmt.Sprintf("%s#%s", subjectType.ObjectType, subjectType.Relation),
343+
func(t *testing.T) {
344+
resolvedSubjects, err := tester.LookupSubjects(t.Context(), resource, subjectType, revision, nil)
345+
require.NoError(t, err)
346+
347+
// The accessibility set only covers directly-accessible defined subjects;
348+
// it does not include inferred subjects or wildcards, so we check subset.
349+
expectedDefinedSubjects := accessibilitySet.DirectlyAccessibleDefinedSubjectsOfType(resource, subjectType)
350+
requireSubsetOf(t,
351+
slices.Collect(maps.Keys(resolvedSubjects)),
352+
slices.Collect(maps.Keys(expectedDefinedSubjects)),
353+
)
354+
})
355+
}
356+
})
357+
}
358+
359+
// testForEachResourceTypeInPopulated runs a subtest for every relation on every
360+
// namespace defined in the populated validation file.
361+
func testForEachResourceTypeInPopulated(
362+
t *testing.T,
363+
populated *validationfile.PopulatedValidationFile,
364+
prefix string,
365+
handler func(t *testing.T, resourceRelation tuple.RelationReference),
366+
) {
367+
t.Helper()
368+
for _, resourceType := range populated.NamespaceDefinitions {
369+
for _, relation := range resourceType.Relation {
370+
t.Run(fmt.Sprintf("%s_%s_%s", prefix, resourceType.Name, relation.Name),
371+
func(t *testing.T) {
372+
handler(t, tuple.RelationReference{
373+
ObjectType: resourceType.Name,
374+
Relation: relation.Name,
375+
})
376+
})
377+
}
378+
}
379+
}
380+
381+
// testForEachResourceInPopulated runs a subtest for every resource instance
382+
// present in the accessibility set, filtered by namespace definitions.
383+
func testForEachResourceInPopulated(
384+
t *testing.T,
385+
populated *validationfile.PopulatedValidationFile,
386+
accessibilitySet *consistencytestutil.AccessibilitySet,
387+
prefix string,
388+
handler func(t *testing.T, resource tuple.ObjectAndRelation),
389+
) {
390+
t.Helper()
391+
for _, resourceType := range populated.NamespaceDefinitions {
392+
resources, ok := accessibilitySet.ResourcesByNamespace.Get(resourceType.Name)
393+
if !ok {
394+
continue
395+
}
396+
for _, relation := range resourceType.Relation {
397+
for _, resource := range resources {
398+
t.Run(fmt.Sprintf("%s_%s_%s_%s", prefix, resourceType.Name, resource.ObjectID, relation.Name),
399+
func(t *testing.T) {
400+
handler(t, tuple.ObjectAndRelation{
401+
ObjectType: resourceType.Name,
402+
ObjectID: resource.ObjectID,
403+
Relation: relation.Name,
404+
})
405+
})
406+
}
407+
}
408+
}
409+
}
410+
237411
func runConsistencyTestSuiteForFile(t *testing.T, filePath string, useCachingDispatcher bool, chunkSize uint16) {
238412
options := []server.ConfigOption{
239413
server.WithDispatchChunkSize(chunkSize),

internal/services/integrationtesting/query_plan_consistency_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,20 +78,23 @@ func runQueryPlanConsistencyForFile(t *testing.T, filePath string) {
7878

7979
t.Run("lookup_resources", func(t *testing.T) {
8080
if os.Getenv("TEST_QUERY_PLAN_RESOURCES") == "" {
81-
t.Skip("Skipping IterResources tests: set TEST_QUERY_PLAN_RESOURCES=true to enable")
81+
t.Skip("Skipping IterResources tests due to deprectation: Set TEST_QUERY_PLAN_RESOURCES=true to enable")
8282
}
8383
runQueryPlanLookupResources(t, handle)
8484
})
8585

8686
t.Run("lookup_subjects", func(t *testing.T) {
8787
if os.Getenv("TEST_QUERY_PLAN_SUBJECTS") == "" {
88-
t.Skip("Skipping IterSubjects tests: set TEST_QUERY_PLAN_SUBJECTS=true to enable")
88+
t.Skip("Skipping IterSubjects tests due to deprectation: Set TEST_QUERY_PLAN_SUBJECTS=true to enable")
8989
}
9090
runQueryPlanLookupSubjects(t, handle)
9191
})
9292
}
9393

9494
func runQueryPlanAssertions(t *testing.T, handle *queryPlanConsistencyHandle) {
95+
if os.Getenv("TEST_QUERY_PLAN_CHECK") == "" {
96+
t.Skip("Skipping Check tests due to deprecation. Set TEST_QUERY_PLAN_CHECK=true to enable")
97+
}
9598
t.Run("assertions", func(t *testing.T) {
9699
for _, parsedFile := range handle.populated.ParsedFiles {
97100
for _, entry := range []struct {

internal/services/v1/permissions.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func (ps *permissionServer) CheckPermission(ctx context.Context, req *v1.CheckPe
7070

7171
telemetry.LogicalChecks.Inc()
7272

73-
if ps.config.ExperimentalQueryPlan {
73+
if ps.config.ExperimentalQueryPlan.Check {
7474
return ps.checkPermissionWithQueryPlan(ctx, req)
7575
}
7676

@@ -475,6 +475,10 @@ func (ps *permissionServer) LookupResources(req *v1.LookupResourcesRequest, resp
475475
}
476476
}
477477

478+
if ps.config.ExperimentalQueryPlan.LookupResources {
479+
return ps.lookupResourcesWithQueryPlan(req, resp)
480+
}
481+
478482
if ps.config.EnableExperimentalLookupResources3 {
479483
return ps.lookupResources3(req, resp)
480484
}
@@ -792,6 +796,10 @@ func (ps *permissionServer) LookupSubjects(req *v1.LookupSubjectsRequest, resp v
792796

793797
ctx := resp.Context()
794798

799+
if ps.config.ExperimentalQueryPlan.LookupSubjects {
800+
return ps.lookupSubjectsWithQueryPlan(req, resp)
801+
}
802+
795803
if req.OptionalConcreteLimit != 0 {
796804
return ps.rewriteError(ctx, status.Errorf(codes.Unimplemented, "concrete limit is not yet supported"))
797805
}

0 commit comments

Comments
 (0)