github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/helper/pluginutils/singleton/singleton_test.go (about)

     1  package singleton
     2  
     3  import (
     4  	"fmt"
     5  	"sync"
     6  	"testing"
     7  	"time"
     8  
     9  	log "github.com/hashicorp/go-hclog"
    10  	plugin "github.com/hashicorp/go-plugin"
    11  	"github.com/hashicorp/nomad/ci"
    12  	"github.com/hashicorp/nomad/helper/pluginutils/loader"
    13  	"github.com/hashicorp/nomad/helper/testlog"
    14  	"github.com/hashicorp/nomad/plugins/base"
    15  	"github.com/stretchr/testify/require"
    16  )
    17  
    18  func harness(t *testing.T) (*SingletonLoader, *loader.MockCatalog) {
    19  	c := &loader.MockCatalog{}
    20  	s := NewSingletonLoader(testlog.HCLogger(t), c)
    21  	return s, c
    22  }
    23  
    24  // Test that multiple dispenses return the same instance
    25  func TestSingleton_Dispense(t *testing.T) {
    26  	ci.Parallel(t)
    27  	require := require.New(t)
    28  
    29  	dispenseCalled := 0
    30  	s, c := harness(t)
    31  	c.DispenseF = func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) {
    32  		p := &base.MockPlugin{}
    33  		i := &loader.MockInstance{
    34  			ExitedF: func() bool { return false },
    35  			PluginF: func() interface{} { return p },
    36  		}
    37  		dispenseCalled++
    38  		return i, nil
    39  	}
    40  
    41  	// Retrieve the plugin many times in parallel
    42  	const count = 128
    43  	var l sync.Mutex
    44  	var wg sync.WaitGroup
    45  	plugins := make(map[interface{}]struct{}, 1)
    46  	waitCh := make(chan struct{})
    47  	for i := 0; i < count; i++ {
    48  		wg.Add(1)
    49  		go func() {
    50  			// Wait for unblock
    51  			<-waitCh
    52  
    53  			// Retrieve the plugin
    54  			p1, err := s.Dispense("foo", "bar", nil, testlog.HCLogger(t))
    55  			require.NotNil(p1)
    56  			require.NoError(err)
    57  			i1 := p1.Plugin()
    58  			require.NotNil(i1)
    59  			l.Lock()
    60  			plugins[i1] = struct{}{}
    61  			l.Unlock()
    62  			wg.Done()
    63  		}()
    64  	}
    65  	time.Sleep(10 * time.Millisecond)
    66  	close(waitCh)
    67  	wg.Wait()
    68  	require.Len(plugins, 1)
    69  	require.Equal(1, dispenseCalled)
    70  }
    71  
    72  // Test that after a plugin is dispensed, if it exits, an error is returned on
    73  // the next dispense
    74  func TestSingleton_Dispense_Exit_Dispense(t *testing.T) {
    75  	ci.Parallel(t)
    76  	require := require.New(t)
    77  
    78  	exited := false
    79  	dispenseCalled := 0
    80  	s, c := harness(t)
    81  	c.DispenseF = func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) {
    82  		p := &base.MockPlugin{}
    83  		i := &loader.MockInstance{
    84  			ExitedF: func() bool { return exited },
    85  			PluginF: func() interface{} { return p },
    86  		}
    87  		dispenseCalled++
    88  		return i, nil
    89  	}
    90  
    91  	// Retrieve the plugin
    92  	logger := testlog.HCLogger(t)
    93  	p1, err := s.Dispense("foo", "bar", nil, logger)
    94  	require.NotNil(p1)
    95  	require.NoError(err)
    96  
    97  	i1 := p1.Plugin()
    98  	require.NotNil(i1)
    99  	require.Equal(1, dispenseCalled)
   100  
   101  	// Mark the plugin as exited and retrieve again
   102  	exited = true
   103  	_, err = s.Dispense("foo", "bar", nil, logger)
   104  	require.Error(err)
   105  	require.Contains(err.Error(), "exited")
   106  	require.Equal(1, dispenseCalled)
   107  
   108  	// Mark the plugin as non-exited and retrieve again
   109  	exited = false
   110  	p2, err := s.Dispense("foo", "bar", nil, logger)
   111  	require.NotNil(p2)
   112  	require.NoError(err)
   113  	require.Equal(2, dispenseCalled)
   114  
   115  	i2 := p2.Plugin()
   116  	require.NotNil(i2)
   117  	if i1 == i2 {
   118  		t.Fatalf("i1 and i2 shouldn't be the same instance: %p vs %p", i1, i2)
   119  	}
   120  }
   121  
   122  // Test that if a plugin errors while being dispensed, the error is returned but
   123  // not saved
   124  func TestSingleton_DispenseError_Dispense(t *testing.T) {
   125  	ci.Parallel(t)
   126  	require := require.New(t)
   127  
   128  	dispenseCalled := 0
   129  	good := func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) {
   130  		p := &base.MockPlugin{}
   131  		i := &loader.MockInstance{
   132  			ExitedF: func() bool { return false },
   133  			PluginF: func() interface{} { return p },
   134  		}
   135  		dispenseCalled++
   136  		return i, nil
   137  	}
   138  
   139  	bad := func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) {
   140  		dispenseCalled++
   141  		return nil, fmt.Errorf("bad")
   142  	}
   143  
   144  	s, c := harness(t)
   145  	c.DispenseF = bad
   146  
   147  	// Retrieve the plugin
   148  	logger := testlog.HCLogger(t)
   149  	p1, err := s.Dispense("foo", "bar", nil, logger)
   150  	require.Nil(p1)
   151  	require.Error(err)
   152  	require.Equal(1, dispenseCalled)
   153  
   154  	// Dispense again and ensure the same error isn't saved
   155  	c.DispenseF = good
   156  	p2, err := s.Dispense("foo", "bar", nil, logger)
   157  	require.NotNil(p2)
   158  	require.NoError(err)
   159  	require.Equal(2, dispenseCalled)
   160  
   161  	i2 := p2.Plugin()
   162  	require.NotNil(i2)
   163  }
   164  
   165  // Test that if a plugin errors while being reattached, the error is returned but
   166  // not saved
   167  func TestSingleton_ReattachError_Dispense(t *testing.T) {
   168  	ci.Parallel(t)
   169  	require := require.New(t)
   170  
   171  	dispenseCalled, reattachCalled := 0, 0
   172  	s, c := harness(t)
   173  	c.DispenseF = func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) {
   174  		p := &base.MockPlugin{}
   175  		i := &loader.MockInstance{
   176  			ExitedF: func() bool { return false },
   177  			PluginF: func() interface{} { return p },
   178  		}
   179  		dispenseCalled++
   180  		return i, nil
   181  	}
   182  	c.ReattachF = func(_, _ string, _ *plugin.ReattachConfig) (loader.PluginInstance, error) {
   183  		reattachCalled++
   184  		return nil, fmt.Errorf("bad")
   185  	}
   186  
   187  	// Retrieve the plugin
   188  	logger := testlog.HCLogger(t)
   189  	p1, err := s.Reattach("foo", "bar", nil)
   190  	require.Nil(p1)
   191  	require.Error(err)
   192  	require.Equal(0, dispenseCalled)
   193  	require.Equal(1, reattachCalled)
   194  
   195  	// Dispense and ensure the same error isn't saved
   196  	p2, err := s.Dispense("foo", "bar", nil, logger)
   197  	require.NotNil(p2)
   198  	require.NoError(err)
   199  	require.Equal(1, dispenseCalled)
   200  	require.Equal(1, reattachCalled)
   201  
   202  	i2 := p2.Plugin()
   203  	require.NotNil(i2)
   204  }
   205  
   206  // Test that after reattaching, dispense returns the same instance
   207  func TestSingleton_Reattach_Dispense(t *testing.T) {
   208  	ci.Parallel(t)
   209  	require := require.New(t)
   210  
   211  	dispenseCalled, reattachCalled := 0, 0
   212  	s, c := harness(t)
   213  	c.DispenseF = func(_, _ string, _ *base.AgentConfig, _ log.Logger) (loader.PluginInstance, error) {
   214  		dispenseCalled++
   215  		return nil, fmt.Errorf("bad")
   216  	}
   217  	c.ReattachF = func(_, _ string, _ *plugin.ReattachConfig) (loader.PluginInstance, error) {
   218  		p := &base.MockPlugin{}
   219  		i := &loader.MockInstance{
   220  			ExitedF: func() bool { return false },
   221  			PluginF: func() interface{} { return p },
   222  		}
   223  		reattachCalled++
   224  		return i, nil
   225  	}
   226  
   227  	// Retrieve the plugin
   228  	logger := testlog.HCLogger(t)
   229  	p1, err := s.Reattach("foo", "bar", nil)
   230  	require.NotNil(p1)
   231  	require.NoError(err)
   232  	require.Equal(0, dispenseCalled)
   233  	require.Equal(1, reattachCalled)
   234  
   235  	i1 := p1.Plugin()
   236  	require.NotNil(i1)
   237  
   238  	// Dispense and ensure the same instance returned
   239  	p2, err := s.Dispense("foo", "bar", nil, logger)
   240  	require.NotNil(p2)
   241  	require.NoError(err)
   242  	require.Equal(0, dispenseCalled)
   243  	require.Equal(1, reattachCalled)
   244  
   245  	i2 := p2.Plugin()
   246  	require.NotNil(i2)
   247  	if i1 != i2 {
   248  		t.Fatalf("i1 and i2 should be the same instance: %p vs %p", i1, i2)
   249  	}
   250  }