master
go 202 lines 6.26 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package sd
4
5 import (
6 "bytes"
7 "context"
8 "fmt"
9 "testing"
10 "time"
11
12 "github.com/netdata/netdata/go/plugins/logger"
13 "github.com/netdata/netdata/go/plugins/pkg/netdataapi"
14 "github.com/netdata/netdata/go/plugins/pkg/safewriter"
15 "github.com/netdata/netdata/go/plugins/plugin/agent/discovery/sd/pipeline"
16 "github.com/netdata/netdata/go/plugins/plugin/agent/policy"
17 "github.com/netdata/netdata/go/plugins/plugin/framework/confgroup"
18 "github.com/netdata/netdata/go/plugins/plugin/framework/dyncfg"
19 "github.com/netdata/netdata/go/plugins/plugin/framework/functions"
20
21 "github.com/stretchr/testify/assert"
22 "github.com/stretchr/testify/require"
23 )
24
25 func TestServiceDiscovery_Run_WaitDecision(t *testing.T) {
26 tests := map[string]struct {
27 run func(t *testing.T, sd *ServiceDiscovery, confCh chan confFile, stop func())
28 }{
29 "enable command clears wait and starts pipeline": {
30 run: func(t *testing.T, sd *ServiceDiscovery, confCh chan confFile, stop func()) {
31 cfg := prepareConfigFile("/etc/netdata/sd.d/job1.conf", "job1")
32 confCh <- cfg
33
34 require.Eventually(t, sd.handler.WaitingForDecision, time.Second, 10*time.Millisecond)
35
36 sd.dyncfgCh <- dyncfg.NewFunction(functions.Function{
37 UID: "enable-job1",
38 Args: []string{sd.dyncfgJobID(testDiscovererTypeNetListeners, "job1"), "enable"},
39 })
40
41 require.Eventually(t, func() bool {
42 return !sd.handler.WaitingForDecision() &&
43 exposedExistsByKey(sd.exposed, testDiscovererTypeNetListeners+":job1") &&
44 sd.mgr.IsRunning(pipelineKeyFromSource(cfg.source))
45 }, time.Second, 10*time.Millisecond)
46
47 stop()
48
49 entry, ok := sd.exposed.LookupByKey(testDiscovererTypeNetListeners + ":job1")
50 require.True(t, ok)
51 assert.Equal(t, dyncfg.StatusRunning, entry.Status)
52 },
53 },
54 "second config blocks while wait gate is open and proceeds after decision": {
55 run: func(t *testing.T, sd *ServiceDiscovery, confCh chan confFile, stop func()) {
56 // Without a wait-decision timeout, the run loop must stay in
57 // WaitingForDecision until an explicit enable/disable for cfg1
58 // arrives. Sending cfg2 in the meantime must block until the
59 // gate clears. This guards against accidental gate-clearing or
60 // a regression that lets new configs interleave.
61 cfg1 := prepareConfigFile("/etc/netdata/sd.d/job1.conf", "job1")
62 cfg2 := prepareConfigFile("/etc/netdata/sd.d/job2.conf", "job2")
63
64 confCh <- cfg1
65 require.Eventually(t, sd.handler.WaitingForDecision, time.Second, 10*time.Millisecond)
66
67 secondSent := make(chan struct{})
68 go func() {
69 confCh <- cfg2
70 close(secondSent)
71 }()
72
73 // Give the goroutine a chance to either block (expected) or
74 // race ahead. We can't use require.Eventually for negative
75 // "still blocked" assertions, but a short window is enough to
76 // catch a regression that lets the second config flow through
77 // while the wait gate is still open.
78 select {
79 case <-secondSent:
80 t.Fatal("second config was processed while wait gate was open")
81 case <-time.After(100 * time.Millisecond):
82 }
83 require.True(t, sd.handler.WaitingForDecision(), "wait gate should still be open before decision")
84
85 // Send the matching enable for cfg1 — this clears the wait gate.
86 sd.dyncfgCh <- dyncfg.NewFunction(functions.Function{
87 UID: "enable-job1",
88 Args: []string{sd.dyncfgJobID(testDiscovererTypeNetListeners, "job1"), "enable"},
89 })
90
91 select {
92 case <-secondSent:
93 case <-time.After(2 * time.Second):
94 t.Fatal("second config did not proceed after wait gate cleared")
95 }
96
97 require.Eventually(t, func() bool {
98 return exposedExistsByKey(sd.exposed, testDiscovererTypeNetListeners+":job1") &&
99 exposedExistsByKey(sd.exposed, testDiscovererTypeNetListeners+":job2")
100 }, time.Second, 10*time.Millisecond)
101 },
102 },
103 }
104
105 for name, tc := range tests {
106 t.Run(name, func(t *testing.T) {
107 sd, confCh, cancel, done := newWaitTestServiceDiscovery(t)
108 stopped := false
109 stop := func() {
110 if stopped {
111 return
112 }
113 stopped = true
114 stopWaitTestServiceDiscovery(t, sd, cancel, done)
115 }
116 defer stop()
117 tc.run(t, sd, confCh, stop)
118 })
119 }
120 }
121
122 func newWaitTestServiceDiscovery(t *testing.T) (*ServiceDiscovery, chan confFile, context.CancelFunc, <-chan struct{}) {
123 t.Helper()
124
125 var out bytes.Buffer
126 confProv := &mockConfigProvider{ch: make(chan confFile)}
127
128 sd := &ServiceDiscovery{
129 Logger: logger.New(),
130 confProv: confProv,
131 pluginName: testPluginName,
132 fnReg: functions.NewManager(),
133 discoverers: testDiscovererRegistry(),
134 dyncfgApi: dyncfg.NewResponder(netdataapi.New(safewriter.New(&out))),
135 seen: dyncfg.NewSeenCache[sdConfig](),
136 exposed: dyncfg.NewExposedCache[sdConfig](),
137 dyncfgCh: make(chan dyncfg.Function, 1),
138 newPipeline: newWaitTestPipeline,
139 runModePolicy: policy.RunModePolicy{},
140 configDefaults: nil,
141 }
142 sd.sdCb = &sdCallbacks{sd: sd}
143 sd.handler = dyncfg.NewHandler(dyncfg.HandlerOpts[sdConfig]{
144 Logger: sd.Logger,
145 API: sd.dyncfgApi,
146 Seen: sd.seen,
147 Exposed: sd.exposed,
148 Callbacks: sd.sdCb,
149 WaitKey: func(cfg sdConfig) string {
150 return cfg.PipelineKey()
151 },
152
153 Path: fmt.Sprintf(dyncfgSDPath, testPluginName),
154 EnableFailCode: 422,
155 JobCommands: []dyncfg.Command{
156 dyncfg.CommandSchema,
157 dyncfg.CommandGet,
158 dyncfg.CommandEnable,
159 dyncfg.CommandDisable,
160 dyncfg.CommandUpdate,
161 dyncfg.CommandTest,
162 dyncfg.CommandUserconfig,
163 },
164 })
165
166 send := func(context.Context, []*confgroup.Group) {}
167 sd.mgr = NewPipelineManager(sd.Logger, sd.newPipeline, send)
168
169 ctx, cancel := context.WithCancel(context.Background())
170 sd.ctx = ctx
171
172 done := make(chan struct{})
173 go func() {
174 defer close(done)
175 sd.run(ctx)
176 }()
177
178 return sd, confProv.ch, cancel, done
179 }
180
181 func stopWaitTestServiceDiscovery(t *testing.T, sd *ServiceDiscovery, cancel context.CancelFunc, done <-chan struct{}) {
182 t.Helper()
183
184 cancel()
185
186 select {
187 case <-done:
188 case <-time.After(5 * time.Second):
189 t.Fatal("service discovery run loop did not stop")
190 }
191
192 sd.mgr.StopAll()
193 }
194
195 func newWaitTestPipeline(cfg pipeline.Config) (sdPipeline, error) {
196 return newTestPipeline(cfg.Name), nil
197 }
198
199 func exposedExistsByKey(cache *dyncfg.ExposedCache[sdConfig], key string) bool {
200 _, ok := cache.LookupByKey(key)
201 return ok
202 }