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 }