Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
49 changes: 37 additions & 12 deletions cni/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ import (
cniTypes "github.com/containernetworking/cni/pkg/types"
cniTypesCurr "github.com/containernetworking/cni/pkg/types/current"
cniVers "github.com/containernetworking/cni/pkg/version"
"github.com/pkg/errors"
)

var errEmptyContent = errors.New("read content is zero bytes")

// Plugin is the parent class for CNI plugins.
type Plugin struct {
*common.Plugin
Expand Down Expand Up @@ -197,26 +200,33 @@ func (plugin *Plugin) IsSafeToRemoveLock(processName string) (bool, error) {
return false, cmdErr
}

// Read pid from lockfile
lockFileName := plugin.Store.GetLockFileName()
content, err := ioutil.ReadFile(lockFileName)
// Read pid from lockfile
lockFilePid, err := plugin.readLockFile(lockFileName)
if err != nil {
log.Errorf("Failed to read lock file :%v, ", err)
return false, err
}

if len(content) <= 0 {
log.Errorf("Num bytes read from lock file is 0")
return false, fmt.Errorf("Num bytes read from lock file is 0")
return false, errors.Wrap(err, "IsSafeToRemoveLock lockfile read failed")
}

log.Printf("Read from Lock file:%s", content)
log.Printf("Read from lockfile:%s", lockFilePid)
// Get the process name if running and
// check if that matches with our expected process
pName, err := platform.GetProcessNameByID(string(content))
// if it returns non-nil error then process is not running
pName, err := platform.GetProcessNameByID(lockFilePid)
if err != nil {
var content string
content, err = plugin.readLockFile(lockFileName)
if err != nil {
return false, errors.Wrap(err, "IsSafeToRemoveLock lockfile 2nd read failed")
}

// pid in lockfile changed after getprocessnamebyid call. so some other process acquired lockfile in between.
// so its not safe to remove lockfile
if string(content) != lockFilePid {
log.Printf("Lockfile content changed from %s to %s. So not safe to remove lockfile", lockFilePid, content)
return false, nil
}

return true, nil
// if process id changed. read lockfile?
}

log.Printf("[CNI] Process name is %s", pName)
Expand All @@ -229,3 +239,18 @@ func (plugin *Plugin) IsSafeToRemoveLock(processName string) (bool, error) {
log.Errorf("Plugin store is nil")
return false, fmt.Errorf("plugin store nil")
}

func (plugin *Plugin) readLockFile(filename string) (string, error) {
content, err := ioutil.ReadFile(filename)
if err != nil {
log.Errorf("Failed to read lockfile :%v", err)
return "", fmt.Errorf("readLockFile error:%w", err)
}

if len(content) == 0 {
log.Errorf("Num bytes read from lockfile is 0")
return "", errEmptyContent
}

return string(content), nil
}
69 changes: 69 additions & 0 deletions cni/plugin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package cni

import (
"os"
"testing"

"github.com/Azure/azure-container-networking/common"
"github.com/Azure/azure-container-networking/store"
"github.com/stretchr/testify/require"
)

func TestMain(m *testing.M) {
// Run tests.
exitCode := m.Run()
os.Exit(exitCode)
}

func TestPluginSafeToRemoveLock(t *testing.T) {
tests := []struct {
name string
plugin Plugin
processName string
wantIsSafe bool
wantErr bool
}{
{
name: "Safe to remove lock-true. Process name does not match",
plugin: Plugin{
Plugin: &common.Plugin{
Name: "cni",
Version: "0.3.0",
Store: store.NewMockStore("testfiles/processfound.lock"),
},
version: "0.3.0",
},
processName: "azure-vnet",
wantIsSafe: true,
wantErr: false,
},
{
name: "Safe to remove lock-true. Process not running",
plugin: Plugin{
Plugin: &common.Plugin{
Name: "cni",
Version: "0.3.0",
Store: store.NewMockStore("testfiles/processnotfound.lock"),
},
version: "0.3.0",
},
processName: "azure-vnet",
wantIsSafe: true,
wantErr: false,
},
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
isSafe, err := tt.plugin.IsSafeToRemoveLock(tt.processName)
if tt.wantErr {
require.Error(t, err)
require.Equal(t, tt.wantIsSafe, isSafe)
} else {
require.NoError(t, err)
require.Equal(t, tt.wantIsSafe, isSafe)
}
})
}
}
1 change: 1 addition & 0 deletions cni/testfiles/processfound.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
1 change: 1 addition & 0 deletions cni/testfiles/processnotfound.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-3
2 changes: 1 addition & 1 deletion store/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const (
lockExtension = ".lock"

// Maximum number of retries before failing a lock call.
lockMaxRetries = 200
lockMaxRetries = 100
Copy link
Collaborator

Choose a reason for hiding this comment

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

how quickly do we retry? this could go by very fast. should there be some exponential back-off, maybe?

Copy link
Member Author

Choose a reason for hiding this comment

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

Isn't 10 seconds long enough? 100 retries with 100ms delay

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't know. Usually (like with http calls), we would do an exponential back-off where we wait maybe twice as long every time instead of a fixed interval (ex: 50ms, 100, 200, 400, 800, ...). But if 100x100ms is what we want, that's fine with me


// Delay between lock retries.
lockRetryDelay = 100 * time.Millisecond
Expand Down
51 changes: 51 additions & 0 deletions store/mockstore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package store

import (
"time"
)

type mockStore struct {
lockFilePath string
}

// NewMockStore creates a new jsonFileStore object, accessed as a KeyValueStore.
func NewMockStore(lockFilePath string) KeyValueStore {
return &mockStore{
lockFilePath: lockFilePath,
}
}

// Read restores the value for the given key from persistent store.
func (ms *mockStore) Read(key string, value interface{}) error {
return nil
}

func (ms *mockStore) Write(key string, value interface{}) error {
return nil
}

func (ms *mockStore) Flush() error {
return nil
}

func (ms *mockStore) Lock(block bool) error {
return nil
}

func (ms *mockStore) Unlock(forceUnlock bool) error {
return nil
}

func (ms *mockStore) GetModificationTime() (time.Time, error) {
return time.Time{}, nil
}

func (ms *mockStore) GetLockFileModificationTime() (time.Time, error) {
return time.Time{}, nil
}

func (ms *mockStore) GetLockFileName() string {
return ms.lockFilePath
}

func (ms *mockStore) Remove() {}