Skip to content

Commit 5379baf

Browse files
fix(core): reduces GetAttributeValuesByFqns calls in getDecisions (#1857)
### Proposed Changes * getDecisions calls `GetAttributeValuesByFqns` per `ResourceAttribute`, with bulk rewrap incoming, this can be quite expensive. * rather than `GetAttributesValuesByFqns` per `ResourceAttribute` get all FQNs in one call by collecting them in the `ResourceAttribute`. ### Checklist - [ ] I have added or updated unit tests - [ ] I have added or updated integration tests (if appropriate) - [ ] I have added or updated documentation ### Testing Instructions `go run ./examples benchmark-decision --insecurePlaintextConn --count 1000` locally I got 1.3s vs 80ms, so a very important performance change.
1 parent 5dbf4d0 commit 5379baf

File tree

2 files changed

+71
-62
lines changed

2 files changed

+71
-62
lines changed

examples/cmd/benchmark_decision.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ func init() {
1616
Long: `A OpenTDF benchmark tool to measure throughput and latency with configurable concurrency.`,
1717
RunE: runDecisionBenchmark,
1818
}
19-
19+
benchmarkCmd.Flags().IntVar(&config.RequestCount, "count", 100, "Total number of requests")
2020
ExamplesCmd.AddCommand(benchmarkCmd)
2121
}
2222

@@ -28,7 +28,7 @@ func runDecisionBenchmark(cmd *cobra.Command, args []string) error {
2828
}
2929

3030
ras := []*authorization.ResourceAttribute{}
31-
for i := 0; i < 100; i++ {
31+
for i := 0; i < config.RequestCount; i++ {
3232
ras = append(ras, &authorization.ResourceAttribute{AttributeValueFqns: []string{"https://example.com/attr/attr1/value/value1"}})
3333
}
3434

service/authorization/authorization.go

Lines changed: 69 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -200,20 +200,21 @@ func (as *AuthorizationService) GetDecisions(ctx context.Context, req *connect.R
200200
}
201201

202202
func (as *AuthorizationService) getDecisions(ctx context.Context, dr *authorization.DecisionRequest) ([]*authorization.DecisionResponse, error) {
203-
var attrDefsReqs [][]*policy.Attribute
204-
var attrValsReqs [][]*policy.Value
205-
var fqnsReqs [][]string
206203
allPertinentFQNS := &authorization.ResourceAttribute{AttributeValueFqns: make([]string, 0)}
207204
response := make([]*authorization.DecisionResponse, len(dr.GetResourceAttributes())*len(dr.GetEntityChains()))
208-
for raIdx, ra := range dr.GetResourceAttributes() {
209-
as.logger.DebugContext(ctx, "getting resource attributes", slog.String("FQNs", strings.Join(ra.GetAttributeValueFqns(), ", ")))
210205

211-
// get attribute definition/value combinations
212-
dataAttrDefsAndVals, err := retrieveAttributeDefinitions(ctx, ra, as.sdk)
213-
if err != nil {
214-
// if attribute an FQN does not exist
215-
// return deny for all entity chains aginst this RA set and continue to next
216-
if errors.Is(err, status.Error(codes.NotFound, db.ErrTextNotFound)) || errors.Is(err, ErrEmptyStringAttribute) {
206+
// TODO: fetching missing FQNs should not lead into a complete failure, rather a list of unknown FQNs would be preferred
207+
var err error
208+
var dataAttrDefsAndVals map[string]*attr.GetAttributeValuesByFqnsResponse_AttributeAndValue
209+
allPertinentFQNS.AttributeValueFqns, err = getAttributesFromRas(dr.GetResourceAttributes())
210+
if err == nil {
211+
dataAttrDefsAndVals, err = retrieveAttributeDefinitions(ctx, allPertinentFQNS.GetAttributeValueFqns(), as.sdk)
212+
}
213+
if err != nil {
214+
// if attribute an FQN does not exist
215+
// return deny for all entity chains aginst this RAs
216+
if errors.Is(err, status.Error(codes.NotFound, db.ErrTextNotFound)) || errors.Is(err, ErrEmptyStringAttribute) {
217+
for raIdx, ra := range dr.GetResourceAttributes() {
217218
for ecIdx, ec := range dr.GetEntityChains() {
218219
decisionResp := &authorization.DecisionResponse{
219220
Decision: authorization.DecisionResponse_DECISION_DENY,
@@ -231,45 +232,28 @@ func (as *AuthorizationService) getDecisions(ctx context.Context, dr *authorizat
231232
}
232233
responseIdx := (raIdx * len(dr.GetEntityChains())) + ecIdx
233234
response[responseIdx] = decisionResp
234-
// append empty values to keep the order of the requests
235-
attrDefsReqs = append(attrDefsReqs, []*policy.Attribute{})
236-
attrValsReqs = append(attrValsReqs, []*policy.Value{})
237-
fqnsReqs = append(fqnsReqs, []string{})
238235
}
239-
continue
240236
}
241-
return nil, db.StatusifyError(err, db.ErrTextGetRetrievalFailed, slog.String("fqns", strings.Join(ra.GetAttributeValueFqns(), ", ")))
237+
return response, nil
242238
}
239+
return nil, db.StatusifyError(err, db.ErrTextGetRetrievalFailed, slog.String("fqns", strings.Join(allPertinentFQNS.GetAttributeValueFqns(), ", ")))
240+
}
243241

244-
var attrDefs []*policy.Attribute
245-
var attrVals []*policy.Value
246-
var fqns []string
247-
248-
for fqn, v := range dataAttrDefsAndVals {
249-
attrDefs = append(attrDefs, v.GetAttribute())
250-
attrVal := v.GetValue()
251-
fqns = append(fqns, fqn)
252-
attrVal.Fqn = fqn
253-
attrVals = append(attrVals, attrVal)
254-
}
255-
256-
attrDefs, err = populateAttrDefValueFqns(attrDefs)
257-
if err != nil {
258-
return nil, connect.NewError(connect.CodeInternal, err)
259-
}
260-
allPertinentFQNS.AttributeValueFqns = append(allPertinentFQNS.GetAttributeValueFqns(), ra.GetAttributeValueFqns()...)
261-
262-
// get the relevant resource attribute fqns
263-
for _, attrDef := range attrDefs {
264-
if attrDef.GetRule() == policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY {
265-
for _, value := range attrDef.GetValues() {
266-
allPertinentFQNS.AttributeValueFqns = append(allPertinentFQNS.AttributeValueFqns, value.GetFqn())
267-
}
242+
var allAttrDefs []*policy.Attribute
243+
for _, v := range dataAttrDefsAndVals {
244+
allAttrDefs = append(allAttrDefs, v.GetAttribute())
245+
}
246+
allAttrDefs, err = populateAttrDefValueFqns(allAttrDefs)
247+
if err != nil {
248+
return nil, connect.NewError(connect.CodeInternal, err)
249+
}
250+
// get the relevant resource attribute fqns
251+
for _, attrDef := range allAttrDefs {
252+
if attrDef.GetRule() == policy.AttributeRuleTypeEnum_ATTRIBUTE_RULE_TYPE_ENUM_HIERARCHY {
253+
for _, value := range attrDef.GetValues() {
254+
allPertinentFQNS.AttributeValueFqns = append(allPertinentFQNS.AttributeValueFqns, value.GetFqn())
268255
}
269256
}
270-
attrDefsReqs = append(attrDefsReqs, attrDefs)
271-
attrValsReqs = append(attrValsReqs, attrVals)
272-
fqnsReqs = append(fqnsReqs, fqns)
273257
}
274258

275259
var ecChainEntitlementsResponse []*connect.Response[authorization.GetEntitlementsResponse]
@@ -294,15 +278,31 @@ func (as *AuthorizationService) getDecisions(ctx context.Context, dr *authorizat
294278
}
295279

296280
for raIdx, ra := range dr.GetResourceAttributes() {
281+
var attrDefs []*policy.Attribute
282+
var attrVals []*policy.Value
283+
var fqns []string
284+
285+
for _, fqn := range ra.GetAttributeValueFqns() {
286+
fqn = strings.ToLower(fqn)
287+
fqns = append(fqns, fqn)
288+
v := dataAttrDefsAndVals[fqn]
289+
attrDefs = append(attrDefs, v.GetAttribute())
290+
attrVal := v.GetValue()
291+
attrVal.Fqn = fqn
292+
attrVals = append(attrVals, attrVal)
293+
}
294+
295+
attrDefs, err = populateAttrDefValueFqns(attrDefs)
296+
if err != nil {
297+
return nil, connect.NewError(connect.CodeInternal, err)
298+
}
299+
297300
for ecIdx, ec := range dr.GetEntityChains() {
298301
// check if we already have a decision for this entity chain
299302
responseIdx := (raIdx * len(dr.GetEntityChains())) + ecIdx
300303
if response[responseIdx] != nil {
301304
continue
302305
}
303-
attrVals := attrValsReqs[raIdx]
304-
attrDefs := attrDefsReqs[raIdx]
305-
fqns := fqnsReqs[raIdx]
306306

307307
//
308308
// TODO: we should already have the subject mappings here and be able to just use OPA to trim down the known data attr values to the ones matched up with the entities
@@ -651,27 +651,36 @@ func (as *AuthorizationService) GetEntitlements(ctx context.Context, req *connec
651651
return resp, nil
652652
}
653653

654-
func retrieveAttributeDefinitions(ctx context.Context, ra *authorization.ResourceAttribute, sdk *otdf.SDK) (map[string]*attr.GetAttributeValuesByFqnsResponse_AttributeAndValue, error) {
655-
attrFqns := ra.GetAttributeValueFqns()
656-
if len(attrFqns) == 0 {
657-
return make(map[string]*attr.GetAttributeValuesByFqnsResponse_AttributeAndValue), nil
658-
}
659-
// remove empty strings
660-
attrFqnsNoEmpty := attrFqns[:0] // Use the same backing array to avoid allocation
661-
for _, str := range attrFqns {
662-
if str != "" {
663-
attrFqnsNoEmpty = append(attrFqnsNoEmpty, str)
654+
func getAttributesFromRas(ras []*authorization.ResourceAttribute) ([]string, error) {
655+
var attrFqns []string
656+
repeats := make(map[string]bool)
657+
moreThanOneAttr := false
658+
for _, ra := range ras {
659+
for _, str := range ra.GetAttributeValueFqns() {
660+
moreThanOneAttr = true
661+
if str != "" && !repeats[str] {
662+
attrFqns = append(attrFqns, str)
663+
repeats[str] = true
664+
}
664665
}
665666
}
666-
// if no attribute value FQNs after removal, return error
667-
if len(attrFqnsNoEmpty) == 0 {
667+
668+
if moreThanOneAttr && len(attrFqns) == 0 {
668669
return nil, ErrEmptyStringAttribute
669670
}
671+
return attrFqns, nil
672+
}
673+
674+
func retrieveAttributeDefinitions(ctx context.Context, attrFqns []string, sdk *otdf.SDK) (map[string]*attr.GetAttributeValuesByFqnsResponse_AttributeAndValue, error) {
675+
if len(attrFqns) == 0 {
676+
return make(map[string]*attr.GetAttributeValuesByFqnsResponse_AttributeAndValue), nil
677+
}
678+
670679
resp, err := sdk.Attributes.GetAttributeValuesByFqns(ctx, &attr.GetAttributeValuesByFqnsRequest{
671680
WithValue: &policy.AttributeValueSelector{
672681
WithSubjectMaps: false,
673682
},
674-
Fqns: attrFqnsNoEmpty,
683+
Fqns: attrFqns,
675684
})
676685
if err != nil {
677686
return nil, err

0 commit comments

Comments
 (0)