github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/client/session_aggregate_test.go (about)

     1  // Copyright (c) 2016 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package client
    22  
    23  import (
    24  	"fmt"
    25  	"sync/atomic"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/m3db/m3/src/dbnode/generated/thrift/rpc"
    30  	"github.com/m3db/m3/src/dbnode/storage/index"
    31  	"github.com/m3db/m3/src/dbnode/topology"
    32  	"github.com/m3db/m3/src/m3ninx/idx"
    33  	xerrors "github.com/m3db/m3/src/x/errors"
    34  	"github.com/m3db/m3/src/x/ident"
    35  	"github.com/m3db/m3/src/x/instrument"
    36  	xretry "github.com/m3db/m3/src/x/retry"
    37  	xtest "github.com/m3db/m3/src/x/test"
    38  	xtime "github.com/m3db/m3/src/x/time"
    39  
    40  	"github.com/golang/mock/gomock"
    41  	"github.com/stretchr/testify/assert"
    42  	"github.com/stretchr/testify/require"
    43  )
    44  
    45  var (
    46  	testSessionAggregateQuery     = index.Query{idx.NewTermQuery([]byte("a"), []byte("b"))}
    47  	testSessionAggregateQueryOpts = func(t0, t1 xtime.UnixNano) index.AggregationOptions {
    48  		return index.AggregationOptions{
    49  			QueryOptions: index.QueryOptions{StartInclusive: t0, EndExclusive: t1},
    50  			Type:         index.AggregateTagNamesAndValues,
    51  		}
    52  	}
    53  )
    54  
    55  func TestSessionAggregateUnsupportedQuery(t *testing.T) {
    56  	ctrl := gomock.NewController(xtest.Reporter{t})
    57  	defer ctrl.Finish()
    58  
    59  	opts := newSessionTestOptions().
    60  		SetFetchRetrier(xretry.NewRetrier(xretry.NewOptions().SetMaxRetries(1)))
    61  
    62  	s, err := newSession(opts)
    63  	assert.NoError(t, err)
    64  
    65  	session, ok := s.(*session)
    66  	assert.True(t, ok)
    67  
    68  	mockHostQueues(ctrl, session, sessionTestReplicas, nil)
    69  	assert.NoError(t, session.Open())
    70  
    71  	leakPool := injectLeakcheckAggregateAttempPool(session)
    72  	_, _, err = s.FetchTagged(testContext(),
    73  		ident.StringID("namespace"),
    74  		index.Query{},
    75  		index.QueryOptions{})
    76  	assert.Error(t, err)
    77  	assert.True(t, xerrors.IsNonRetryableError(err))
    78  	leakPool.Check(t)
    79  
    80  	_, _, err = s.FetchTaggedIDs(testContext(),
    81  		ident.StringID("namespace"),
    82  		index.Query{},
    83  		index.QueryOptions{})
    84  	assert.Error(t, err)
    85  	assert.True(t, xerrors.IsNonRetryableError(err))
    86  	leakPool.Check(t)
    87  
    88  	assert.NoError(t, session.Close())
    89  }
    90  
    91  func TestSessionAggregateNotOpenError(t *testing.T) {
    92  	ctrl := gomock.NewController(t)
    93  	defer ctrl.Finish()
    94  
    95  	opts := newSessionTestOptions()
    96  	s, err := newSession(opts)
    97  	assert.NoError(t, err)
    98  	t0 := xtime.Now()
    99  
   100  	_, _, err = s.Aggregate(testContext(), ident.StringID("namespace"),
   101  		testSessionAggregateQuery, testSessionAggregateQueryOpts(t0, t0))
   102  	assert.Error(t, err)
   103  	assert.Equal(t, ErrSessionStatusNotOpen, err)
   104  }
   105  
   106  func TestSessionAggregateGuardAgainstInvalidCall(t *testing.T) {
   107  	ctrl := gomock.NewController(t)
   108  	defer ctrl.Finish()
   109  
   110  	opts := newSessionTestOptions()
   111  	s, err := newSession(opts)
   112  	assert.NoError(t, err)
   113  	session := s.(*session)
   114  
   115  	start := xtime.Now().Truncate(time.Hour)
   116  	end := start.Add(2 * time.Hour)
   117  
   118  	mockHostQueues(ctrl, session, sessionTestReplicas, []testEnqueueFn{
   119  		func(idx int, op op) {
   120  			go func() {
   121  				op.CompletionFn()(nil, nil)
   122  			}()
   123  		},
   124  	})
   125  
   126  	assert.NoError(t, session.Open())
   127  
   128  	_, _, err = session.Aggregate(testContext(), ident.StringID("namespace"),
   129  		testSessionAggregateQuery, testSessionAggregateQueryOpts(start, end))
   130  	assert.Error(t, err)
   131  	assert.NoError(t, session.Close())
   132  }
   133  
   134  func TestSessionAggregateGuardAgainstNilHost(t *testing.T) {
   135  	ctrl := gomock.NewController(t)
   136  	defer ctrl.Finish()
   137  
   138  	opts := newSessionTestOptions()
   139  	s, err := newSession(opts)
   140  	assert.NoError(t, err)
   141  	session := s.(*session)
   142  
   143  	start := xtime.Now().Truncate(time.Hour)
   144  	end := start.Add(2 * time.Hour)
   145  
   146  	mockHostQueues(ctrl, session, sessionTestReplicas, []testEnqueueFn{
   147  		func(idx int, op op) {
   148  			go func() {
   149  				op.CompletionFn()(aggregateResultAccumulatorOpts{}, nil)
   150  			}()
   151  		},
   152  	})
   153  
   154  	assert.NoError(t, session.Open())
   155  
   156  	_, _, err = session.Aggregate(testContext(), ident.StringID("namespace"),
   157  		testSessionAggregateQuery, testSessionAggregateQueryOpts(start, end))
   158  	assert.Error(t, err)
   159  	assert.NoError(t, session.Close())
   160  }
   161  
   162  func TestSessionAggregateGuardAgainstInvalidHost(t *testing.T) {
   163  	ctrl := gomock.NewController(t)
   164  	defer ctrl.Finish()
   165  
   166  	opts := newSessionTestOptions()
   167  	s, err := newSession(opts)
   168  	assert.NoError(t, err)
   169  	session := s.(*session)
   170  
   171  	start := xtime.Now().Truncate(time.Hour)
   172  	end := start.Add(2 * time.Hour)
   173  
   174  	host := topology.NewHost("some-random-host", "some-random-host:12345")
   175  	mockHostQueues(ctrl, session, sessionTestReplicas, []testEnqueueFn{
   176  		func(idx int, op op) {
   177  			go func() {
   178  				op.CompletionFn()(aggregateResultAccumulatorOpts{host: host}, nil)
   179  			}()
   180  		},
   181  	})
   182  
   183  	assert.NoError(t, session.Open())
   184  
   185  	_, _, err = session.Aggregate(testContext(), ident.StringID("namespace"),
   186  		testSessionAggregateQuery, testSessionAggregateQueryOpts(start, end))
   187  	assert.Error(t, err)
   188  	assert.NoError(t, session.Close())
   189  }
   190  
   191  func TestSessionAggregateIDsBadRequestErrorIsNonRetryable(t *testing.T) {
   192  	ctrl := gomock.NewController(t)
   193  	defer ctrl.Finish()
   194  
   195  	opts := newSessionTestOptions()
   196  	s, err := newSession(opts)
   197  	assert.NoError(t, err)
   198  	session := s.(*session)
   199  
   200  	start := xtime.Now().Truncate(time.Hour)
   201  	end := start.Add(2 * time.Hour)
   202  
   203  	topoInit := opts.TopologyInitializer()
   204  	topoWatch, err := topoInit.Init()
   205  	require.NoError(t, err)
   206  	topoMap := topoWatch.Get()
   207  	require.True(t, topoMap.HostsLen() > 0)
   208  
   209  	mockHostQueues(ctrl, session, sessionTestReplicas, []testEnqueueFn{
   210  		func(idx int, op op) {
   211  			go func() {
   212  				host := topoMap.Hosts()[idx]
   213  				op.CompletionFn()(aggregateResultAccumulatorOpts{host: host}, &rpc.Error{
   214  					Type:    rpc.ErrorType_BAD_REQUEST,
   215  					Message: "expected bad request error",
   216  				})
   217  			}()
   218  		},
   219  	})
   220  
   221  	assert.NoError(t, session.Open())
   222  	// NB: stubbing needs to be done after session.Open
   223  	leakStatePool := injectLeakcheckFetchStatePool(session)
   224  	leakOpPool := injectLeakcheckAggregateOpPool(session)
   225  
   226  	_, _, err = session.Aggregate(testContext(), ident.StringID("namespace"),
   227  		testSessionAggregateQuery, testSessionAggregateQueryOpts(start, end))
   228  	assert.Error(t, err)
   229  	assert.NoError(t, session.Close())
   230  
   231  	numStateAllocs := 0
   232  	leakStatePool.CheckExtended(t, func(e leakcheckFetchState) {
   233  		require.Equal(t, int32(0), atomic.LoadInt32(&e.Value.refCounter.n), string(e.GetStacktrace))
   234  		numStateAllocs++
   235  	})
   236  	require.Equal(t, 1, numStateAllocs)
   237  
   238  	numOpAllocs := 0
   239  	leakOpPool.CheckExtended(t, func(e leakcheckAggregateOp) {
   240  		require.Equal(t, int32(0), atomic.LoadInt32(&e.Value.refCounter.n), string(e.GetStacktrace))
   241  		numOpAllocs++
   242  	})
   243  	require.Equal(t, 1, numOpAllocs)
   244  }
   245  
   246  func TestSessionAggregateIDsEnqueueErr(t *testing.T) {
   247  	ctrl := gomock.NewController(t)
   248  	defer ctrl.Finish()
   249  
   250  	opts := newSessionTestOptions()
   251  	s, err := newSession(opts)
   252  	assert.NoError(t, err)
   253  	session := s.(*session)
   254  
   255  	start := xtime.Now().Truncate(time.Hour)
   256  	end := start.Add(2 * time.Hour)
   257  
   258  	require.Equal(t, 3, sessionTestReplicas) // the code below assumes this
   259  	mockExtendedHostQueues(
   260  		t, ctrl, session, sessionTestReplicas,
   261  		testHostQueueOpsByHost{
   262  			testHostName(0): &testHostQueueOps{
   263  				enqueues: []testEnqueue{
   264  					{
   265  						enqueueFn: func(idx int, op op) {},
   266  					},
   267  				},
   268  			},
   269  			testHostName(1): &testHostQueueOps{
   270  				enqueues: []testEnqueue{
   271  					{
   272  						enqueueFn: func(idx int, op op) {},
   273  					},
   274  				},
   275  			},
   276  			testHostName(2): &testHostQueueOps{
   277  				enqueues: []testEnqueue{
   278  					{
   279  						enqueueErr: fmt.Errorf("random-error"),
   280  					},
   281  				},
   282  			},
   283  		})
   284  
   285  	assert.NoError(t, session.Open())
   286  
   287  	defer instrument.SetShouldPanicEnvironmentVariable(true)()
   288  	require.Panics(t, func() {
   289  		_, _, _ = session.Aggregate(testContext(), ident.StringID("namespace"),
   290  			testSessionAggregateQuery, testSessionAggregateQueryOpts(start, end))
   291  	})
   292  }
   293  
   294  func TestSessionAggregateMergeTest(t *testing.T) {
   295  	ctrl := gomock.NewController(t)
   296  	defer ctrl.Finish()
   297  
   298  	opts := newSessionTestOptions()
   299  	opts = opts.SetReadConsistencyLevel(topology.ReadConsistencyLevelAll)
   300  	s, err := newSession(opts)
   301  	assert.NoError(t, err)
   302  	session := s.(*session)
   303  
   304  	start := xtime.Now().Truncate(time.Hour)
   305  	end := start.Add(2 * time.Hour)
   306  
   307  	var (
   308  		numPoints = 100
   309  		sg0       = newTestSerieses(1, 10)
   310  		sg1       = newTestSerieses(6, 15)
   311  		sg2       = newTestSerieses(11, 15)
   312  	)
   313  	sg0.addDatapoints(numPoints, start, end)
   314  	sg1.addDatapoints(numPoints, start, end)
   315  	sg2.addDatapoints(numPoints, start, end)
   316  
   317  	topoInit := opts.TopologyInitializer()
   318  	topoWatch, err := topoInit.Init()
   319  	require.NoError(t, err)
   320  	topoMap := topoWatch.Get()
   321  	require.Equal(t, 3, topoMap.HostsLen()) // the code below assumes this
   322  	mockExtendedHostQueues(
   323  		t, ctrl, session, sessionTestReplicas,
   324  		testHostQueueOpsByHost{
   325  			testHostName(0): &testHostQueueOps{
   326  				enqueues: []testEnqueue{
   327  					{
   328  						enqueueFn: func(idx int, op op) {
   329  							go func() {
   330  								op.CompletionFn()(aggregateResultAccumulatorOpts{
   331  									host:     topoMap.Hosts()[idx],
   332  									response: sg0.toRPCAggResult(true),
   333  								}, nil)
   334  							}()
   335  						},
   336  					},
   337  				},
   338  			},
   339  			testHostName(1): &testHostQueueOps{
   340  				enqueues: []testEnqueue{
   341  					{
   342  						enqueueFn: func(idx int, op op) {
   343  							go func() {
   344  								op.CompletionFn()(aggregateResultAccumulatorOpts{
   345  									host:     topoMap.Hosts()[idx],
   346  									response: sg1.toRPCAggResult(false),
   347  								}, nil)
   348  							}()
   349  						},
   350  					},
   351  				},
   352  			},
   353  			testHostName(2): &testHostQueueOps{
   354  				enqueues: []testEnqueue{
   355  					{
   356  						enqueueFn: func(idx int, op op) {
   357  							go func() {
   358  								op.CompletionFn()(aggregateResultAccumulatorOpts{
   359  									host:     topoMap.Hosts()[idx],
   360  									response: sg2.toRPCAggResult(true),
   361  								}, nil)
   362  							}()
   363  						},
   364  					},
   365  				},
   366  			},
   367  		})
   368  
   369  	assert.NoError(t, session.Open())
   370  
   371  	// NB: stubbing needs to be done after session.Open
   372  	leakStatePool := injectLeakcheckFetchStatePool(session)
   373  	leakOpPool := injectLeakcheckAggregateOpPool(session)
   374  
   375  	iters, metadata, err := session.Aggregate(testContext(), ident.StringID("namespace"),
   376  		testSessionAggregateQuery, testSessionAggregateQueryOpts(start, end))
   377  	assert.NoError(t, err)
   378  	assert.False(t, metadata.Exhaustive)
   379  	expected := append(sg0, sg1...)
   380  	expected = append(expected, sg2...)
   381  	expected.assertMatchesAggregatedTagsIter(t, iters)
   382  
   383  	assert.NoError(t, session.Close())
   384  
   385  	numStateAllocs := 0
   386  	leakStatePool.CheckExtended(t, func(e leakcheckFetchState) {
   387  		require.Equal(t, int32(0), atomic.LoadInt32(&e.Value.refCounter.n), string(e.GetStacktrace))
   388  		numStateAllocs++
   389  	})
   390  	require.Equal(t, 1, numStateAllocs)
   391  
   392  	numOpAllocs := 0
   393  	leakOpPool.CheckExtended(t, func(e leakcheckAggregateOp) {
   394  		require.Equal(t, int32(0), atomic.LoadInt32(&e.Value.refCounter.n), string(e.GetStacktrace))
   395  		numOpAllocs++
   396  	})
   397  	require.Equal(t, 1, numOpAllocs)
   398  }
   399  
   400  func TestSessionAggregateMergeWithRetriesTest(t *testing.T) {
   401  	ctrl := gomock.NewController(t)
   402  	defer ctrl.Finish()
   403  
   404  	opts := newSessionTestOptions().
   405  		SetReadConsistencyLevel(topology.ReadConsistencyLevelAll).
   406  		SetFetchRetrier(xretry.NewRetrier(xretry.NewOptions().SetMaxRetries(1)))
   407  
   408  	s, err := newSession(opts)
   409  	assert.NoError(t, err)
   410  	session := s.(*session)
   411  
   412  	start := xtime.Now().Truncate(time.Hour)
   413  	end := start.Add(2 * time.Hour)
   414  
   415  	var (
   416  		numPoints = 100
   417  		sg0       = newTestSerieses(1, 5)
   418  		sg1       = newTestSerieses(6, 10)
   419  		sg2       = newTestSerieses(11, 15)
   420  	)
   421  	sg0.addDatapoints(numPoints, start, end)
   422  	sg1.addDatapoints(numPoints, start, end)
   423  	sg2.addDatapoints(numPoints, start, end)
   424  
   425  	topoInit := opts.TopologyInitializer()
   426  	topoWatch, err := topoInit.Init()
   427  	require.NoError(t, err)
   428  	topoMap := topoWatch.Get()
   429  	require.Equal(t, 3, topoMap.HostsLen()) // the code below assumes this
   430  	mockExtendedHostQueues(
   431  		t, ctrl, session, sessionTestReplicas,
   432  		testHostQueueOpsByHost{
   433  			testHostName(0): &testHostQueueOps{
   434  				enqueues: []testEnqueue{
   435  					{
   436  						enqueueFn: func(idx int, op op) {
   437  							go func() {
   438  								op.CompletionFn()(aggregateResultAccumulatorOpts{
   439  									host: topoMap.Hosts()[idx],
   440  								}, fmt.Errorf("random-err-0"))
   441  							}()
   442  						},
   443  					},
   444  					{
   445  						enqueueFn: func(idx int, op op) {
   446  							go func() {
   447  								op.CompletionFn()(aggregateResultAccumulatorOpts{
   448  									host:     topoMap.Hosts()[idx],
   449  									response: sg0.toRPCAggResult(true),
   450  								}, nil)
   451  							}()
   452  						},
   453  					},
   454  				},
   455  			},
   456  			testHostName(1): &testHostQueueOps{
   457  				enqueues: []testEnqueue{
   458  					{
   459  						enqueueFn: func(idx int, op op) {
   460  							go func() {
   461  								op.CompletionFn()(aggregateResultAccumulatorOpts{
   462  									host: topoMap.Hosts()[idx],
   463  								}, fmt.Errorf("random-err-1"))
   464  							}()
   465  						},
   466  					},
   467  					{
   468  						enqueueFn: func(idx int, op op) {
   469  							go func() {
   470  								op.CompletionFn()(aggregateResultAccumulatorOpts{
   471  									host:     topoMap.Hosts()[idx],
   472  									response: sg1.toRPCAggResult(false),
   473  								}, nil)
   474  							}()
   475  						},
   476  					},
   477  				},
   478  			},
   479  			testHostName(2): &testHostQueueOps{
   480  				enqueues: []testEnqueue{
   481  					{
   482  						enqueueFn: func(idx int, op op) {
   483  							go func() {
   484  								op.CompletionFn()(aggregateResultAccumulatorOpts{
   485  									host: topoMap.Hosts()[idx],
   486  								}, fmt.Errorf("random-err-2"))
   487  							}()
   488  						},
   489  					},
   490  					{
   491  						enqueueFn: func(idx int, op op) {
   492  							go func() {
   493  								op.CompletionFn()(aggregateResultAccumulatorOpts{
   494  									host:     topoMap.Hosts()[idx],
   495  									response: sg2.toRPCAggResult(true),
   496  								}, nil)
   497  							}()
   498  						},
   499  					},
   500  				},
   501  			},
   502  		})
   503  
   504  	assert.NoError(t, session.Open())
   505  
   506  	// NB: stubbing needs to be done after session.Open
   507  	leakStatePool := injectLeakcheckFetchStatePool(session)
   508  	leakOpPool := injectLeakcheckAggregateOpPool(session)
   509  	iters, meta, err := session.Aggregate(testContext(), ident.StringID("namespace"),
   510  		testSessionAggregateQuery, testSessionAggregateQueryOpts(start, end))
   511  	assert.NoError(t, err)
   512  	assert.False(t, meta.Exhaustive)
   513  	expected := append(sg0, sg1...)
   514  	expected = append(expected, sg2...)
   515  	expected.assertMatchesAggregatedTagsIter(t, iters)
   516  
   517  	numStateAllocs := 0
   518  	leakStatePool.CheckExtended(t, func(e leakcheckFetchState) {
   519  		require.Equal(t, int32(0), atomic.LoadInt32(&e.Value.refCounter.n), string(e.GetStacktrace))
   520  		numStateAllocs++
   521  	})
   522  	require.Equal(t, 2, numStateAllocs)
   523  
   524  	numOpAllocs := 0
   525  	leakOpPool.CheckExtended(t, func(e leakcheckAggregateOp) {
   526  		require.Equal(t, int32(0), atomic.LoadInt32(&e.Value.refCounter.n), string(e.GetStacktrace))
   527  		numOpAllocs++
   528  	})
   529  	require.Equal(t, 2, numOpAllocs)
   530  
   531  	assert.NoError(t, session.Close())
   532  }
   533  
   534  func injectLeakcheckAggregateAttempPool(session *session) *leakcheckAggregateAttemptPool {
   535  	leakPool := newLeakcheckAggregateAttemptPool(leakcheckAggregateAttemptPoolOpts{}, session.pools.aggregateAttempt)
   536  	session.pools.aggregateAttempt = leakPool
   537  	return leakPool
   538  }
   539  
   540  func injectLeakcheckAggregateOpPool(session *session) *leakcheckAggregateOpPool {
   541  	leakOpPool := newLeakcheckAggregateOpPool(leakcheckAggregateOpPoolOpts{}, session.pools.aggregateOp)
   542  	leakOpPool.opts.GetHookFn = func(f *aggregateOp) *aggregateOp { f.pool = leakOpPool; return f }
   543  	session.pools.aggregateOp = leakOpPool
   544  	return leakOpPool
   545  }