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

ifname: avoid unpredictable conditions in getMap test #7848

Merged
merged 2 commits into from
Jul 17, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 8 additions & 12 deletions plugins/processors/ifname/ifname.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,7 @@ type IfName struct {

gsBase snmp.GosnmpWrapper `toml:"-"`

sigs sigMap `toml:"-"`
sigsLock sync.Mutex `toml:"-"`
sigs sigMap `toml:"-"`
}

const minRetry time.Duration = 5 * time.Minute
Expand Down Expand Up @@ -251,14 +250,14 @@ func (d *IfName) getMap(agent string) (entry nameMap, age time.Duration, err err
}

// Is this the first request for this agent?
d.sigsLock.Lock()
d.rwLock.Lock()
sig, found := d.sigs[agent]
if !found {
s := make(chan struct{})
d.sigs[agent] = s
sig = s
}
d.sigsLock.Unlock()
d.rwLock.Unlock()

if found {
// This is not the first request. Wait for first to finish.
Expand All @@ -281,24 +280,21 @@ func (d *IfName) getMap(agent string) (entry nameMap, age time.Duration, err err
m, err = d.getMapRemote(agent)
if err != nil {
//failure. signal without saving to cache
d.sigsLock.Lock()
d.rwLock.Lock()
close(sig)
delete(d.sigs, agent)
d.sigsLock.Unlock()
d.rwLock.Unlock()

return nil, 0, fmt.Errorf("getting remote table: %w", err)
}

// Cache it
// Cache it, then signal any other waiting requests for this agent
// and clean up
d.rwLock.Lock()
d.cache.Put(agent, m)
d.rwLock.Unlock()

// Signal any other waiting requests for this agent and clean up
d.sigsLock.Lock()
close(sig)
delete(d.sigs, agent)
d.sigsLock.Unlock()
d.rwLock.Unlock()

return m, 0, nil
}
Expand Down
51 changes: 16 additions & 35 deletions plugins/processors/ifname/ifname_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package ifname

import (
"sync"
"sync/atomic"
"testing"
"time"
Expand All @@ -26,7 +25,7 @@ func TestTable(t *testing.T) {

config := snmp.ClientConfig{
Version: 2,
Timeout: internal.Duration{Duration: 5 * time.Second}, // doesn't work with 0 timeout
Timeout: internal.Duration{Duration: 5 * time.Second}, // Doesn't work with 0 timeout
}
gs, err := snmp.NewWrapper(config)
require.NoError(t, err)
Expand All @@ -36,7 +35,7 @@ func TestTable(t *testing.T) {
err = gs.Connect()
require.NoError(t, err)

//could use ifIndex but oid index is always the same
// Could use ifIndex but oid index is always the same
m, err := buildMap(gs, tab, "ifDescr")
require.NoError(t, err)
require.NotEmpty(t, m)
Expand All @@ -53,7 +52,7 @@ func TestIfName(t *testing.T) {
CacheSize: 1000,
ClientConfig: snmp.ClientConfig{
Version: 2,
Timeout: internal.Duration{Duration: 5 * time.Second}, // doesn't work with 0 timeout
Timeout: internal.Duration{Duration: 5 * time.Second}, // Doesn't work with 0 timeout
},
}
err := d.Init()
Expand Down Expand Up @@ -93,65 +92,47 @@ func TestIfName(t *testing.T) {

func TestGetMap(t *testing.T) {
d := IfName{
SourceTag: "ifIndex",
DestTag: "ifName",
AgentTag: "agent",
CacheSize: 1000,
ClientConfig: snmp.ClientConfig{
Version: 2,
Timeout: internal.Duration{Duration: 5 * time.Second}, // doesn't work with 0 timeout
},
CacheTTL: config.Duration(10 * time.Second),
CacheTTL: config.Duration(10 * time.Second),
}

// This test mocks the snmp transaction so don't run net-snmp
// commands to look up table names.
// Don't run net-snmp commands to look up table names.
d.makeTable = func(agent string) (*si.Table, error) {
return &si.Table{}, nil
}
err := d.Init()
require.NoError(t, err)

// Request the same agent multiple times in goroutines. The first
// request should make the mocked remote call and the others
// should block until the response is cached, then return the
// cached response.

expected := nameMap{
1: "ifname1",
2: "ifname2",
}

var wgRemote sync.WaitGroup
var remoteCalls int32

wgRemote.Add(1)
// Mock the snmp transaction
d.getMapRemote = func(agent string) (nameMap, error) {
atomic.AddInt32(&remoteCalls, 1)
wgRemote.Wait() //don't return until all requests are made
return expected, nil
}
m, age, err := d.getMap("agent")
require.NoError(t, err)
require.Zero(t, age) // Age is zero when map comes from getMapRemote
require.Equal(t, expected, m)

const thMax = 3
var wgReq sync.WaitGroup
// Remote call should happen the first time getMap runs
require.Equal(t, int32(1), remoteCalls)

const thMax = 3
for th := 0; th < thMax; th++ {
wgReq.Add(1)
go func() {
defer wgReq.Done()
m, _, err := d.getMap("agent")
m, age, err := d.getMap("agent")
require.NoError(t, err)
require.NotZero(t, age) // Age is nonzero when map comes from cache
require.Equal(t, expected, m)
}()
}

//signal mocked remote call to finish
wgRemote.Done()

//wait for requests to finish
wgReq.Wait()

//remote call should only happen once
// Remote call should not happen subsequent times getMap runs
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like you still need a waitgroup for these goroutines, otherwise it's possible they haven't event started running yet when the function ends.

require.Equal(t, int32(1), remoteCalls)

}