2023-04-10 15:36:59 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
2018-09-18 17:08:46 +00:00
|
|
|
package singleton
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"sync"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
log "github.com/hashicorp/go-hclog"
|
|
|
|
plugin "github.com/hashicorp/go-plugin"
|
2022-03-15 12:42:43 +00:00
|
|
|
"github.com/hashicorp/nomad/ci"
|
2019-01-23 14:27:14 +00:00
|
|
|
"github.com/hashicorp/nomad/helper/pluginutils/loader"
|
2018-09-18 17:08:46 +00:00
|
|
|
"github.com/hashicorp/nomad/helper/testlog"
|
|
|
|
"github.com/hashicorp/nomad/plugins/base"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
)
|
|
|
|
|
|
|
|
func harness(t *testing.T) (*SingletonLoader, *loader.MockCatalog) {
|
|
|
|
c := &loader.MockCatalog{}
|
|
|
|
s := NewSingletonLoader(testlog.HCLogger(t), c)
|
|
|
|
return s, c
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test that multiple dispenses return the same instance
|
|
|
|
func TestSingleton_Dispense(t *testing.T) {
|
2022-03-15 12:42:43 +00:00
|
|
|
ci.Parallel(t)
|
2018-09-18 17:08:46 +00:00
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
dispenseCalled := 0
|
|
|
|
s, c := harness(t)
|
2018-12-18 00:40:58 +00:00
|
|
|
c.DispenseF = func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) {
|
2018-09-18 17:08:46 +00:00
|
|
|
p := &base.MockPlugin{}
|
|
|
|
i := &loader.MockInstance{
|
|
|
|
ExitedF: func() bool { return false },
|
|
|
|
PluginF: func() interface{} { return p },
|
|
|
|
}
|
|
|
|
dispenseCalled++
|
|
|
|
return i, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Retrieve the plugin many times in parallel
|
|
|
|
const count = 128
|
|
|
|
var l sync.Mutex
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
plugins := make(map[interface{}]struct{}, 1)
|
|
|
|
waitCh := make(chan struct{})
|
|
|
|
for i := 0; i < count; i++ {
|
|
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
|
|
// Wait for unblock
|
|
|
|
<-waitCh
|
|
|
|
|
|
|
|
// Retrieve the plugin
|
2018-10-17 02:21:15 +00:00
|
|
|
p1, err := s.Dispense("foo", "bar", nil, testlog.HCLogger(t))
|
2018-09-18 17:08:46 +00:00
|
|
|
require.NotNil(p1)
|
|
|
|
require.NoError(err)
|
|
|
|
i1 := p1.Plugin()
|
|
|
|
require.NotNil(i1)
|
|
|
|
l.Lock()
|
|
|
|
plugins[i1] = struct{}{}
|
|
|
|
l.Unlock()
|
|
|
|
wg.Done()
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
close(waitCh)
|
|
|
|
wg.Wait()
|
|
|
|
require.Len(plugins, 1)
|
|
|
|
require.Equal(1, dispenseCalled)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test that after a plugin is dispensed, if it exits, an error is returned on
|
|
|
|
// the next dispense
|
|
|
|
func TestSingleton_Dispense_Exit_Dispense(t *testing.T) {
|
2022-03-15 12:42:43 +00:00
|
|
|
ci.Parallel(t)
|
2018-09-18 17:08:46 +00:00
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
exited := false
|
|
|
|
dispenseCalled := 0
|
|
|
|
s, c := harness(t)
|
2018-12-18 00:40:58 +00:00
|
|
|
c.DispenseF = func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) {
|
2018-09-18 17:08:46 +00:00
|
|
|
p := &base.MockPlugin{}
|
|
|
|
i := &loader.MockInstance{
|
|
|
|
ExitedF: func() bool { return exited },
|
|
|
|
PluginF: func() interface{} { return p },
|
|
|
|
}
|
|
|
|
dispenseCalled++
|
|
|
|
return i, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Retrieve the plugin
|
|
|
|
logger := testlog.HCLogger(t)
|
2018-10-17 02:21:15 +00:00
|
|
|
p1, err := s.Dispense("foo", "bar", nil, logger)
|
2018-09-18 17:08:46 +00:00
|
|
|
require.NotNil(p1)
|
|
|
|
require.NoError(err)
|
|
|
|
|
|
|
|
i1 := p1.Plugin()
|
|
|
|
require.NotNil(i1)
|
|
|
|
require.Equal(1, dispenseCalled)
|
|
|
|
|
|
|
|
// Mark the plugin as exited and retrieve again
|
|
|
|
exited = true
|
2018-10-17 02:21:15 +00:00
|
|
|
_, err = s.Dispense("foo", "bar", nil, logger)
|
2018-09-18 17:08:46 +00:00
|
|
|
require.Error(err)
|
|
|
|
require.Contains(err.Error(), "exited")
|
|
|
|
require.Equal(1, dispenseCalled)
|
|
|
|
|
|
|
|
// Mark the plugin as non-exited and retrieve again
|
|
|
|
exited = false
|
2018-10-17 02:21:15 +00:00
|
|
|
p2, err := s.Dispense("foo", "bar", nil, logger)
|
2018-09-18 17:08:46 +00:00
|
|
|
require.NotNil(p2)
|
|
|
|
require.NoError(err)
|
|
|
|
require.Equal(2, dispenseCalled)
|
|
|
|
|
|
|
|
i2 := p2.Plugin()
|
|
|
|
require.NotNil(i2)
|
|
|
|
if i1 == i2 {
|
|
|
|
t.Fatalf("i1 and i2 shouldn't be the same instance: %p vs %p", i1, i2)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test that if a plugin errors while being dispensed, the error is returned but
|
|
|
|
// not saved
|
|
|
|
func TestSingleton_DispenseError_Dispense(t *testing.T) {
|
2022-03-15 12:42:43 +00:00
|
|
|
ci.Parallel(t)
|
2018-09-18 17:08:46 +00:00
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
dispenseCalled := 0
|
2018-12-18 00:40:58 +00:00
|
|
|
good := func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) {
|
2018-09-18 17:08:46 +00:00
|
|
|
p := &base.MockPlugin{}
|
|
|
|
i := &loader.MockInstance{
|
|
|
|
ExitedF: func() bool { return false },
|
|
|
|
PluginF: func() interface{} { return p },
|
|
|
|
}
|
|
|
|
dispenseCalled++
|
|
|
|
return i, nil
|
|
|
|
}
|
|
|
|
|
2018-12-18 00:40:58 +00:00
|
|
|
bad := func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) {
|
2018-09-18 17:08:46 +00:00
|
|
|
dispenseCalled++
|
|
|
|
return nil, fmt.Errorf("bad")
|
|
|
|
}
|
|
|
|
|
|
|
|
s, c := harness(t)
|
|
|
|
c.DispenseF = bad
|
|
|
|
|
|
|
|
// Retrieve the plugin
|
|
|
|
logger := testlog.HCLogger(t)
|
2018-10-17 02:21:15 +00:00
|
|
|
p1, err := s.Dispense("foo", "bar", nil, logger)
|
2018-09-18 17:08:46 +00:00
|
|
|
require.Nil(p1)
|
|
|
|
require.Error(err)
|
|
|
|
require.Equal(1, dispenseCalled)
|
|
|
|
|
|
|
|
// Dispense again and ensure the same error isn't saved
|
|
|
|
c.DispenseF = good
|
2018-10-17 02:21:15 +00:00
|
|
|
p2, err := s.Dispense("foo", "bar", nil, logger)
|
2018-09-18 17:08:46 +00:00
|
|
|
require.NotNil(p2)
|
|
|
|
require.NoError(err)
|
|
|
|
require.Equal(2, dispenseCalled)
|
|
|
|
|
|
|
|
i2 := p2.Plugin()
|
|
|
|
require.NotNil(i2)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test that if a plugin errors while being reattached, the error is returned but
|
|
|
|
// not saved
|
|
|
|
func TestSingleton_ReattachError_Dispense(t *testing.T) {
|
2022-03-15 12:42:43 +00:00
|
|
|
ci.Parallel(t)
|
2018-09-18 17:08:46 +00:00
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
dispenseCalled, reattachCalled := 0, 0
|
|
|
|
s, c := harness(t)
|
2018-12-18 00:40:58 +00:00
|
|
|
c.DispenseF = func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) {
|
2018-09-18 17:08:46 +00:00
|
|
|
p := &base.MockPlugin{}
|
|
|
|
i := &loader.MockInstance{
|
|
|
|
ExitedF: func() bool { return false },
|
|
|
|
PluginF: func() interface{} { return p },
|
|
|
|
}
|
|
|
|
dispenseCalled++
|
|
|
|
return i, nil
|
|
|
|
}
|
|
|
|
c.ReattachF = func(_, _ string, _ *plugin.ReattachConfig) (loader.PluginInstance, error) {
|
|
|
|
reattachCalled++
|
|
|
|
return nil, fmt.Errorf("bad")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Retrieve the plugin
|
|
|
|
logger := testlog.HCLogger(t)
|
|
|
|
p1, err := s.Reattach("foo", "bar", nil)
|
|
|
|
require.Nil(p1)
|
|
|
|
require.Error(err)
|
|
|
|
require.Equal(0, dispenseCalled)
|
|
|
|
require.Equal(1, reattachCalled)
|
|
|
|
|
|
|
|
// Dispense and ensure the same error isn't saved
|
2018-10-17 02:21:15 +00:00
|
|
|
p2, err := s.Dispense("foo", "bar", nil, logger)
|
2018-09-18 17:08:46 +00:00
|
|
|
require.NotNil(p2)
|
|
|
|
require.NoError(err)
|
|
|
|
require.Equal(1, dispenseCalled)
|
|
|
|
require.Equal(1, reattachCalled)
|
|
|
|
|
|
|
|
i2 := p2.Plugin()
|
|
|
|
require.NotNil(i2)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test that after reattaching, dispense returns the same instance
|
|
|
|
func TestSingleton_Reattach_Dispense(t *testing.T) {
|
2022-03-15 12:42:43 +00:00
|
|
|
ci.Parallel(t)
|
2018-09-18 17:08:46 +00:00
|
|
|
require := require.New(t)
|
|
|
|
|
|
|
|
dispenseCalled, reattachCalled := 0, 0
|
|
|
|
s, c := harness(t)
|
2018-12-18 00:40:58 +00:00
|
|
|
c.DispenseF = func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) {
|
2018-09-18 17:08:46 +00:00
|
|
|
dispenseCalled++
|
|
|
|
return nil, fmt.Errorf("bad")
|
|
|
|
}
|
|
|
|
c.ReattachF = func(_, _ string, _ *plugin.ReattachConfig) (loader.PluginInstance, error) {
|
|
|
|
p := &base.MockPlugin{}
|
|
|
|
i := &loader.MockInstance{
|
|
|
|
ExitedF: func() bool { return false },
|
|
|
|
PluginF: func() interface{} { return p },
|
|
|
|
}
|
|
|
|
reattachCalled++
|
|
|
|
return i, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Retrieve the plugin
|
|
|
|
logger := testlog.HCLogger(t)
|
|
|
|
p1, err := s.Reattach("foo", "bar", nil)
|
|
|
|
require.NotNil(p1)
|
|
|
|
require.NoError(err)
|
|
|
|
require.Equal(0, dispenseCalled)
|
|
|
|
require.Equal(1, reattachCalled)
|
|
|
|
|
|
|
|
i1 := p1.Plugin()
|
|
|
|
require.NotNil(i1)
|
|
|
|
|
|
|
|
// Dispense and ensure the same instance returned
|
2018-10-17 02:21:15 +00:00
|
|
|
p2, err := s.Dispense("foo", "bar", nil, logger)
|
2018-09-18 17:08:46 +00:00
|
|
|
require.NotNil(p2)
|
|
|
|
require.NoError(err)
|
|
|
|
require.Equal(0, dispenseCalled)
|
|
|
|
require.Equal(1, reattachCalled)
|
|
|
|
|
|
|
|
i2 := p2.Plugin()
|
|
|
|
require.NotNil(i2)
|
|
|
|
if i1 != i2 {
|
|
|
|
t.Fatalf("i1 and i2 should be the same instance: %p vs %p", i1, i2)
|
|
|
|
}
|
|
|
|
}
|