github.com/hernad/nomad@v1.6.112/helper/pluginutils/singleton/singleton_test.go (about)

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