-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix/virtual-cache ref unification scope #6957
Merged
johanfylling
merged 10 commits into
open-policy-agent:main
from
johanfylling:fix/virtual_cache_ref_unification_scope
Aug 28, 2024
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
fd9cdb7
topdown: Adding scope component to virtual cache key to capture pre-e…
johanfylling 7cc79b3
Adding yaml regression test
johanfylling 9d38eea
Making linter happy
johanfylling 0e7698a
Merge branch 'main' into fix/virtual_cache_ref_unification_scope
johanfylling 33f6d95
Merge branch 'main' into fix/virtual_cache_ref_unification_scope
johanfylling bb071c5
Making `vcKeyScope.Hash()` consider var indices
johanfylling da9b1ea
* Adding test for key scope order
johanfylling beffd30
Adding clarifying comment
johanfylling 7a7687f
Merge branch 'main' into fix/virtual_cache_ref_unification_scope
johanfylling 92852f5
Merge branch 'main' into fix/virtual_cache_ref_unification_scope
johanfylling File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2407,6 +2407,15 @@ type evalVirtualPartialCacheHint struct { | |
full bool | ||
} | ||
|
||
func (h *evalVirtualPartialCacheHint) keyWithoutScope() ast.Ref { | ||
if h.key != nil { | ||
if _, ok := h.key[len(h.key)-1].Value.(vcKeyScope); ok { | ||
return h.key[:len(h.key)-1] | ||
} | ||
} | ||
return h.key | ||
} | ||
|
||
func (e evalVirtualPartial) eval(iter unifyIterator) error { | ||
|
||
unknown := e.e.unknown(e.ref[:e.pos+1], e.bindings) | ||
|
@@ -2485,7 +2494,7 @@ func (e evalVirtualPartial) evalEachRule(iter unifyIterator, unknown bool) error | |
} | ||
|
||
if hint.key != nil { | ||
if v, err := result.Value.Find(hint.key[e.pos+1:]); err == nil && v != nil { | ||
if v, err := result.Value.Find(hint.keyWithoutScope()[e.pos+1:]); err == nil && v != nil { | ||
e.e.virtualCache.Put(hint.key, ast.NewTerm(v)) | ||
} | ||
} | ||
|
@@ -2832,6 +2841,8 @@ func (e evalVirtualPartial) evalCache(iter unifyIterator) (evalVirtualPartialCac | |
plugged := e.bindings.Plug(e.ref[e.pos+1]) | ||
|
||
if _, ok := plugged.Value.(ast.Var); ok { | ||
// Note: we might have additional opportunity to optimize here, if we consider that ground values | ||
// right of e.pos could create a smaller eval "scope" through ref bi-unification before evaluating rules. | ||
hint.full = true | ||
hint.key = e.plugged[:e.pos+1] | ||
e.e.instr.counterIncr(evalOpVirtualCacheMiss) | ||
|
@@ -2840,19 +2851,76 @@ func (e evalVirtualPartial) evalCache(iter unifyIterator) (evalVirtualPartialCac | |
|
||
m := maxRefLength(e.ir.Rules, len(e.ref)) | ||
|
||
// Creating the hint key by walking the ref and plugging vars until we hit a non-ground term. | ||
// Any ground term right of this point will affect the scope of evaluation by ref unification, | ||
// so we create a virtual-cache scope key to qualify the result stored in the cache. | ||
// | ||
// E.g. given the following rule: | ||
// | ||
// package example | ||
// | ||
// a[x][y][z] := x + y + z if { | ||
// some x in [1, 2] | ||
// some y in [3, 4] | ||
// some z in [5, 6] | ||
// } | ||
// | ||
// and the following ref (1): | ||
// | ||
// data.example.a[1][_][5] | ||
// | ||
// then the hint key will be: | ||
// | ||
// data.example.a[1][<_,5>] | ||
// | ||
// where <_,5> is the scope of the pre-eval unification. | ||
// This part does not contribute to the "location" of the cached data. | ||
// | ||
// The following ref (2): | ||
// | ||
// data.example.a[1][_][6] | ||
// | ||
// will produce the same hint key "location" 'data.example.a[1]', but a different scope component | ||
// '<_,6>', which will create a different entry in the cache. | ||
scoping := false | ||
hintKeyEnd := 0 | ||
for i := e.pos + 1; i < m; i++ { | ||
plugged = e.bindings.Plug(e.ref[i]) | ||
|
||
if !plugged.IsGround() { | ||
break | ||
if plugged.IsGround() && !scoping { | ||
hintKeyEnd = i | ||
hint.key = append(e.plugged[:i], plugged) | ||
} else { | ||
scoping = true | ||
hl := len(hint.key) | ||
if hl == 0 { | ||
break | ||
} | ||
if scope, ok := hint.key[hl-1].Value.(vcKeyScope); ok { | ||
scope.Ref = append(scope.Ref, plugged) | ||
hint.key[len(hint.key)-1] = ast.NewTerm(scope) | ||
} else { | ||
scope = vcKeyScope{} | ||
scope.Ref = append(scope.Ref, plugged) | ||
hint.key = append(hint.key, ast.NewTerm(scope)) | ||
} | ||
} | ||
|
||
hint.key = append(e.plugged[:i], plugged) | ||
|
||
if cached, _ := e.e.virtualCache.Get(hint.key); cached != nil { | ||
e.e.instr.counterIncr(evalOpVirtualCacheHit) | ||
hint.hit = true | ||
return hint, e.evalTerm(iter, i+1, cached, e.bindings) | ||
return hint, e.evalTerm(iter, hintKeyEnd+1, cached, e.bindings) | ||
} | ||
} | ||
|
||
if hl := len(hint.key); hl > 0 { | ||
if scope, ok := hint.key[hl-1].Value.(vcKeyScope); ok { | ||
scope = scope.reduce() | ||
if scope.empty() { | ||
hint.key = hint.key[:hl-1] | ||
} else { | ||
hint.key[hl-1].Value = scope | ||
} | ||
} | ||
} | ||
|
||
|
@@ -2861,6 +2929,85 @@ func (e evalVirtualPartial) evalCache(iter unifyIterator) (evalVirtualPartialCac | |
return hint, nil | ||
} | ||
|
||
// vcKeyScope represents the scoping that pre-rule-eval ref unification imposes on a virtual cache entry. | ||
type vcKeyScope struct { | ||
ast.Ref | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would we safe memory if we kept the index next to the ast.Ref? type vcKeyScope struct {
ref ast.Ref
idx int
} so that we can avoid copying the |
||
} | ||
|
||
func (q vcKeyScope) Compare(other ast.Value) int { | ||
if q2, ok := other.(vcKeyScope); ok { | ||
r1 := q.Ref | ||
r2 := q2.Ref | ||
if len(r1) != len(r2) { | ||
return -1 | ||
} | ||
|
||
for i := range r1 { | ||
_, v1IsVar := r1[i].Value.(ast.Var) | ||
_, v2IsVar := r2[i].Value.(ast.Var) | ||
if v1IsVar && v2IsVar { | ||
continue | ||
} | ||
if r1[i].Value.Compare(r2[i].Value) != 0 { | ||
return -1 | ||
} | ||
} | ||
|
||
return 0 | ||
} | ||
return 1 | ||
} | ||
|
||
func (vcKeyScope) Find(ast.Ref) (ast.Value, error) { | ||
return nil, nil | ||
} | ||
|
||
func (q vcKeyScope) Hash() int { | ||
var hash int | ||
for _, v := range q.Ref { | ||
if _, ok := v.Value.(ast.Var); ok { | ||
// all vars are equal | ||
hash++ | ||
} else { | ||
hash += v.Value.Hash() | ||
} | ||
} | ||
return hash | ||
} | ||
|
||
func (q vcKeyScope) IsGround() bool { | ||
return false | ||
} | ||
|
||
func (q vcKeyScope) String() string { | ||
buf := make([]string, 0, len(q.Ref)) | ||
for _, t := range q.Ref { | ||
if _, ok := t.Value.(ast.Var); ok { | ||
buf = append(buf, "_") | ||
} else { | ||
buf = append(buf, t.String()) | ||
} | ||
} | ||
return fmt.Sprintf("<%s>", strings.Join(buf, ",")) | ||
} | ||
|
||
// reduce removes vars from the tail of the ref. | ||
func (q vcKeyScope) reduce() vcKeyScope { | ||
ref := q.Ref.Copy() | ||
var i int | ||
for i = len(q.Ref) - 1; i >= 0; i-- { | ||
if _, ok := q.Ref[i].Value.(ast.Var); !ok { | ||
break | ||
} | ||
} | ||
ref = ref[:i+1] | ||
return vcKeyScope{ref} | ||
} | ||
|
||
func (q vcKeyScope) empty() bool { | ||
return len(q.Ref) == 0 | ||
} | ||
|
||
func getNestedObject(ref ast.Ref, rootObj *ast.Object, b *bindings, l *ast.Location) (*ast.Object, error) { | ||
current := rootObj | ||
for _, term := range ref { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be helpful if we were to add a comment here explaining the code in the loop with an example if feasible. It would probably help a future reviewer.