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  }