Skip to content
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

perf: optimize activation of bundles with no inter-bundle path overlap #7155

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
10 changes: 10 additions & 0 deletions v1/ast/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ type Compiler struct {
maxErrs int
sorted []string // list of sorted module names
pathExists func([]string) (bool, error)
pathConflictCheckRoots []string
after map[string][]CompilerStageDefinition
metrics metrics.Metrics
capabilities *Capabilities // user-supplied capabilities
Expand Down Expand Up @@ -389,6 +390,15 @@ func (c *Compiler) WithPathConflictsCheck(fn func([]string) (bool, error)) *Comp
return c
}

// WithPathConflictsCheckRoots enables checking path conflicts from the specified root instead
// of the top root node. Limiting conflict checks to a known set of roots, such as bundle roots,
// improves performance. Each root has the format of a "/"-delimited string, excluding the "data"
// root document.
func (c *Compiler) WithPathConflictsCheckRoots(rootPaths []string) *Compiler {
c.pathConflictCheckRoots = rootPaths
return c
}

// WithStageAfter registers a stage to run during compilation after
// the named stage.
func (c *Compiler) WithStageAfter(after string, stage CompilerStageDefinition) *Compiler {
Expand Down
34 changes: 34 additions & 0 deletions v1/ast/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"errors"
"fmt"
"reflect"
"slices"
"sort"
"strings"
"testing"
Expand Down Expand Up @@ -1989,6 +1990,39 @@ p[r] := 2 if { r := "foo" }`,
assertCompilerErrorStrings(t, c, expected)
}

func TestCompilerCheckRuleConflictsWithRoots(t *testing.T) {

c := getCompilerWithParsedModules(map[string]string{
"mod1.rego": `package badrules.dataoverlap
p if { true }`,
"mod2.rego": `package badrules.existserr
p if { true }`,

// this does not trigger conflict check because
// WithPathConflictsCheckRoots limits the root to "badrules".
"mod3.rego": `package badrules_outside_root.dataoverlap
p if { true }`,
})

c.WithPathConflictsCheck(func(path []string) (bool, error) {
if slices.Contains(path, "dataoverlap") {
return true, nil
} else if reflect.DeepEqual(path, []string{"badrules", "existserr", "p"}) {
return false, fmt.Errorf("unexpected error")
}
return false, nil
}).WithPathConflictsCheckRoots([]string{"badrules"})

compileStages(c, c.checkRuleConflicts)

expected := []string{
"rego_compile_error: conflict check for data path badrules/existserr/p: unexpected error",
"rego_compile_error: conflicting rule for data path badrules/dataoverlap/p found",
}

assertCompilerErrorStrings(t, c, expected)
}

func TestCompilerCheckRuleConflictsDefaultFunction(t *testing.T) {
tests := []struct {
note string
Expand Down
30 changes: 28 additions & 2 deletions v1/ast/conflicts.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package ast

import (
"slices"
"strings"
)

Expand All @@ -18,8 +19,33 @@ func CheckPathConflicts(c *Compiler, exists func([]string) (bool, error)) Errors
return nil
}

for _, node := range root.Children {
errs = append(errs, checkDocumentConflicts(node, exists, nil)...)
if len(c.pathConflictCheckRoots) == 0 || slices.Contains(c.pathConflictCheckRoots, "") {
for _, child := range root.Children {
errs = append(errs, checkDocumentConflicts(child, exists, nil)...)
}
return errs
}

for _, rootPath := range c.pathConflictCheckRoots {
// traverse AST from `path` to go to the new root
paths := strings.Split(rootPath, "/")
node := root
for _, key := range paths {
node = node.Child(String(key))
if node == nil {
break
}
}

if node == nil {
// could not find the node from the AST (e.g. `path` is from a data file)
// then no conflict is possible
continue
}

for _, child := range node.Children {
errs = append(errs, checkDocumentConflicts(child, exists, paths)...)
}
}

return errs
Expand Down
4 changes: 4 additions & 0 deletions v1/plugins/bundle/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,10 @@ func (p *Plugin) activate(ctx context.Context, name string, b *bundle.Bundle, is
compiler = compiler.WithPathConflictsCheck(storage.NonEmpty(ctx, p.manager.Store, txn)).
WithEnablePrintStatements(p.manager.EnablePrintStatements())

if b.Manifest.Roots != nil {
compiler = compiler.WithPathConflictsCheckRoots(*b.Manifest.Roots)
}

var activateErr error

opts := &bundle.ActivateOpts{
Expand Down
188 changes: 188 additions & 0 deletions v1/plugins/bundle/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6718,6 +6718,194 @@ func TestGetNormalizedBundleName(t *testing.T) {
}
}

func TestBundleActivationWithRootOverlap(t *testing.T) {
ctx := context.Background()
plugin := getPluginWithExistingLoadedBundle(
t,
"policy-bundle",
[]string{"foo/bar"},
nil,
[]testModule{
{
Path: "foo/bar/bar.rego",
Data: `package foo.bar
result := true`,
},
},
)

bundleName := "new-bundle"
plugin.status[bundleName] = &Status{Name: bundleName, Metrics: metrics.New()}
plugin.downloaders[bundleName] = download.New(download.Config{}, plugin.manager.Client(""), bundleName)

b := getTestBundleWithData(
[]string{"foo/bar/baz"},
[]byte(`{"foo": {"bar": 1, "baz": "qux"}}`),
nil,
)

b.Manifest.Init()
plugin.oneShot(ctx, bundleName, download.Update{Bundle: &b, Metrics: metrics.New(), Size: snapshotBundleSize})

// "foo/bar" and "foo/bar/baz" overlap with each other; activation will fail
status, ok := plugin.status[bundleName]
if !ok {
t.Fatalf("Expected to find status for %s, found nil", bundleName)
}
if status.Code != errCode {
t.Fatalf("Expected status code to be %s, found %s", errCode, status.Code)
}
if exp := "detected overlapping roots"; !strings.Contains(status.Message, exp) {
t.Fatalf(`Expected status message to contain "%s", found %s`, exp, status.Message)
}
}

func TestBundleActivationWithNoManifestRootsButWithPathConflict(t *testing.T) {
ctx := context.Background()
plugin := getPluginWithExistingLoadedBundle(
t,
"policy-bundle",
[]string{"foo/bar"},
nil,
[]testModule{
{
Path: "foo/bar/bar.rego",
Data: `package foo.bar
result := true`,
},
},
)

bundleName := "new-bundle"
plugin.status[bundleName] = &Status{Name: bundleName, Metrics: metrics.New()}
plugin.downloaders[bundleName] = download.New(download.Config{}, plugin.manager.Client(""), bundleName)

b := getTestBundleWithData(
nil,
[]byte(`{"foo": {"bar": 1, "baz": "qux"}}`),
nil,
)

b.Manifest.Init()
plugin.oneShot(ctx, bundleName, download.Update{Bundle: &b, Metrics: metrics.New(), Size: snapshotBundleSize})

// new bundle has path "foo/bar" which overlaps with existing bundle with path "foo/bar"; activation will fail
status, ok := plugin.status[bundleName]
if !ok {
t.Fatalf("Expected to find status for %s, found nil", bundleName)
}
if status.Code != errCode {
t.Fatalf("Expected status code to be %s, found %s", errCode, status.Code)
}
if !strings.Contains(status.Message, "detected overlapping") {
t.Fatalf(`Expected status message to contain "detected overlapping roots", found %s`, status.Message)
}
}

func TestBundleActivationWithNoManifestRootsOverlap(t *testing.T) {
ctx := context.Background()
plugin := getPluginWithExistingLoadedBundle(
t,
"policy-bundle",
[]string{"foo/bar"},
nil,
[]testModule{
{
Path: "foo/bar/bar.rego",
Data: `package foo.bar
result := true`,
},
},
)

bundleName := "new-bundle"
plugin.status[bundleName] = &Status{Name: bundleName, Metrics: metrics.New()}
plugin.downloaders[bundleName] = download.New(download.Config{}, plugin.manager.Client(""), bundleName)

b := getTestBundleWithData(
[]string{"foo/baz"},
nil,
[]testModule{
{
Path: "foo/bar/baz.rego",
Data: `package foo.baz
result := true`,
},
},
)

b.Manifest.Init()
plugin.oneShot(ctx, bundleName, download.Update{Bundle: &b, Metrics: metrics.New(), Size: snapshotBundleSize})

status, ok := plugin.status[bundleName]
if !ok {
t.Fatalf("Expected to find status for %s, found nil", bundleName)
}
if status.Code != "" {
t.Fatalf("Expected status code to be empty, found %s", status.Code)
}
}

type testModule struct {
Path string
Data string
}

func getTestBundleWithData(roots []string, data []byte, modules []testModule) bundle.Bundle {
b := bundle.Bundle{}

if len(roots) > 0 {
b.Manifest = bundle.Manifest{Roots: &roots}
}

if len(data) > 0 {
b.Data = util.MustUnmarshalJSON(data).(map[string]interface{})
}

for _, m := range modules {
if len(m.Data) > 0 {
b.Modules = append(b.Modules,
bundle.ModuleFile{
Path: m.Path,
Parsed: ast.MustParseModule(m.Data),
Raw: []byte(m.Data),
},
)
}
}

b.Manifest.Init()

return b
}

func getPluginWithExistingLoadedBundle(t *testing.T, bundleName string, roots []string, data []byte, modules []testModule) *Plugin {
ctx := context.Background()
store := inmem.NewWithOpts(inmem.OptRoundTripOnWrite(false), inmem.OptReturnASTValuesOnRead(true))
manager := getTestManagerWithOpts(nil, store)
plugin := New(&Config{}, manager)
plugin.status[bundleName] = &Status{Name: bundleName, Metrics: metrics.New()}
plugin.downloaders[bundleName] = download.New(download.Config{}, plugin.manager.Client(""), bundleName)

ensurePluginState(t, plugin, plugins.StateNotReady)

b := getTestBundleWithData(roots, data, modules)

plugin.oneShot(ctx, bundleName, download.Update{Bundle: &b, Metrics: metrics.New(), Size: snapshotBundleSize})

ensurePluginState(t, plugin, plugins.StateOK)

if status, ok := plugin.status[bundleName]; !ok {
t.Fatalf("Expected to find status for %s, found nil", bundleName)
} else if status.Type != bundle.SnapshotBundleType {
t.Fatalf("Expected snapshot bundle but got %v", status.Type)
} else if status.Size != snapshotBundleSize {
t.Fatalf("Expected snapshot bundle size %d but got %d", snapshotBundleSize, status.Size)
}

return plugin
}

func writeTestBundleToDisk(t *testing.T, srcDir string, signed bool) bundle.Bundle {
t.Helper()

Expand Down