Skip to content

Commit

Permalink
feat: batch check relations (#1521)
Browse files Browse the repository at this point in the history
* batch check relations

* rename path

* shared parallelized function. batch size and parallelization configurable

* move check to engine

* fail individual requests

* move parallelization factor to be request parameter

* document and update configurable max batch size

* end to end tests

* unit tests

* cleanup

* run make format

* fix pipeline failures

* PR Feedback: move parallelization factor to config. Use err group

---------

Co-authored-by: Patrick Duffy <[email protected]>
  • Loading branch information
patrickduffy95 and Patrick Duffy authored Jul 18, 2024
1 parent 8578756 commit d670d50
Show file tree
Hide file tree
Showing 61 changed files with 3,466 additions and 241 deletions.
2 changes: 1 addition & 1 deletion cmd/status/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func TestStatusCmd(t *testing.T) {
defer cancel()

stdErr := cmdx.ExecExpectedErrCtx(ctx, t, newStatusCmd(), "--"+FlagEndpoint, string(serverType), "--"+ts.FlagRemote, ts.Addr[:len(ts.Addr)-1])
assert.Equal(t, "context deadline exceeded", stdErr)
assert.Contains(t, stdErr, "context deadline exceeded")
})

t.Run("case=noblock", func(t *testing.T) {
Expand Down
16 changes: 16 additions & 0 deletions embedx/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,22 @@
"description": "The global maximum width on all read operations. Note that this does not affect how deeply nested the tuples can be. This value can be decreased for a request by a value specified on the request, only if the request-specific value is greater than 1 and less than the global maximum width.",
"minimum": 1,
"maximum": 65535
},
"max_batch_check_size": {
"type": "integer",
"default": 10,
"title": "Maximum batch check size",
"description": "The maximum number of tuples that will be accepted by the batch check endpoint.",
"minimum": 1,
"maximum": 65535
},
"batch_check_max_parallelization": {
"type": "integer",
"default": 5,
"title": "Max concurrent checks during batch check",
"description": "The limit for the number of tuples that will be checked concurrently during a batch check.",
"minimum": 1,
"maximum": 65535
}
},
"additionalProperties": false
Expand Down
12 changes: 6 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ require (
go.opentelemetry.io/otel/trace v1.21.0
go.uber.org/goleak v1.3.0
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa
golang.org/x/oauth2 v0.14.0
golang.org/x/oauth2 v0.16.0
golang.org/x/sync v0.7.0
google.golang.org/grpc v1.59.0
google.golang.org/grpc v1.62.0
google.golang.org/protobuf v1.33.0
)

Expand Down Expand Up @@ -97,7 +97,7 @@ require (
github.com/goccy/go-yaml v1.11.2 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/glog v1.1.2 // indirect
github.com/golang/glog v1.2.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
Expand Down Expand Up @@ -192,9 +192,9 @@ require (
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
Expand Down
32 changes: 16 additions & 16 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k=
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa h1:jQCWAUqqlij9Pgj2i/PB79y4KOPYVyFYdROxgaCwdTQ=
github.com/cncf/xds/go v0.0.0-20231128003011-0fa0005c9caa/go.mod h1:x/1Gn8zydmfq8dk6e9PdstVsDgu9RuyIIJqAaF//0IM=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/cockroachdb/cockroach-go/v2 v2.3.5 h1:Khtm8K6fTTz/ZCWPzU9Ne3aOW9VyAnj4qIPCJgKtwK0=
Expand Down Expand Up @@ -121,8 +121,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA=
github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
github.com/evanphx/json-patch/v5 v5.7.0 h1:nJqP7uwL84RJInrohHfW0Fx3awjbm8qZeFv0nW9SYGc=
github.com/evanphx/json-patch/v5 v5.7.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
Expand Down Expand Up @@ -212,8 +212,8 @@ github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo=
github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ=
github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68=
github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
Expand Down Expand Up @@ -767,8 +767,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.14.0 h1:P0Vrf/2538nmC0H+pEQ3MNFRRnVR7RlqyVw+bvm26z0=
golang.org/x/oauth2 v0.14.0/go.mod h1:lAtNWgaWfL4cm7j2OV8TxGi9Qb7ECORx8DktCY74OwM=
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down Expand Up @@ -992,12 +992,12 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ=
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY=
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo=
google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA=
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ=
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro=
google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 h1:Lj5rbfG876hIAYFjqiJnPHfhXbv+nzTWfm04Fg/XSVU=
google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
Expand All @@ -1014,8 +1014,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk=
google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
google.golang.org/grpc/examples v0.0.0-20210304020650-930c79186c99 h1:qA8rMbz1wQ4DOFfM2ouD29DG9aHWBm6ZOy9BGxiUMmY=
google.golang.org/grpc/examples v0.0.0-20210304020650-930c79186c99/go.mod h1:Ly7ZA/ARzg8fnPU9TyZIxoz33sEUuWX7txiqs8lPTgE=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
Expand Down
3 changes: 0 additions & 3 deletions internal/check/checkgroup/membership_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 39 additions & 3 deletions internal/check/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ package check
import (
"context"

"github.com/pkg/errors"
"go.opentelemetry.io/otel/trace"

"github.com/ory/herodot"
"github.com/ory/x/otelx"
"github.com/pkg/errors"
"go.opentelemetry.io/otel/trace"
"golang.org/x/sync/errgroup"

"github.com/ory/keto/x/events"

Expand All @@ -34,6 +34,7 @@ type (
}
EngineDependencies interface {
relationtuple.ManagerProvider
relationtuple.MapperProvider
persistence.Provider
config.Provider
x.LoggerProvider
Expand Down Expand Up @@ -264,3 +265,38 @@ func (e *Engine) astRelationFor(ctx context.Context, r *relationTuple) (*ast.Rel
}
return namespace.ASTRelationFor(ctx, namespaceManager, r.Namespace, r.Relation)
}

// BatchCheck makes parallelized check requests for tuples. The check results are returned as slice, where the
// result index matches the tuple index of the incoming tuples array.
func (e *Engine) BatchCheck(ctx context.Context,
tuples []*ketoapi.RelationTuple,
maxDepth int) ([]checkgroup.Result, error) {

eg := &errgroup.Group{}
eg.SetLimit(e.d.Config(ctx).BatchCheckParallelizationLimit())

mapper := e.d.ReadOnlyMapper()
results := make([]checkgroup.Result, len(tuples))
for i, tuple := range tuples {
i := i
tuple := tuple
eg.Go(func() error {
internalTuple, err := mapper.FromTuple(ctx, tuple)
if err != nil {
results[i] = checkgroup.Result{
Membership: checkgroup.MembershipUnknown,
Err: err,
}
} else {
results[i] = e.CheckRelationTuple(ctx, internalTuple[0], maxDepth)
}
return nil
})
}

if err := eg.Wait(); err != nil {
return nil, err
}

return results, nil
}
105 changes: 105 additions & 0 deletions internal/check/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import (
"testing"

"github.com/gofrs/uuid"
"github.com/ory/herodot"
"github.com/ory/x/pointerx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/ory/keto/internal/check"
"github.com/ory/keto/internal/check/checkgroup"
"github.com/ory/keto/internal/driver"
"github.com/ory/keto/internal/driver/config"
"github.com/ory/keto/internal/namespace"
Expand All @@ -27,6 +30,7 @@ type configProvider = config.Provider
// deps are defined to capture engine dependencies in a single struct
type deps struct {
*relationtuple.ManagerWrapper // managerProvider
relationtuple.MapperProvider
persistence.Provider
configProvider
x.LoggerProvider
Expand All @@ -41,6 +45,7 @@ func newDepsProvider(t testing.TB, namespaces []*namespace.Namespace, pageOpts .

return &deps{
ManagerWrapper: mr,
MapperProvider: reg,
Provider: reg,
configProvider: reg,
LoggerProvider: reg,
Expand Down Expand Up @@ -576,4 +581,104 @@ func TestEngine(t *testing.T) {
assert.True(t, res)
}
})

t.Run("case=batch check", func(t *testing.T) {
reg := newDepsProvider(t, []*namespace.Namespace{
{Name: "test"},
})

relationtuple.MapAndWriteTuples(t, reg, &ketoapi.RelationTuple{
Namespace: "test",
Object: "object",
Relation: "admin",
SubjectID: pointerx.Ptr("user"),
},
&ketoapi.RelationTuple{
Namespace: "test",
Object: "object",
Relation: "owner",
SubjectSet: &ketoapi.SubjectSet{
Namespace: "test",
Object: "object",
Relation: "admin",
},
},
&ketoapi.RelationTuple{
Namespace: "test",
Object: "object",
Relation: "access",
SubjectSet: &ketoapi.SubjectSet{
Namespace: "test",
Object: "object",
Relation: "owner",
},
})

e := check.NewEngine(reg)

targetTuples := []*ketoapi.RelationTuple{
{ // direct relation
Namespace: "test",
Object: "object",
Relation: "admin",
SubjectID: pointerx.Ptr("user"),
},
{ // indirect relation
Namespace: "test",
Object: "object",
Relation: "owner",
SubjectID: pointerx.Ptr("user"),
},
{ // indirect relation, greater than max depth
Namespace: "test",
Object: "object",
Relation: "access",
SubjectID: pointerx.Ptr("user"),
},
{ // non-existent namespace
Namespace: "test2",
Object: "object",
Relation: "admin",
SubjectID: pointerx.Ptr("user"),
},
{ // unknown subject
Namespace: "test",
Object: "object",
Relation: "admin",
SubjectID: pointerx.Ptr("user2"),
},
{ // relation via subject set
Namespace: "test",
Object: "object",
Relation: "access",
SubjectSet: &ketoapi.SubjectSet{
Namespace: "test",
Object: "object",
Relation: "owner",
},
},
}

// Batch check with low max depth
results, err := e.BatchCheck(ctx, targetTuples, 2)
require.NoError(t, err)

require.Equal(t, checkgroup.IsMember, results[0].Membership)
require.NoError(t, results[0].Err)
require.Equal(t, checkgroup.IsMember, results[1].Membership)
require.NoError(t, results[1].Err)
require.Equal(t, checkgroup.NotMember, results[2].Membership)
require.NoError(t, results[2].Err)
require.Equal(t, checkgroup.MembershipUnknown, results[3].Membership)
require.EqualError(t, results[3].Err, herodot.ErrNotFound.Error())
require.Equal(t, checkgroup.NotMember, results[4].Membership)
require.NoError(t, results[4].Err)
require.Equal(t, checkgroup.IsMember, results[5].Membership)
require.NoError(t, results[5].Err)

// Check with higher max depth and verify the third tuple is now shown as a member
results, err = e.BatchCheck(ctx, targetTuples, 3)
require.NoError(t, err)
require.Equal(t, checkgroup.IsMember, results[2].Membership)
})
}
Loading

0 comments on commit d670d50

Please sign in to comment.