github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/storage/limits/query_limits_test.go (about)

     1  // Copyright (c) 2020 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 limits
    22  
    23  import (
    24  	"fmt"
    25  	"testing"
    26  	"time"
    27  
    28  	xclock "github.com/m3db/m3/src/x/clock"
    29  	xerrors "github.com/m3db/m3/src/x/errors"
    30  	"github.com/m3db/m3/src/x/instrument"
    31  	"github.com/m3db/m3/src/x/tallytest"
    32  
    33  	"github.com/stretchr/testify/assert"
    34  	"github.com/stretchr/testify/require"
    35  	"github.com/uber-go/tally"
    36  )
    37  
    38  func testQueryLimitOptions(
    39  	docOpts LookbackLimitOptions,
    40  	bytesOpts LookbackLimitOptions,
    41  	seriesOpts LookbackLimitOptions,
    42  	aggDocsOpts LookbackLimitOptions,
    43  	iOpts instrument.Options,
    44  ) Options {
    45  	return NewOptions().
    46  		SetDocsLimitOpts(docOpts).
    47  		SetBytesReadLimitOpts(bytesOpts).
    48  		SetDiskSeriesReadLimitOpts(seriesOpts).
    49  		SetAggregateDocsLimitOpts(aggDocsOpts).
    50  		SetInstrumentOptions(iOpts)
    51  }
    52  
    53  func TestQueryLimits(t *testing.T) {
    54  	l := int64(1)
    55  	docOpts := LookbackLimitOptions{
    56  		Limit:    l,
    57  		Lookback: time.Second,
    58  	}
    59  	bytesOpts := LookbackLimitOptions{
    60  		Limit:    l,
    61  		Lookback: time.Second,
    62  	}
    63  	seriesOpts := LookbackLimitOptions{
    64  		Limit:    l,
    65  		Lookback: time.Second,
    66  	}
    67  	aggOpts := LookbackLimitOptions{
    68  		Limit:    l,
    69  		Lookback: time.Second,
    70  	}
    71  	opts := testQueryLimitOptions(docOpts, bytesOpts, seriesOpts, aggOpts, instrument.NewOptions())
    72  	queryLimits, err := NewQueryLimits(opts)
    73  	require.NoError(t, err)
    74  	require.NotNil(t, queryLimits)
    75  
    76  	// No error yet.
    77  	require.NoError(t, queryLimits.AnyFetchExceeded())
    78  
    79  	// Limit from docs.
    80  	require.Error(t, queryLimits.FetchDocsLimit().Inc(2, nil))
    81  	err = queryLimits.AnyFetchExceeded()
    82  	require.Error(t, err)
    83  	require.True(t, xerrors.IsInvalidParams(err))
    84  	require.True(t, IsQueryLimitExceededError(err))
    85  
    86  	opts = testQueryLimitOptions(docOpts, bytesOpts, seriesOpts, aggOpts, instrument.NewOptions())
    87  	queryLimits, err = NewQueryLimits(opts)
    88  	require.NoError(t, err)
    89  	require.NotNil(t, queryLimits)
    90  
    91  	// No error yet.
    92  	err = queryLimits.AnyFetchExceeded()
    93  	require.NoError(t, err)
    94  
    95  	// Limit from bytes.
    96  	require.Error(t, queryLimits.BytesReadLimit().Inc(2, nil))
    97  	err = queryLimits.AnyFetchExceeded()
    98  	require.Error(t, err)
    99  	require.True(t, xerrors.IsInvalidParams(err))
   100  	require.True(t, IsQueryLimitExceededError(err))
   101  
   102  	opts = testQueryLimitOptions(docOpts, bytesOpts, seriesOpts, aggOpts, instrument.NewOptions())
   103  	queryLimits, err = NewQueryLimits(opts)
   104  	require.NoError(t, err)
   105  	require.NotNil(t, queryLimits)
   106  
   107  	// No error yet.
   108  	err = queryLimits.AnyFetchExceeded()
   109  	require.NoError(t, err)
   110  
   111  	// Limit from aggregate does not trip any fetched exceeded.
   112  	require.Error(t, queryLimits.AggregateDocsLimit().Inc(2, nil))
   113  	err = queryLimits.AnyFetchExceeded()
   114  	require.NoError(t, err)
   115  	require.NotNil(t, queryLimits)
   116  
   117  	opts = testQueryLimitOptions(docOpts, bytesOpts, seriesOpts, aggOpts, instrument.NewOptions())
   118  	queryLimits, err = NewQueryLimits(opts)
   119  	require.NoError(t, err)
   120  	require.NotNil(t, queryLimits)
   121  
   122  	// No error yet.
   123  	err = queryLimits.AnyFetchExceeded()
   124  	require.NoError(t, err)
   125  }
   126  
   127  func TestLookbackLimit(t *testing.T) {
   128  	for _, test := range []struct {
   129  		name          string
   130  		limit         int64
   131  		forceExceeded bool
   132  	}{
   133  		{name: "no limit", limit: 0},
   134  		{name: "limit", limit: 5},
   135  		{name: "force exceeded limit", limit: 5, forceExceeded: true},
   136  	} {
   137  		t.Run(test.name, func(t *testing.T) {
   138  			scope := tally.NewTestScope("", nil)
   139  			iOpts := instrument.NewOptions().SetMetricsScope(scope)
   140  			opts := LookbackLimitOptions{
   141  				Limit:         test.limit,
   142  				Lookback:      time.Millisecond * 100,
   143  				ForceExceeded: test.forceExceeded,
   144  			}
   145  			name := "test"
   146  			limit := newLookbackLimit(limitNames{
   147  				limitName:  name,
   148  				metricName: name,
   149  				metricType: name,
   150  			}, opts, iOpts, &sourceLoggerBuilder{})
   151  
   152  			require.Equal(t, int64(0), limit.current())
   153  
   154  			var exceededCount int64
   155  			err := limit.exceeded()
   156  			if test.limit >= 0 && !test.forceExceeded {
   157  				require.NoError(t, err)
   158  			} else {
   159  				require.Error(t, err)
   160  				exceededCount++
   161  			}
   162  
   163  			// Validate ascending while checking limits.
   164  			exceededCount += verifyLimit(t, limit, 3, test.limit, test.forceExceeded)
   165  			require.Equal(t, int64(3), limit.current())
   166  			verifyMetrics(t, scope, name, 3, 0, 3, exceededCount)
   167  
   168  			exceededCount += verifyLimit(t, limit, 2, test.limit, test.forceExceeded)
   169  			require.Equal(t, int64(5), limit.current())
   170  			verifyMetrics(t, scope, name, 5, 0, 5, exceededCount)
   171  
   172  			exceededCount += verifyLimit(t, limit, 1, test.limit, test.forceExceeded)
   173  			require.Equal(t, int64(6), limit.current())
   174  			verifyMetrics(t, scope, name, 6, 0, 6, exceededCount)
   175  
   176  			exceededCount += verifyLimit(t, limit, 4, test.limit, test.forceExceeded)
   177  			require.Equal(t, int64(10), limit.current())
   178  			verifyMetrics(t, scope, name, 10, 0, 10, exceededCount)
   179  
   180  			// Validate first reset.
   181  			limit.reset()
   182  			require.Equal(t, int64(0), limit.current())
   183  			verifyMetrics(t, scope, name, 0, 10, 10, exceededCount)
   184  
   185  			// Validate ascending again post-reset.
   186  			exceededCount += verifyLimit(t, limit, 2, test.limit, test.forceExceeded)
   187  			require.Equal(t, int64(2), limit.current())
   188  			verifyMetrics(t, scope, name, 2, 10, 12, exceededCount)
   189  
   190  			exceededCount += verifyLimit(t, limit, 5, test.limit, test.forceExceeded)
   191  			require.Equal(t, int64(7), limit.current())
   192  			verifyMetrics(t, scope, name, 7, 10, 17, exceededCount)
   193  
   194  			// Validate second reset.
   195  			limit.reset()
   196  
   197  			require.Equal(t, int64(0), limit.current())
   198  			verifyMetrics(t, scope, name, 0, 7, 17, exceededCount)
   199  
   200  			// Validate consecutive reset (ensure peak goes to zero).
   201  			limit.reset()
   202  
   203  			require.Equal(t, int64(0), limit.current())
   204  			verifyMetrics(t, scope, name, 0, 0, 17, exceededCount)
   205  
   206  			limit.reset()
   207  
   208  			opts.Limit = 0
   209  			require.NoError(t, limit.Update(opts))
   210  
   211  			exceededCount += verifyLimit(t, limit, 0, opts.Limit, test.forceExceeded)
   212  			require.Equal(t, int64(0), limit.current())
   213  
   214  			opts.Limit = 2
   215  			require.NoError(t, limit.Update(opts))
   216  
   217  			exceededCount += verifyLimit(t, limit, 1, opts.Limit, test.forceExceeded)
   218  			require.Equal(t, int64(1), limit.current())
   219  			verifyMetrics(t, scope, name, 1, 0, 18, exceededCount)
   220  
   221  			exceededCount += verifyLimit(t, limit, 1, opts.Limit, test.forceExceeded)
   222  			require.Equal(t, int64(2), limit.current())
   223  			verifyMetrics(t, scope, name, 2, 0, 19, exceededCount)
   224  
   225  			exceededCount += verifyLimit(t, limit, 1, opts.Limit, test.forceExceeded)
   226  			require.Equal(t, int64(3), limit.current())
   227  			verifyMetrics(t, scope, name, 3, 0, 20, exceededCount)
   228  		})
   229  	}
   230  }
   231  
   232  func verifyLimit(t *testing.T, limit *lookbackLimit, inc int, expectedLimit int64, forceExceeded bool) int64 {
   233  	var exceededCount int64
   234  	err := limit.Inc(inc, nil)
   235  	if (expectedLimit == 0 || limit.current() < expectedLimit) && !forceExceeded {
   236  		require.NoError(t, err)
   237  	} else {
   238  		require.Error(t, err)
   239  		require.True(t, xerrors.IsInvalidParams(err))
   240  		require.True(t, IsQueryLimitExceededError(err))
   241  		exceededCount++
   242  	}
   243  
   244  	err = limit.exceeded()
   245  	if (expectedLimit == 0 || limit.current() < expectedLimit) && !forceExceeded {
   246  		require.NoError(t, err)
   247  	} else {
   248  		require.Error(t, err)
   249  		require.True(t, xerrors.IsInvalidParams(err))
   250  		require.True(t, IsQueryLimitExceededError(err))
   251  		exceededCount++
   252  	}
   253  	return exceededCount
   254  }
   255  
   256  func TestLookbackReset(t *testing.T) {
   257  	scope := tally.NewTestScope("", nil)
   258  	iOpts := instrument.NewOptions().SetMetricsScope(scope)
   259  	opts := LookbackLimitOptions{
   260  		Limit:    5,
   261  		Lookback: time.Millisecond * 100,
   262  	}
   263  	name := "test"
   264  	limit := newLookbackLimit(limitNames{
   265  		limitName:  name,
   266  		metricName: name,
   267  		metricType: name,
   268  	}, opts, iOpts, &sourceLoggerBuilder{})
   269  
   270  	err := limit.Inc(3, nil)
   271  	require.NoError(t, err)
   272  	require.Equal(t, int64(3), limit.current())
   273  
   274  	limit.start()
   275  	defer limit.stop()
   276  	time.Sleep(opts.Lookback * 2)
   277  
   278  	success := xclock.WaitUntil(func() bool {
   279  		return limit.current() == 0
   280  	}, 5*time.Second)
   281  	require.True(t, success, "did not eventually reset to zero")
   282  }
   283  
   284  func TestValidateLookbackLimitOptions(t *testing.T) {
   285  	for _, test := range []struct {
   286  		name        string
   287  		max         int64
   288  		lookback    time.Duration
   289  		expectError bool
   290  	}{
   291  		{
   292  			name:     "valid lookback without limit",
   293  			max:      0,
   294  			lookback: time.Millisecond,
   295  		},
   296  		{
   297  			name:     "valid lookback with valid limit",
   298  			max:      1,
   299  			lookback: time.Millisecond,
   300  		},
   301  		{
   302  			name:        "negative lookback",
   303  			max:         0,
   304  			lookback:    -time.Millisecond,
   305  			expectError: true,
   306  		},
   307  		{
   308  			name:        "zero lookback",
   309  			max:         0,
   310  			lookback:    time.Duration(0),
   311  			expectError: true,
   312  		},
   313  		{
   314  			name:        "negative max",
   315  			max:         -1,
   316  			lookback:    time.Millisecond,
   317  			expectError: true,
   318  		},
   319  	} {
   320  		t.Run(test.name, func(t *testing.T) {
   321  			err := LookbackLimitOptions{
   322  				Limit:    test.max,
   323  				Lookback: test.lookback,
   324  			}.validate()
   325  			if test.expectError {
   326  				require.Error(t, err)
   327  			} else {
   328  				require.NoError(t, err)
   329  			}
   330  
   331  			// Validate empty.
   332  			require.Error(t, LookbackLimitOptions{}.validate())
   333  		})
   334  	}
   335  }
   336  
   337  func verifyMetrics(t *testing.T,
   338  	scope tally.TestScope,
   339  	name string,
   340  	expectedRecent float64,
   341  	expectedRecentPeak float64,
   342  	expectedTotal int64,
   343  	expectedExceeded int64,
   344  ) {
   345  	snapshot := scope.Snapshot()
   346  	tallytest.AssertGaugeValue(
   347  		t, expectedRecent, snapshot,
   348  		fmt.Sprintf("query-limit.recent-count-%s", name),
   349  		map[string]string{"type": "test"})
   350  
   351  	tallytest.AssertGaugeValue(
   352  		t, expectedRecentPeak, snapshot,
   353  		fmt.Sprintf("query-limit.recent-max-%s", name),
   354  		map[string]string{"type": "test"})
   355  
   356  	tallytest.AssertCounterValue(
   357  		t, expectedTotal, snapshot,
   358  		fmt.Sprintf("query-limit.total-%s", name),
   359  		map[string]string{"type": "test"})
   360  
   361  	tallytest.AssertCounterValue(
   362  		t, expectedExceeded, snapshot,
   363  		"query-limit.exceeded",
   364  		map[string]string{"type": "test", "limit": "test"})
   365  }
   366  
   367  type testLoggerRecord struct {
   368  	name   string
   369  	val    int64
   370  	source []byte
   371  }
   372  
   373  func TestSourceLogger(t *testing.T) {
   374  	var (
   375  		scope   = tally.NewTestScope("test", nil)
   376  		iOpts   = instrument.NewOptions().SetMetricsScope(scope)
   377  		noLimit = LookbackLimitOptions{
   378  			Limit:    0,
   379  			Lookback: time.Millisecond * 100,
   380  		}
   381  
   382  		builder = &testBuilder{records: []testLoggerRecord{}}
   383  		opts    = testQueryLimitOptions(noLimit, noLimit, noLimit, noLimit, iOpts).
   384  			SetSourceLoggerBuilder(builder)
   385  	)
   386  
   387  	require.NoError(t, opts.Validate())
   388  
   389  	queryLimits, err := NewQueryLimits(opts)
   390  	require.NoError(t, err)
   391  	require.NotNil(t, queryLimits)
   392  
   393  	require.NoError(t, queryLimits.FetchDocsLimit().Inc(100, []byte("docs")))
   394  	require.NoError(t, queryLimits.BytesReadLimit().Inc(200, []byte("bytes")))
   395  
   396  	assert.Equal(t, []testLoggerRecord{
   397  		{name: "docs-matched", val: 100, source: []byte("docs")},
   398  		{name: "disk-bytes-read", val: 200, source: []byte("bytes")},
   399  	}, builder.records)
   400  
   401  	require.NoError(t, queryLimits.AggregateDocsLimit().Inc(1000, []byte("docs")))
   402  	assert.Equal(t, []testLoggerRecord{
   403  		{name: "docs-matched", val: 100, source: []byte("docs")},
   404  		{name: "disk-bytes-read", val: 200, source: []byte("bytes")},
   405  		{name: "docs-matched", val: 1000, source: []byte("docs")},
   406  	}, builder.records)
   407  }
   408  
   409  // NB: creates test logger records that share an underlying record set,
   410  // differentiated by source logger name.
   411  type testBuilder struct {
   412  	records []testLoggerRecord
   413  }
   414  
   415  var _ SourceLoggerBuilder = (*testBuilder)(nil)
   416  
   417  func (s *testBuilder) NewSourceLogger(n string, opts instrument.Options) SourceLogger {
   418  	return &testSourceLogger{name: n, builder: s}
   419  }
   420  
   421  type testSourceLogger struct {
   422  	name    string
   423  	builder *testBuilder
   424  }
   425  
   426  var _ SourceLogger = (*testSourceLogger)(nil)
   427  
   428  func (l *testSourceLogger) LogSourceValue(val int64, source []byte) {
   429  	l.builder.records = append(l.builder.records, testLoggerRecord{
   430  		name:   l.name,
   431  		val:    val,
   432  		source: source,
   433  	})
   434  }