github.com/yandex/pandora@v0.5.32/core/engine/engine_test.go (about) 1 package engine 2 3 import ( 4 "context" 5 "reflect" 6 "sync" 7 "testing" 8 "time" 9 10 "github.com/pkg/errors" 11 "github.com/stretchr/testify/assert" 12 "github.com/stretchr/testify/mock" 13 "github.com/stretchr/testify/require" 14 "github.com/yandex/pandora/core" 15 "github.com/yandex/pandora/core/aggregator" 16 "github.com/yandex/pandora/core/config" 17 coremock "github.com/yandex/pandora/core/mocks" 18 "github.com/yandex/pandora/core/provider" 19 "github.com/yandex/pandora/core/schedule" 20 "go.uber.org/atomic" 21 "go.uber.org/zap" 22 "go.uber.org/zap/zapcore" 23 ) 24 25 func Test_ConfigValidation(t *testing.T) { 26 t.Run("dive validation", func(t *testing.T) { 27 conf := Config{ 28 Pools: []InstancePoolConfig{ 29 {}, 30 }, 31 } 32 err := config.Validate(conf) 33 require.Error(t, err) 34 }) 35 t.Run("pools required", func(t *testing.T) { 36 conf := Config{} 37 err := config.Validate(conf) 38 require.Error(t, err) 39 }) 40 } 41 42 func newTestPoolConf() (InstancePoolConfig, *coremock.Gun) { 43 gun := &coremock.Gun{} 44 gun.On("Bind", mock.Anything, mock.Anything).Return(nil) 45 gun.On("Shoot", mock.Anything) 46 conf := InstancePoolConfig{ 47 Provider: provider.NewNum(-1), 48 Aggregator: aggregator.NewTest(), 49 NewGun: func() (core.Gun, error) { 50 return gun, nil 51 }, 52 NewRPSSchedule: func() (core.Schedule, error) { 53 return schedule.NewOnce(1), nil 54 }, 55 StartupSchedule: schedule.NewOnce(1), 56 } 57 return conf, gun 58 } 59 60 func Test_InstancePool(t *testing.T) { 61 var ( 62 gun *coremock.Gun 63 conf InstancePoolConfig 64 ctx context.Context 65 cancel context.CancelFunc 66 67 waitDoneCalled atomic.Bool 68 onWaitDone func() 69 70 p *instancePool 71 ) 72 73 // Conf for starting only instance. 74 var beforeEach = func() { 75 conf, gun = newTestPoolConf() 76 onWaitDone = func() { 77 old := waitDoneCalled.Swap(true) 78 if old { 79 panic("double on wait done call") 80 } 81 } 82 waitDoneCalled.Store(false) 83 ctx, cancel = context.WithCancel(context.Background()) 84 } 85 var justBeforeEach = func(metricPrefix string) { 86 metrics := NewMetrics(metricPrefix) 87 p = newPool(newNopLogger(), metrics, onWaitDone, conf) 88 } 89 _ = cancel 90 91 t.Run("shoot ok", func(t *testing.T) { 92 beforeEach() 93 justBeforeEach("shoot-ok") 94 95 err := p.Run(ctx) 96 require.NoError(t, err) 97 gun.AssertExpectations(t) 98 require.True(t, waitDoneCalled.Load()) 99 }) 100 101 t.Run("context canceled", func(t *testing.T) { 102 var ( 103 blockShoot sync.WaitGroup 104 ) 105 var beforeEachContext = func() { 106 blockShoot.Add(1) 107 prov := &coremock.Provider{} 108 prov.On("Run", mock.Anything, mock.Anything). 109 Return(func(startCtx context.Context, deps core.ProviderDeps) error { 110 <-startCtx.Done() 111 return nil 112 }) 113 prov.On("Acquire").Return(func() (core.Ammo, bool) { 114 cancel() 115 blockShoot.Wait() 116 return struct{}{}, true 117 }) 118 conf.Provider = prov 119 } 120 121 beforeEach() 122 beforeEachContext() 123 justBeforeEach("context-canceled") 124 125 err := p.Run(ctx) 126 require.Equal(t, context.Canceled, err) 127 gun.AssertNotCalled(t, "Shoot") 128 assert.False(t, waitDoneCalled.Load()) 129 blockShoot.Done() 130 131 tick := time.NewTicker(100 * time.Millisecond) 132 i := 0 133 for range tick.C { 134 if waitDoneCalled.Load() { 135 break 136 } 137 if i > 6 { 138 break 139 } 140 i++ 141 } 142 tick.Stop() 143 assert.True(t, waitDoneCalled.Load()) //TODO: eventually 144 }) 145 146 t.Run("provider failed", func(t *testing.T) { 147 beforeEach() 148 149 var ( 150 failErr = errors.New("test err") 151 blockShootAndAggr sync.WaitGroup 152 ) 153 blockShootAndAggr.Add(1) 154 prov := &coremock.Provider{} 155 prov.On("Run", mock.Anything, mock.Anything). 156 Return(func(context.Context, core.ProviderDeps) error { 157 return failErr 158 }) 159 prov.On("Acquire").Return(func() (core.Ammo, bool) { 160 blockShootAndAggr.Wait() 161 return nil, false 162 }) 163 conf.Provider = prov 164 aggr := &coremock.Aggregator{} 165 aggr.On("Run", mock.Anything, mock.Anything). 166 Return(func(context.Context, core.AggregatorDeps) error { 167 blockShootAndAggr.Wait() 168 return nil 169 }) 170 conf.Aggregator = aggr 171 172 justBeforeEach("provider-failed") 173 174 err := p.Run(ctx) 175 require.Error(t, err) 176 require.ErrorContains(t, err, failErr.Error()) 177 gun.AssertNotCalled(t, "Shoot") 178 179 assert.False(t, waitDoneCalled.Load()) 180 blockShootAndAggr.Done() 181 182 tick := time.NewTicker(100 * time.Millisecond) 183 i := 0 184 for range tick.C { 185 if waitDoneCalled.Load() { 186 break 187 } 188 if i > 6 { 189 break 190 } 191 i++ 192 } 193 tick.Stop() 194 assert.True(t, waitDoneCalled.Load()) //TODO: eventually 195 }) 196 197 t.Run("aggregator failed", func(t *testing.T) { 198 beforeEach() 199 failErr := errors.New("test err") 200 aggr := &coremock.Aggregator{} 201 aggr.On("Run", mock.Anything, mock.Anything).Return(failErr) 202 conf.Aggregator = aggr 203 justBeforeEach("aggregator-failed") 204 205 err := p.Run(ctx) 206 require.Error(t, err) 207 require.ErrorContains(t, err, failErr.Error()) 208 tick := time.NewTicker(100 * time.Millisecond) 209 i := 0 210 for range tick.C { 211 if waitDoneCalled.Load() { 212 break 213 } 214 if i > 6 { 215 break 216 } 217 i++ 218 } 219 tick.Stop() 220 assert.True(t, waitDoneCalled.Load()) //TODO: eventually 221 }) 222 223 t.Run("start instances failed", func(t *testing.T) { 224 failErr := errors.New("test err") 225 beforeEach() 226 conf.NewGun = func() (core.Gun, error) { 227 return nil, failErr 228 } 229 justBeforeEach("start-instances-failed") 230 231 err := p.Run(ctx) 232 require.Error(t, err) 233 require.ErrorContains(t, err, failErr.Error()) 234 tick := time.NewTicker(100 * time.Millisecond) 235 i := 0 236 for range tick.C { 237 if waitDoneCalled.Load() { 238 break 239 } 240 if i > 6 { 241 break 242 } 243 i++ 244 } 245 tick.Stop() 246 assert.True(t, waitDoneCalled.Load()) //TODO: eventually 247 }) 248 } 249 250 func Test_MultipleInstance(t *testing.T) { 251 t.Run("out of ammo - instance start is canceled", func(t *testing.T) { 252 conf, _ := newTestPoolConf() 253 conf.Provider = provider.NewNum(3) 254 conf.NewRPSSchedule = func() (core.Schedule, error) { 255 return schedule.NewUnlimited(time.Hour), nil 256 } 257 conf.StartupSchedule = schedule.NewComposite( 258 schedule.NewOnce(2), 259 schedule.NewConst(1, 5*time.Second), 260 ) 261 pool := newPool(newNopLogger(), NewMetrics("test_engine_1"), nil, conf) 262 ctx := context.Background() 263 264 err := pool.Run(ctx) 265 require.NoError(t, err) 266 require.True(t, pool.metrics.InstanceStart.Get() == 3) 267 }) 268 269 t.Run("when provider run done it does not mean out of ammo; instance start is not canceled", func(t *testing.T) { 270 conf, _ := newTestPoolConf() 271 conf.Provider = provider.NewNumBuffered(3) 272 conf.NewRPSSchedule = func() (core.Schedule, error) { 273 return schedule.NewOnce(1), nil 274 } 275 conf.StartupSchedule = schedule.NewOnce(3) 276 pool := newPool(newNopLogger(), NewMetrics("test_engine_2"), nil, conf) 277 ctx := context.Background() 278 279 err := pool.Run(ctx) 280 require.NoError(t, err) 281 require.True(t, pool.metrics.InstanceStart.Get() <= 3) 282 }) 283 284 t.Run("out of RPS - instance start is canceled", func(t *testing.T) { 285 conf, _ := newTestPoolConf() 286 conf.NewRPSSchedule = func() (core.Schedule, error) { 287 return schedule.NewOnce(5), nil 288 } 289 conf.StartupSchedule = schedule.NewComposite( 290 schedule.NewOnce(2), 291 schedule.NewConst(1, 2*time.Second), 292 ) 293 pool := newPool(newNopLogger(), NewMetrics("test_engine_3"), nil, conf) 294 ctx := context.Background() 295 296 err := pool.Run(ctx) 297 require.NoError(t, err) 298 require.True(t, pool.metrics.InstanceStart.Get() <= 3) 299 }) 300 } 301 302 // TODO instance start canceled after out of ammo 303 // TODO instance start cancdled after RPS finish 304 305 func Test_Engine(t *testing.T) { 306 var ( 307 gun1, gun2 *coremock.Gun 308 confs []InstancePoolConfig 309 ctx context.Context 310 cancel context.CancelFunc 311 engine *Engine 312 ) 313 _ = cancel 314 var beforeEach = func() { 315 confs = make([]InstancePoolConfig, 2) 316 confs[0], gun1 = newTestPoolConf() 317 confs[1], gun2 = newTestPoolConf() 318 ctx, cancel = context.WithCancel(context.Background()) 319 } 320 321 var justBeforeEach = func(metricPrefix string) { 322 metrics := NewMetrics(metricPrefix) 323 engine = New(newNopLogger(), metrics, Config{confs}) 324 } 325 326 t.Run("shoot ok", func(t *testing.T) { 327 beforeEach() 328 justBeforeEach("shoot-ok-2") 329 330 err := engine.Run(ctx) 331 require.NoError(t, err) 332 gun1.AssertExpectations(t) 333 gun2.AssertExpectations(t) 334 }) 335 336 t.Run("context canceled", func(t *testing.T) { 337 338 // Cancel context on ammo acquire, an check that engine returns before 339 // instance finish. 340 var ( 341 blockPools sync.WaitGroup 342 ) 343 var beforeEachCtx = func() { 344 blockPools.Add(1) 345 for i := range confs { 346 prov := &coremock.Provider{} 347 prov.On("Run", mock.Anything, mock.Anything). 348 Return(func(startCtx context.Context, deps core.ProviderDeps) error { 349 <-startCtx.Done() 350 blockPools.Wait() 351 return nil 352 }) 353 prov.On("Acquire").Return(func() (core.Ammo, bool) { 354 cancel() 355 blockPools.Wait() 356 return struct{}{}, true 357 }) 358 confs[i].Provider = prov 359 } 360 } 361 beforeEach() 362 beforeEachCtx() 363 justBeforeEach("context-canceled-2") 364 365 err := engine.Run(ctx) 366 require.Equal(t, err, context.Canceled) 367 awaited := make(chan struct{}) 368 go func() { 369 defer close(awaited) 370 engine.Wait() 371 }() 372 373 assert.False(t, IsClosed(awaited)) 374 blockPools.Done() 375 376 tick := time.NewTicker(100 * time.Millisecond) 377 i := 0 378 for range tick.C { 379 if IsClosed(awaited) { 380 break 381 } 382 if i > 6 { 383 break 384 } 385 i++ 386 } 387 tick.Stop() 388 assert.True(t, IsClosed(awaited)) //TODO: eventually 389 }) 390 391 t.Run("one pool failed", func(t *testing.T) { 392 beforeEach() 393 var ( 394 failErr = errors.New("test err") 395 ) 396 aggr := &coremock.Aggregator{} 397 aggr.On("Run", mock.Anything, mock.Anything).Return(failErr) 398 confs[0].Aggregator = aggr 399 400 justBeforeEach("one-pool-failed") 401 402 err := engine.Run(ctx) 403 require.Error(t, err) 404 require.ErrorContains(t, err, failErr.Error()) 405 engine.Wait() 406 }) 407 } 408 409 func Test_BuildInstanceSchedule(t *testing.T) { 410 t.Run("per instance schedule", func(t *testing.T) { 411 conf, _ := newTestPoolConf() 412 conf.RPSPerInstance = true 413 pool := newPool(newNopLogger(), NewMetrics("per-instance-schedule"), nil, conf) 414 newInstanceSchedule, err := pool.buildNewInstanceSchedule(context.Background(), func() { 415 panic("should not be called") 416 }) 417 require.NoError(t, err) 418 419 val1 := reflect.ValueOf(newInstanceSchedule) 420 val2 := reflect.ValueOf(conf.NewRPSSchedule) 421 require.Equal(t, val1.Pointer(), val2.Pointer()) 422 }) 423 424 t.Run("shared schedule create failed", func(t *testing.T) { 425 conf, _ := newTestPoolConf() 426 scheduleCreateErr := errors.New("test err") 427 conf.NewRPSSchedule = func() (core.Schedule, error) { 428 return nil, scheduleCreateErr 429 } 430 pool := newPool(newNopLogger(), NewMetrics("shared-schedule-create-failed"), nil, conf) 431 newInstanceSchedule, err := pool.buildNewInstanceSchedule(context.Background(), func() { 432 panic("should not be called") 433 }) 434 435 require.Error(t, err) 436 require.Equal(t, err, scheduleCreateErr) 437 require.Nil(t, newInstanceSchedule) 438 }) 439 440 t.Run("shared schedule work", func(t *testing.T) { 441 conf, _ := newTestPoolConf() 442 var newScheduleCalled bool 443 conf.NewRPSSchedule = func() (core.Schedule, error) { 444 require.False(t, newScheduleCalled) 445 newScheduleCalled = true 446 return schedule.NewOnce(1), nil 447 } 448 pool := newPool(newNopLogger(), NewMetrics("shared-schedule-work"), nil, conf) 449 ctx, cancel := context.WithCancel(context.Background()) 450 newInstanceSchedule, err := pool.buildNewInstanceSchedule(context.Background(), cancel) 451 require.NoError(t, err) 452 453 schedule, err := newInstanceSchedule() 454 require.NoError(t, err) 455 456 assert.False(t, IsClosed(ctx.Done())) 457 _, ok := schedule.Next() 458 assert.True(t, ok) 459 assert.False(t, IsClosed(ctx.Done())) 460 _, ok = schedule.Next() 461 assert.False(t, ok) 462 assert.True(t, IsClosed(ctx.Done())) 463 }) 464 } 465 466 func IsClosed(actual any) (success bool) { 467 if !isChan(actual) { 468 return false 469 } 470 channelValue := reflect.ValueOf(actual) 471 channelType := reflect.TypeOf(actual) 472 if channelType.ChanDir() == reflect.SendDir { 473 return false 474 } 475 476 winnerIndex, _, open := reflect.Select([]reflect.SelectCase{ 477 {Dir: reflect.SelectRecv, Chan: channelValue}, 478 {Dir: reflect.SelectDefault}, 479 }) 480 481 var closed bool 482 if winnerIndex == 0 { 483 closed = !open 484 } else if winnerIndex == 1 { 485 closed = false 486 } 487 488 return closed 489 } 490 491 func isChan(a interface{}) bool { 492 if isNil(a) { 493 return false 494 } 495 return reflect.TypeOf(a).Kind() == reflect.Chan 496 } 497 498 func isNil(a interface{}) bool { 499 if a == nil { 500 return true 501 } 502 503 switch reflect.TypeOf(a).Kind() { 504 case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: 505 return reflect.ValueOf(a).IsNil() 506 } 507 508 return false 509 } 510 511 type TestLogWriter struct { 512 t *testing.T 513 } 514 515 func (w *TestLogWriter) Write(p []byte) (n int, err error) { 516 w.t.Helper() 517 w.t.Log(string(p)) 518 return len(p), nil 519 } 520 521 func newTestLogger(t *testing.T) *zap.Logger { 522 conf := zap.NewDevelopmentConfig() 523 enc := zapcore.NewConsoleEncoder(conf.EncoderConfig) 524 core := zapcore.NewCore(enc, zapcore.AddSync(&TestLogWriter{t: t}), zap.DebugLevel) 525 log := zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.DPanicLevel)) 526 return log 527 } 528 529 func newNopLogger() *zap.Logger { 530 core := zapcore.NewNopCore() 531 log := zap.New(core) 532 return log 533 }