Skip to content

Commit 97bf11f

Browse files
authored
try to filter through objects isntead of fetching all objects (#374)
1 parent 8a3a5b3 commit 97bf11f

2 files changed

Lines changed: 304 additions & 16 deletions

File tree

fgax/benchmark_test.go

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
//go:build integration
2+
3+
package fgax_test
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"os"
9+
"testing"
10+
11+
"github.com/stretchr/testify/require"
12+
13+
"github.com/theopenlane/iam/fgax"
14+
"github.com/theopenlane/iam/fgax/testutils"
15+
)
16+
17+
const benchmarkModelDSL = `
18+
model
19+
schema 1.1
20+
21+
type user
22+
23+
type organization
24+
relations
25+
define member: [user]
26+
define admin: [user]
27+
define owner: [user]
28+
`
29+
30+
func setupIntegrationBenchmark(b *testing.B) (*fgax.Client, context.Context, func()) {
31+
b.Helper()
32+
33+
ctx := context.Background()
34+
35+
tmpModelFile := b.TempDir() + "/model.fga"
36+
err := os.WriteFile(tmpModelFile, []byte(benchmarkModelDSL), 0644)
37+
require.NoError(b, err)
38+
39+
container := testutils.NewFGATestcontainer(ctx,
40+
testutils.WithStoreName("benchmark-store"),
41+
testutils.WithReuse(true),
42+
testutils.WithContainerName("fga-benchmark"),
43+
testutils.WithModelFile(tmpModelFile),
44+
)
45+
46+
client, err := container.NewFgaClient(ctx)
47+
require.NoError(b, err)
48+
49+
return client, ctx, func() {
50+
container.TeardownFixture()
51+
}
52+
}
53+
54+
func makeTuples(b *testing.B, client *fgax.Client, ctx context.Context, totalTuples, targetTuples int, targetObject string) {
55+
b.Helper()
56+
57+
var writes []fgax.TupleKey
58+
59+
for i := 0; i < targetTuples; i++ {
60+
writes = append(writes, fgax.TupleKey{
61+
Subject: fgax.Entity{
62+
Kind: "user",
63+
Identifier: fmt.Sprintf("target-user-%d", i),
64+
},
65+
Relation: "member",
66+
Object: fgax.Entity{
67+
Kind: "organization",
68+
Identifier: targetObject,
69+
},
70+
})
71+
}
72+
73+
for i := 0; i < totalTuples-targetTuples; i++ {
74+
writes = append(writes, fgax.TupleKey{
75+
Subject: fgax.Entity{
76+
Kind: "user",
77+
Identifier: fmt.Sprintf("other-user-%d", i),
78+
},
79+
Relation: "member",
80+
Object: fgax.Entity{
81+
Kind: "organization",
82+
Identifier: fmt.Sprintf("other-org-%d", i),
83+
},
84+
})
85+
}
86+
87+
maxWrites := 10
88+
for i := 0; i < len(writes); i += maxWrites {
89+
end := i + maxWrites
90+
if end > len(writes) {
91+
end = len(writes)
92+
}
93+
94+
batch := writes[i:end]
95+
_, err := client.WriteTupleKeys(ctx, batch, []fgax.TupleKey{})
96+
require.NoError(b, err)
97+
}
98+
}
99+
100+
func cleanupTuples(b *testing.B, client *fgax.Client, ctx context.Context) {
101+
b.Helper()
102+
103+
allTuples, err := client.GetAllTuples(ctx)
104+
if err != nil {
105+
b.Logf("Warning: failed to get tuples for cleanup: %v", err)
106+
return
107+
}
108+
109+
if len(allTuples) == 0 {
110+
return
111+
}
112+
113+
var deletes []fgax.TupleKey
114+
for _, tuple := range allTuples {
115+
subject, _ := fgax.ParseEntity(tuple.Key.User)
116+
object, _ := fgax.ParseEntity(tuple.Key.Object)
117+
118+
deletes = append(deletes, fgax.TupleKey{
119+
Subject: subject,
120+
Relation: fgax.Relation(tuple.Key.Relation),
121+
Object: object,
122+
})
123+
}
124+
125+
maxWrites := 10
126+
for i := 0; i < len(deletes); i += maxWrites {
127+
end := i + maxWrites
128+
if end > len(deletes) {
129+
end = len(deletes)
130+
}
131+
132+
batch := deletes[i:end]
133+
_, err := client.WriteTupleKeys(ctx, []fgax.TupleKey{}, batch)
134+
if err != nil {
135+
b.Logf("Warning: failed to delete tuples in cleanup: %v", err)
136+
}
137+
}
138+
}
139+
140+
func BenchmarkDeleteAllObjectRelations_Small(b *testing.B) {
141+
client, ctx, cleanup := setupIntegrationBenchmark(b)
142+
defer cleanup()
143+
144+
targetObject := "target-org-small"
145+
totalTuples := 100
146+
targetTuples := 10
147+
148+
b.ResetTimer()
149+
for i := 0; i < b.N; i++ {
150+
b.StopTimer()
151+
makeTuples(b, client, ctx, totalTuples, targetTuples, targetObject)
152+
b.StartTimer()
153+
154+
err := client.DeleteAllObjectRelations(ctx, fmt.Sprintf("organization:%s", targetObject), []string{})
155+
require.NoError(b, err)
156+
}
157+
}
158+
159+
func BenchmarkDeleteAllObjectRelations_Medium(b *testing.B) {
160+
client, ctx, cleanup := setupIntegrationBenchmark(b)
161+
defer cleanup()
162+
163+
targetObject := "target-org-medium"
164+
totalTuples := 1000
165+
targetTuples := 10
166+
167+
b.ResetTimer()
168+
for i := 0; i < b.N; i++ {
169+
b.StopTimer()
170+
makeTuples(b, client, ctx, totalTuples, targetTuples, targetObject)
171+
b.StartTimer()
172+
173+
err := client.DeleteAllObjectRelations(ctx, fmt.Sprintf("organization:%s", targetObject), []string{})
174+
require.NoError(b, err)
175+
}
176+
}
177+
178+
func BenchmarkDeleteAllObjectRelations_Large(b *testing.B) {
179+
client, ctx, cleanup := setupIntegrationBenchmark(b)
180+
defer cleanup()
181+
182+
targetObject := "target-org-large"
183+
totalTuples := 5000
184+
targetTuples := 20
185+
186+
b.ResetTimer()
187+
for i := 0; i < b.N; i++ {
188+
b.StopTimer()
189+
makeTuples(b, client, ctx, totalTuples, targetTuples, targetObject)
190+
b.StartTimer()
191+
192+
err := client.DeleteAllObjectRelations(ctx, fmt.Sprintf("organization:%s", targetObject), []string{})
193+
require.NoError(b, err)
194+
}
195+
}
196+
197+
func BenchmarkGetTuplesForObject(b *testing.B) {
198+
client, ctx, cleanup := setupIntegrationBenchmark(b)
199+
defer cleanup()
200+
201+
testCases := []struct {
202+
name string
203+
totalTuples int
204+
targetTuples int
205+
}{
206+
{"Small", 100, 10},
207+
{"Medium", 1000, 10},
208+
{"Large", 5000, 20},
209+
}
210+
211+
for _, tc := range testCases {
212+
b.Run(tc.name, func(b *testing.B) {
213+
targetObject := fmt.Sprintf("target-org-%s", tc.name)
214+
215+
makeTuples(b, client, ctx, tc.totalTuples, tc.targetTuples, targetObject)
216+
defer cleanupTuples(b, client, ctx)
217+
218+
b.ResetTimer()
219+
for i := 0; i < b.N; i++ {
220+
_, err := client.GetTuplesForObject(ctx, fmt.Sprintf("organization:%s", targetObject))
221+
require.NoError(b, err)
222+
}
223+
})
224+
}
225+
}
226+
227+
func BenchmarkGetAllTuples(b *testing.B) {
228+
client, ctx, cleanup := setupIntegrationBenchmark(b)
229+
defer cleanup()
230+
231+
testCases := []struct {
232+
name string
233+
totalTuples int
234+
}{
235+
{"Small", 100},
236+
{"Medium", 1000},
237+
{"Large", 5000},
238+
}
239+
240+
for _, tc := range testCases {
241+
b.Run(tc.name, func(b *testing.B) {
242+
targetObject := fmt.Sprintf("any-org-%s", tc.name)
243+
244+
makeTuples(b, client, ctx, tc.totalTuples, 10, targetObject)
245+
defer cleanupTuples(b, client, ctx)
246+
247+
b.ResetTimer()
248+
for i := 0; i < b.N; i++ {
249+
_, err := client.GetAllTuples(ctx)
250+
require.NoError(b, err)
251+
}
252+
})
253+
}
254+
}

fgax/tuples.go

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -314,8 +314,8 @@ func (c *Client) deleteRelationshipTuple(ctx context.Context, tuples []openfga.T
314314
return resp, nil
315315
}
316316

317-
// getAllTuples gets all the relationship tuples in the openFGA store
318-
func (c *Client) getAllTuples(ctx context.Context, opts ...RequestOption) ([]openfga.Tuple, error) {
317+
// GetAllTuples gets all the relationship tuples in the openFGA store
318+
func (c *Client) GetAllTuples(ctx context.Context, opts ...RequestOption) ([]openfga.Tuple, error) {
319319
var tuples []openfga.Tuple
320320

321321
ropts := getReadOptions(opts...)
@@ -342,6 +342,42 @@ func (c *Client) getAllTuples(ctx context.Context, opts ...RequestOption) ([]ope
342342
return tuples, nil
343343
}
344344

345+
// GetTuplesForObject gets all the relationship tuples for a specific object in the openFGA store
346+
func (c *Client) GetTuplesForObject(ctx context.Context, object string) ([]openfga.Tuple, error) {
347+
var tuples []openfga.Tuple
348+
349+
readRequest := ofgaclient.ClientReadRequest{
350+
Object: openfga.PtrString(object),
351+
}
352+
353+
opts := ofgaclient.ClientReadOptions{
354+
PageSize: openfga.PtrInt32(defaultPageSize),
355+
Consistency: &defaultConsistency,
356+
}
357+
358+
notComplete := true
359+
360+
for notComplete {
361+
resp, err := c.Ofga.Read(ctx).Body(readRequest).Options(opts).Execute()
362+
if err != nil {
363+
log.Error().Err(err).Str("object", object).Msg("error getting relationship tuples for object")
364+
365+
return nil, err
366+
}
367+
368+
tuples = append(tuples, resp.GetTuples()...)
369+
370+
if resp.ContinuationToken == "" {
371+
notComplete = false
372+
continue
373+
}
374+
375+
opts.ContinuationToken = &resp.ContinuationToken
376+
}
377+
378+
return tuples, nil
379+
}
380+
345381
// DeleteAllObjectRelations deletes all the relationship tuples for a given object
346382
func (c *Client) DeleteAllObjectRelations(ctx context.Context, object string, excludeRelations []string, opts ...RequestOption) error {
347383
// validate object is not empty
@@ -354,28 +390,26 @@ func (c *Client) DeleteAllObjectRelations(ctx context.Context, object string, ex
354390
return newInvalidEntityError(object)
355391
}
356392

357-
tuples, err := c.getAllTuples(ctx, opts...)
393+
tuples, err := c.GetTuplesForObject(ctx, object)
358394
if err != nil {
359395
return err
360396
}
361397

362398
var tuplesToDelete []openfga.TupleKeyWithoutCondition
363399

364-
// check all the tuples for the object
400+
// Filter out relations that should be excluded
365401
for _, t := range tuples {
366-
if t.Key.Object == object {
367-
// if the relation is in the exclude list, skip it
368-
if slices.Contains(excludeRelations, t.Key.Relation) {
369-
continue
370-
}
371-
372-
k := openfga.TupleKeyWithoutCondition{
373-
User: t.Key.User,
374-
Relation: t.Key.Relation,
375-
Object: t.Key.Object,
376-
}
377-
tuplesToDelete = append(tuplesToDelete, k)
402+
// if the relation is in the exclude list, skip it
403+
if slices.Contains(excludeRelations, t.Key.Relation) {
404+
continue
405+
}
406+
407+
k := openfga.TupleKeyWithoutCondition{
408+
User: t.Key.User,
409+
Relation: t.Key.Relation,
410+
Object: t.Key.Object,
378411
}
412+
tuplesToDelete = append(tuplesToDelete, k)
379413
}
380414

381415
// delete the tuples in batches of 10, the max supported by the OpenFGA transactional write api

0 commit comments

Comments
 (0)