Skip to content

Commit 5d4315e

Browse files
committed
test(query): Add a new benchmark for wide arrows
1 parent 0d88fba commit 5d4315e

File tree

1 file changed

+148
-0
lines changed

1 file changed

+148
-0
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package benchmarks
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/authzed/spicedb/internal/datastore/common"
11+
"github.com/authzed/spicedb/internal/datastore/memdb"
12+
"github.com/authzed/spicedb/pkg/datalayer"
13+
"github.com/authzed/spicedb/pkg/datastore"
14+
"github.com/authzed/spicedb/pkg/query"
15+
"github.com/authzed/spicedb/pkg/schema/v2"
16+
"github.com/authzed/spicedb/pkg/schemadsl/compiler"
17+
"github.com/authzed/spicedb/pkg/schemadsl/input"
18+
"github.com/authzed/spicedb/pkg/tuple"
19+
)
20+
21+
// BenchmarkCheckWideArrow benchmarks permission checking through a wide arrow relationship.
22+
// This creates a scenario with:
23+
// - 10 files
24+
// - 100 groups (group0 through group99)
25+
// - 1000 users (user0 through user999)
26+
// - Each file belongs to 30 groups (deterministic assignment)
27+
// - Each group has ~20 users (deterministic assignment)
28+
//
29+
// The permission viewer = view + group->member creates a wide arrow traversal where
30+
// checking if a user has viewer permission on a file requires checking many group memberships.
31+
func BenchmarkCheckWideArrow(b *testing.B) {
32+
const (
33+
numFiles = 10
34+
numGroups = 97 // Prime number to avoid duplicate assignments with stepping
35+
numUsers = 997 // Prime number to avoid duplicate assignments with stepping
36+
groupsPerFile = 30
37+
usersPerGroup = 20
38+
)
39+
40+
// Create an in-memory datastore
41+
rawDS, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC)
42+
require.NoError(b, err)
43+
44+
ctx := context.Background()
45+
46+
schemaText := `
47+
definition user {}
48+
49+
definition group {
50+
relation member: user
51+
}
52+
53+
definition file {
54+
relation group: group
55+
relation view: user
56+
permission viewer = view + group->member
57+
}
58+
`
59+
60+
// Compile the schema
61+
compiled, err := compiler.Compile(compiler.InputSchema{
62+
Source: input.Source("benchmark"),
63+
SchemaString: schemaText,
64+
}, compiler.AllowUnprefixedObjectType())
65+
require.NoError(b, err)
66+
67+
// Write the schema
68+
_, err = rawDS.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error {
69+
return rwt.LegacyWriteNamespaces(ctx, compiled.ObjectDefinitions...)
70+
})
71+
require.NoError(b, err)
72+
73+
// Build relationships deterministically
74+
relationships := make([]tuple.Relationship, 0, numFiles*groupsPerFile+numGroups*usersPerGroup)
75+
76+
// Create file->group relationships
77+
// Each file belongs to 30 groups with different step sizes (deterministic assignment with modular wrapping)
78+
for fileID := 0; fileID < numFiles; fileID++ {
79+
// file0 steps by 1s (groups 0, 1, 2, ..., 29)
80+
// file1 steps by 2s (groups 0, 2, 4, ..., 58)
81+
// file2 steps by 3s (groups 0, 3, 6, ..., 87)
82+
// etc., with modular wrapping
83+
step := fileID + 1
84+
for i := 0; i < groupsPerFile; i++ {
85+
groupID := (i * step) % numGroups
86+
rel := fmt.Sprintf("file:file%d#group@group:group%d", fileID, groupID)
87+
relationships = append(relationships, tuple.MustParse(rel))
88+
}
89+
}
90+
91+
// Create group->user relationships
92+
// Each group has 20 users with different step sizes (deterministic assignment with modular wrapping)
93+
for groupID := 0; groupID < numGroups; groupID++ {
94+
// group0 steps by 1s (users 0, 1, 2, ..., 19)
95+
// group1 steps by 2s (users 0, 2, 4, ..., 38)
96+
// group2 steps by 3s (users 0, 3, 6, ..., 57)
97+
// etc., with modular wrapping
98+
step := groupID + 1
99+
for i := 0; i < usersPerGroup; i++ {
100+
userID := (i * step) % numUsers
101+
rel := fmt.Sprintf("group:group%d#member@user:user%d", groupID, userID)
102+
relationships = append(relationships, tuple.MustParse(rel))
103+
}
104+
}
105+
106+
// Write all relationships to the datastore
107+
revision, err := common.WriteRelationships(ctx, rawDS, tuple.UpdateOperationCreate, relationships...)
108+
require.NoError(b, err)
109+
110+
// Build schema for querying
111+
dsSchema, err := schema.BuildSchemaFromDefinitions(compiled.ObjectDefinitions, nil)
112+
require.NoError(b, err)
113+
114+
// Create the iterator tree for the viewer permission using BuildIteratorFromSchema
115+
viewerIterator, err := query.BuildIteratorFromSchema(dsSchema, "file", "viewer")
116+
require.NoError(b, err)
117+
118+
// Create query context
119+
queryCtx := query.NewLocalContext(ctx,
120+
query.WithReader(datalayer.NewDataLayer(rawDS).SnapshotReader(revision)),
121+
)
122+
123+
// The resource we're checking: file:file0
124+
resources := query.NewObjects("file", "file0")
125+
126+
// The subject we're checking: user:user15
127+
// This user should have access through multiple groups
128+
subject := query.NewObject("user", "user15").WithEllipses()
129+
130+
// Reset the timer - everything before this is setup
131+
b.ResetTimer()
132+
133+
// Run the benchmark
134+
for b.Loop() {
135+
// Check if user:user15 can view file:file0
136+
// This will traverse through many group memberships
137+
seq, err := queryCtx.Check(viewerIterator, resources, subject)
138+
require.NoError(b, err)
139+
140+
// Collect all results
141+
paths, err := query.CollectAll(seq)
142+
require.NoError(b, err)
143+
144+
// Verify we found at least one path
145+
// user15 should have access through multiple groups
146+
require.NotEmpty(b, paths)
147+
}
148+
}

0 commit comments

Comments
 (0)