-
Notifications
You must be signed in to change notification settings - Fork 487
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
Module id fail #5261
Module id fail #5261
Changes from all commits
6db453b
a2c2755
13c99ca
dfa0b77
01552ce
f8f9930
8ceafb2
54b238c
91c0abe
f7caffe
cb8d986
92cb1c1
f9e89e0
6d28509
a96a32e
6c44b21
db23876
a9d509d
e81a2ef
e5588be
e69db8c
acaae50
77777b0
9b9f7ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package componenttest | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"github.com/grafana/agent/component" | ||
mod "github.com/grafana/agent/component/module" | ||
) | ||
|
||
func init() { | ||
component.Register(component.Registration{ | ||
Name: "test.fail.module", | ||
Args: TestFailArguments{}, | ||
Exports: mod.Exports{}, | ||
|
||
Build: func(opts component.Options, args component.Arguments) (component.Component, error) { | ||
m, err := mod.NewModuleComponent(opts) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if args.(TestFailArguments).Fail { | ||
return nil, fmt.Errorf("module told to fail") | ||
} | ||
err = m.LoadFlowSource(nil, args.(TestFailArguments).Content) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &TestFailModule{ | ||
mc: m, | ||
content: args.(TestFailArguments).Content, | ||
opts: opts, | ||
fail: args.(TestFailArguments).Fail, | ||
ch: make(chan error), | ||
}, nil | ||
}, | ||
}) | ||
} | ||
|
||
type TestFailArguments struct { | ||
Content string `river:"content,attr"` | ||
Fail bool `river:"fail,attr,optional"` | ||
} | ||
|
||
type TestFailModule struct { | ||
content string | ||
opts component.Options | ||
ch chan error | ||
mc *mod.ModuleComponent | ||
fail bool | ||
} | ||
|
||
func (t *TestFailModule) Run(ctx context.Context) error { | ||
go t.mc.RunFlowController(ctx) | ||
<-ctx.Done() | ||
return nil | ||
} | ||
|
||
func (t *TestFailModule) UpdateContent(content string) error { | ||
t.content = content | ||
err := t.mc.LoadFlowSource(nil, t.content) | ||
return err | ||
} | ||
|
||
func (t *TestFailModule) Update(_ component.Arguments) error { | ||
return nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ import ( | |
"path" | ||
"sync" | ||
|
||
"github.com/go-kit/log/level" | ||
"github.com/grafana/agent/component" | ||
"github.com/grafana/agent/pkg/flow/internal/controller" | ||
"github.com/grafana/agent/pkg/flow/internal/worker" | ||
|
@@ -46,9 +47,6 @@ func (m *moduleController) NewModule(id string, export component.ExportFunc) (co | |
if id != "" { | ||
fullPath = path.Join(fullPath, id) | ||
} | ||
if _, found := m.modules[fullPath]; found { | ||
return nil, fmt.Errorf("id %s already exists", id) | ||
} | ||
|
||
mod := newModule(&moduleOptions{ | ||
ID: fullPath, | ||
|
@@ -57,26 +55,33 @@ func (m *moduleController) NewModule(id string, export component.ExportFunc) (co | |
parent: m, | ||
}) | ||
|
||
if err := m.o.ModuleRegistry.Register(fullPath, mod); err != nil { | ||
return nil, err | ||
} | ||
|
||
m.modules[fullPath] = struct{}{} | ||
return mod, nil | ||
} | ||
|
||
func (m *moduleController) removeID(id string) { | ||
func (m *moduleController) removeModule(mod *module) { | ||
m.mut.Lock() | ||
defer m.mut.Unlock() | ||
|
||
delete(m.modules, id) | ||
m.o.ModuleRegistry.Unregister(id) | ||
m.o.ModuleRegistry.Unregister(mod.o.ID) | ||
delete(m.modules, mod.o.ID) | ||
} | ||
|
||
func (m *moduleController) addModule(mod *module) error { | ||
m.mut.Lock() | ||
defer m.mut.Unlock() | ||
if err := m.o.ModuleRegistry.Register(mod.o.ID, mod); err != nil { | ||
level.Error(m.o.Logger).Log("msg", "error registering module", "id", mod.o.ID, "err", err) | ||
return err | ||
} | ||
m.modules[mod.o.ID] = struct{}{} | ||
return nil | ||
} | ||
|
||
// ModuleIDs implements [controller.ModuleController]. | ||
func (m *moduleController) ModuleIDs() []string { | ||
m.mut.RLock() | ||
defer m.mut.RUnlock() | ||
|
||
return maps.Keys(m.modules) | ||
} | ||
|
||
|
@@ -86,7 +91,7 @@ type module struct { | |
} | ||
|
||
type moduleOptions struct { | ||
ID string | ||
ID string // ID is the full name including all parents, "module.file.example.prometheus.remote_write.id". | ||
export component.ExportFunc | ||
parent *moduleController | ||
*moduleControllerOptions | ||
|
@@ -135,9 +140,14 @@ func (c *module) LoadConfig(config []byte, args map[string]any) error { | |
// will be run until Run is called. | ||
// | ||
// Run blocks until the provided context is canceled. | ||
func (c *module) Run(ctx context.Context) { | ||
defer c.o.parent.removeID(c.o.ID) | ||
mattdurham marked this conversation as resolved.
Show resolved
Hide resolved
|
||
func (c *module) Run(ctx context.Context) error { | ||
if err := c.o.parent.addModule(c); err != nil { | ||
return err | ||
} | ||
defer c.o.parent.removeModule(c) | ||
Comment on lines
+144
to
+147
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. We should get this merged, but it's standing out as potentially concerning to me that this changes things such that the lifetime of a component and module are now different: a component exists within the controller whenever it's defined in a file, even if it's not running, but a module doesn't exist until it starts running. I'm not sure if this will introduce any problems, so let's keep an eye for related issues once this is merged. 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. IMO this feels alright, the component is the parent of module(s), so the component life span should be greater than the modules it controls. Def something to keep an eye on. |
||
|
||
c.f.Run(ctx) | ||
return nil | ||
} | ||
|
||
// moduleControllerOptions holds static options for module controller. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
package flow | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
"time" | ||
|
||
"github.com/grafana/agent/pkg/flow/componenttest" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestIDRemovalIfFailedToLoad(t *testing.T) { | ||
f := New(testOptions(t)) | ||
|
||
fullContent := "test.fail.module \"t1\" { content = \"\" }" | ||
fl, err := ParseSource("test", []byte(fullContent)) | ||
require.NoError(t, err) | ||
err = f.LoadSource(fl, nil) | ||
require.NoError(t, err) | ||
ctx := context.Background() | ||
ctx, cnc := context.WithTimeout(ctx, 600*time.Second) | ||
|
||
go f.Run(ctx) | ||
var t1 *componenttest.TestFailModule | ||
require.Eventually(t, func() bool { | ||
t1 = f.loader.Components()[0].Component().(*componenttest.TestFailModule) | ||
return t1 != nil | ||
}, 10*time.Second, 100*time.Millisecond) | ||
require.Eventually(t, func() bool { | ||
f.loadMut.RLock() | ||
defer f.loadMut.RUnlock() | ||
// This should be one due to t1. | ||
return len(f.modules.List()) == 1 | ||
}, 10*time.Second, 100*time.Millisecond) | ||
badContent := | ||
`test.fail.module "bad" { | ||
content="" | ||
fail=true | ||
}` | ||
err = t1.UpdateContent(badContent) | ||
// Because we have bad content this should fail, but the ids should be removed. | ||
require.Error(t, err) | ||
require.Eventually(t, func() bool { | ||
f.loadMut.RLock() | ||
defer f.loadMut.RUnlock() | ||
// Only one since the bad one never should have been added. | ||
rightLength := len(f.modules.List()) == 1 | ||
_, foundT1 := f.modules.Get("test.fail.module.t1") | ||
return rightLength && foundT1 | ||
}, 10*time.Second, 100*time.Millisecond) | ||
// fail a second time to ensure the once is done again. | ||
err = t1.UpdateContent(badContent) | ||
require.Error(t, err) | ||
|
||
goodContent := | ||
`test.fail.module "good" { | ||
content="" | ||
fail=false | ||
}` | ||
err = t1.UpdateContent(goodContent) | ||
require.NoError(t, err) | ||
require.Eventually(t, func() bool { | ||
f.loadMut.RLock() | ||
defer f.loadMut.RUnlock() | ||
modT1, foundT1 := f.modules.Get("test.fail.module.t1") | ||
modGood, foundGood := f.modules.Get("test.fail.module.t1/test.fail.module.good") | ||
return modT1 != nil && modGood != nil && foundT1 && foundGood | ||
}, 10*time.Second, 100*time.Millisecond) | ||
cnc() | ||
require.Eventually(t, func() bool { | ||
f.loadMut.RLock() | ||
defer f.loadMut.RUnlock() | ||
// All should be cleaned up. | ||
return len(f.modules.List()) == 0 | ||
}, 10*time.Second, 100*time.Millisecond) | ||
} |
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.
Just as an aside: this would be considered a breaking change to the API, which is re-enforcing the idea for me that everything should be moved to internal for 1.0 until we're ready to start exposing parts of our code as stable APIs.
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.
Agreed, this is mostly for the tests so we can check specific error conditions.