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 9 commits into
base: main
Choose a base branch
from
9 changes: 9 additions & 0 deletions 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 @@ -383,6 +384,14 @@ 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. This would enable optimizting path conflict checks during bundle
// activation if a bundle already defines its own root paths.
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
5 changes: 4 additions & 1 deletion ast/compile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1954,6 +1954,9 @@ bar.baz contains "quz" if true`,
"mod8.rego": `package badrules.complete_partial
p := 1
p[r] := 2 if { r := "foo" }`,

"mod9.rego": `package anotherbadrules.dataoverlap
p { true }`,
})

c.WithPathConflictsCheck(func(path []string) (bool, error) {
Expand All @@ -1963,7 +1966,7 @@ p[r] := 2 if { r := "foo" }`,
return false, fmt.Errorf("unexpected error")
}
return false, nil
})
}).WithPathConflictsCheckRoots([]string{"badrules"})

compileStages(c, c.checkRuleConflicts)

Expand Down
33 changes: 31 additions & 2 deletions 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,36 @@ 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
nodeNotFound := false
for _, key := range paths {
child := node.Child(String(key))
if child == nil {
nodeNotFound = true
break
}
node = child
}

if nodeNotFound {
// 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 plugins/bundle/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,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 plugins/bundle/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6574,6 +6574,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 !strings.Contains(status.Message, "detected overlapping") {
t.Fatalf(`Expected status message to contain "detected overlapping roots", found %s`, 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