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