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 }