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

     1  // Copyright (c) 2021 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  	"sync"
    26  	"time"
    27  
    28  	"github.com/uber-go/tally"
    29  	"go.uber.org/atomic"
    30  	"go.uber.org/zap"
    31  
    32  	xerrors "github.com/m3db/m3/src/x/errors"
    33  	"github.com/m3db/m3/src/x/instrument"
    34  )
    35  
    36  const (
    37  	disabledLimitValue = 0
    38  	defaultLookback    = time.Second * 15
    39  )
    40  
    41  type queryLimits struct {
    42  	docsLimit           *lookbackLimit
    43  	bytesReadLimit      *lookbackLimit
    44  	aggregatedDocsLimit *lookbackLimit
    45  }
    46  
    47  type lookbackLimit struct {
    48  	name      string
    49  	started   bool
    50  	options   LookbackLimitOptions
    51  	metrics   lookbackLimitMetrics
    52  	logger    *zap.Logger
    53  	recent    *atomic.Int64
    54  	stopCh    chan struct{}
    55  	stoppedCh chan struct{}
    56  	lock      sync.RWMutex
    57  	iOpts     instrument.Options
    58  }
    59  
    60  type lookbackLimitMetrics struct {
    61  	optionsLimit    tally.Gauge
    62  	optionsLookback tally.Gauge
    63  	recentCount     tally.Gauge
    64  	recentMax       tally.Gauge
    65  	total           tally.Counter
    66  	exceeded        tally.Counter
    67  
    68  	sourceLogger SourceLogger
    69  }
    70  
    71  var (
    72  	_ QueryLimits   = (*queryLimits)(nil)
    73  	_ LookbackLimit = (*lookbackLimit)(nil)
    74  )
    75  
    76  // DefaultLookbackLimitOptions returns a new query limits manager.
    77  func DefaultLookbackLimitOptions() LookbackLimitOptions {
    78  	return LookbackLimitOptions{
    79  		// Default to no limit.
    80  		Limit:    disabledLimitValue,
    81  		Lookback: defaultLookback,
    82  	}
    83  }
    84  
    85  // DefaultLimitsOptions is the set of default limits options.
    86  func DefaultLimitsOptions(iOpts instrument.Options) Options {
    87  	return NewOptions().
    88  		SetInstrumentOptions(iOpts).
    89  		SetBytesReadLimitOpts(DefaultLookbackLimitOptions()).
    90  		SetDocsLimitOpts(DefaultLookbackLimitOptions()).
    91  		SetAggregateDocsLimitOpts(DefaultLookbackLimitOptions()).
    92  		SetDiskSeriesReadLimitOpts(DefaultLookbackLimitOptions())
    93  }
    94  
    95  // NewQueryLimits returns a new query limits manager.
    96  func NewQueryLimits(options Options) (QueryLimits, error) {
    97  	if err := options.Validate(); err != nil {
    98  		return nil, err
    99  	}
   100  
   101  	var (
   102  		iOpts               = options.InstrumentOptions()
   103  		docsLimitOpts       = options.DocsLimitOpts()
   104  		bytesReadLimitOpts  = options.BytesReadLimitOpts()
   105  		aggDocsLimitOpts    = options.AggregateDocsLimitOpts()
   106  		sourceLoggerBuilder = options.SourceLoggerBuilder()
   107  
   108  		docsMatched      = "docs-matched"
   109  		bytesRead        = "disk-bytes-read"
   110  		aggregateMatched = "aggregate-matched"
   111  		docsLimit        = newLookbackLimit(limitNames{
   112  			limitName:  docsMatched,
   113  			metricName: docsMatched,
   114  			metricType: "fetch",
   115  		}, docsLimitOpts, iOpts, sourceLoggerBuilder)
   116  		bytesReadLimit = newLookbackLimit(limitNames{
   117  			limitName:  bytesRead,
   118  			metricName: bytesRead,
   119  			metricType: "read",
   120  		}, bytesReadLimitOpts, iOpts, sourceLoggerBuilder)
   121  		aggregatedDocsLimit = newLookbackLimit(limitNames{
   122  			limitName:  aggregateMatched,
   123  			metricName: docsMatched,
   124  			metricType: "aggregate",
   125  		}, aggDocsLimitOpts, iOpts, sourceLoggerBuilder)
   126  	)
   127  
   128  	return &queryLimits{
   129  		docsLimit:           docsLimit,
   130  		bytesReadLimit:      bytesReadLimit,
   131  		aggregatedDocsLimit: aggregatedDocsLimit,
   132  	}, nil
   133  }
   134  
   135  // NewLookbackLimit returns a new lookback limit.
   136  func NewLookbackLimit(
   137  	name string,
   138  	opts LookbackLimitOptions,
   139  	instrumentOpts instrument.Options,
   140  	sourceLoggerBuilder SourceLoggerBuilder,
   141  ) LookbackLimit {
   142  	return newLookbackLimit(limitNames{
   143  		limitName:  name,
   144  		metricName: name,
   145  		metricType: name,
   146  	}, opts, instrumentOpts, sourceLoggerBuilder)
   147  }
   148  
   149  type limitNames struct {
   150  	limitName  string
   151  	metricName string
   152  	metricType string
   153  }
   154  
   155  func newLookbackLimit(
   156  	limitNames limitNames,
   157  	opts LookbackLimitOptions,
   158  	instrumentOpts instrument.Options,
   159  	sourceLoggerBuilder SourceLoggerBuilder,
   160  ) *lookbackLimit {
   161  	metrics := newLookbackLimitMetrics(
   162  		limitNames,
   163  		instrumentOpts,
   164  		sourceLoggerBuilder,
   165  	)
   166  
   167  	return &lookbackLimit{
   168  		name:      limitNames.limitName,
   169  		options:   opts,
   170  		metrics:   metrics,
   171  		logger:    instrumentOpts.Logger(),
   172  		recent:    atomic.NewInt64(0),
   173  		stopCh:    make(chan struct{}),
   174  		stoppedCh: make(chan struct{}),
   175  		iOpts:     instrumentOpts,
   176  	}
   177  }
   178  
   179  func newLookbackLimitMetrics(
   180  	limitNames limitNames,
   181  	instrumentOpts instrument.Options,
   182  	sourceLoggerBuilder SourceLoggerBuilder,
   183  ) lookbackLimitMetrics {
   184  	metricName := limitNames.metricName
   185  	loggerScope := instrumentOpts.MetricsScope().Tagged(map[string]string{
   186  		"type": limitNames.metricType,
   187  	})
   188  
   189  	var (
   190  		loggerOpts  = instrumentOpts.SetMetricsScope(loggerScope)
   191  		metricScope = loggerScope.SubScope("query-limit")
   192  	)
   193  
   194  	return lookbackLimitMetrics{
   195  		optionsLimit:    metricScope.Gauge(fmt.Sprintf("current-limit-%s", metricName)),
   196  		optionsLookback: metricScope.Gauge(fmt.Sprintf("current-lookback-%s", metricName)),
   197  		recentCount:     metricScope.Gauge(fmt.Sprintf("recent-count-%s", metricName)),
   198  		recentMax:       metricScope.Gauge(fmt.Sprintf("recent-max-%s", metricName)),
   199  		total:           metricScope.Counter(fmt.Sprintf("total-%s", metricName)),
   200  		exceeded:        metricScope.Tagged(map[string]string{"limit": metricName}).Counter("exceeded"),
   201  
   202  		sourceLogger: sourceLoggerBuilder.NewSourceLogger(metricName, loggerOpts),
   203  	}
   204  }
   205  
   206  func (q *queryLimits) FetchDocsLimit() LookbackLimit {
   207  	return q.docsLimit
   208  }
   209  
   210  func (q *queryLimits) BytesReadLimit() LookbackLimit {
   211  	return q.bytesReadLimit
   212  }
   213  
   214  func (q *queryLimits) AggregateDocsLimit() LookbackLimit {
   215  	return q.aggregatedDocsLimit
   216  }
   217  
   218  func (q *queryLimits) Start() {
   219  	q.docsLimit.Start()
   220  	q.bytesReadLimit.Start()
   221  	q.aggregatedDocsLimit.Start()
   222  }
   223  
   224  func (q *queryLimits) Stop() {
   225  	q.docsLimit.Stop()
   226  	q.bytesReadLimit.Stop()
   227  	q.aggregatedDocsLimit.Stop()
   228  }
   229  
   230  func (q *queryLimits) AnyFetchExceeded() error {
   231  	if err := q.docsLimit.exceeded(); err != nil {
   232  		return err
   233  	}
   234  
   235  	return q.bytesReadLimit.exceeded()
   236  }
   237  
   238  func (q *lookbackLimit) Options() LookbackLimitOptions {
   239  	q.lock.RLock()
   240  	o := q.options
   241  	q.lock.RUnlock()
   242  	return o
   243  }
   244  
   245  // Update updates the limit.
   246  func (q *lookbackLimit) Update(opts LookbackLimitOptions) error {
   247  	if err := opts.validate(); err != nil {
   248  		return err
   249  	}
   250  
   251  	q.lock.Lock()
   252  	defer q.lock.Unlock()
   253  
   254  	old := q.options
   255  	q.options = opts
   256  
   257  	// If the lookback changed, replace the background goroutine that manages the periodic resetting.
   258  	if q.options.Lookback != old.Lookback {
   259  		q.stop()
   260  		q.start()
   261  	}
   262  
   263  	q.logger.Info("query limit options updated",
   264  		zap.String("name", q.name),
   265  		zap.Any("new", opts),
   266  		zap.Any("old", old))
   267  
   268  	return nil
   269  }
   270  
   271  // Inc increments the current value and returns an error if above the limit.
   272  func (q *lookbackLimit) Inc(val int, source []byte) error {
   273  	if val < 0 {
   274  		return fmt.Errorf("invalid negative query limit inc %d", val)
   275  	}
   276  	if val == 0 {
   277  		return q.exceeded()
   278  	}
   279  
   280  	// Add the new stats to the global state.
   281  	valI64 := int64(val)
   282  	recent := q.recent.Add(valI64)
   283  
   284  	// Update metrics.
   285  	q.metrics.recentCount.Update(float64(recent))
   286  	q.metrics.total.Inc(valI64)
   287  	q.metrics.sourceLogger.LogSourceValue(valI64, source)
   288  
   289  	// Enforce limit (if specified).
   290  	return q.checkLimit(recent)
   291  }
   292  
   293  func (q *lookbackLimit) exceeded() error {
   294  	return q.checkLimit(q.recent.Load())
   295  }
   296  
   297  func (q *lookbackLimit) checkLimit(recent int64) error {
   298  	q.lock.RLock()
   299  	currentOpts := q.options
   300  	q.lock.RUnlock()
   301  
   302  	if currentOpts.ForceExceeded {
   303  		q.metrics.exceeded.Inc(1)
   304  
   305  		return xerrors.NewInvalidParamsError(NewQueryLimitExceededError(fmt.Sprintf(
   306  			"query aborted due to forced limit: name=%s", q.name)))
   307  	}
   308  
   309  	if currentOpts.Limit == disabledLimitValue {
   310  		return nil
   311  	}
   312  
   313  	if recent >= currentOpts.Limit {
   314  		q.metrics.exceeded.Inc(1)
   315  
   316  		return xerrors.NewInvalidParamsError(NewQueryLimitExceededError(fmt.Sprintf(
   317  			"query aborted due to limit: name=%s, limit=%d, current=%d, within=%s",
   318  			q.name, q.options.Limit, recent, q.options.Lookback)))
   319  	}
   320  
   321  	return nil
   322  }
   323  
   324  func (q *lookbackLimit) Start() {
   325  	// Lock on explicit start to avoid any collision with asynchronous updating
   326  	// which will call stop/start if the lookback has changed.
   327  	q.lock.Lock()
   328  	defer q.lock.Unlock()
   329  	q.start()
   330  }
   331  
   332  func (q *lookbackLimit) Stop() {
   333  	// Lock on explicit stop to avoid any collision with asynchronous updating
   334  	// which will call stop/start if the lookback has changed.
   335  	q.lock.Lock()
   336  	defer q.lock.Unlock()
   337  	q.stop()
   338  }
   339  
   340  func (q *lookbackLimit) start() {
   341  	q.started = true
   342  	ticker := time.NewTicker(q.options.Lookback)
   343  	go func() {
   344  		q.logger.Info("query limit interval started", zap.String("name", q.name))
   345  		for {
   346  			select {
   347  			case <-ticker.C:
   348  				q.reset()
   349  			case <-q.stopCh:
   350  				ticker.Stop()
   351  				q.stoppedCh <- struct{}{}
   352  				return
   353  			}
   354  		}
   355  	}()
   356  
   357  	q.metrics.optionsLimit.Update(float64(q.options.Limit))
   358  	q.metrics.optionsLookback.Update(q.options.Lookback.Seconds())
   359  }
   360  
   361  func (q *lookbackLimit) stop() {
   362  	if !q.started {
   363  		// NB: this lookback limit has not yet been started.
   364  		instrument.EmitAndLogInvariantViolation(q.iOpts, func(l *zap.Logger) {
   365  			l.With(
   366  				zap.Any("limit_name", q.name),
   367  			).Error("cannot stop non-started lookback limit")
   368  		})
   369  		return
   370  	}
   371  
   372  	close(q.stopCh)
   373  	<-q.stoppedCh
   374  	q.stopCh = make(chan struct{})
   375  	q.stoppedCh = make(chan struct{})
   376  
   377  	q.logger.Info("query limit interval stopped", zap.String("name", q.name))
   378  }
   379  
   380  func (q *lookbackLimit) current() int64 {
   381  	return q.recent.Load()
   382  }
   383  
   384  func (q *lookbackLimit) reset() {
   385  	// Update peak gauge only on resets so it only tracks
   386  	// the peak values for each lookback period.
   387  	recent := q.recent.Load()
   388  
   389  	q.metrics.recentMax.Update(float64(recent))
   390  	// Update the standard recent gauge to reflect drop back to zero.
   391  	q.metrics.recentCount.Update(0)
   392  	q.recent.Store(0)
   393  }
   394  
   395  // Equals returns true if the other options match the current.
   396  func (opts LookbackLimitOptions) Equals(other LookbackLimitOptions) bool {
   397  	return opts.Limit == other.Limit &&
   398  		opts.Lookback == other.Lookback &&
   399  		opts.ForceExceeded == other.ForceExceeded &&
   400  		opts.ForceWaited == other.ForceWaited
   401  }
   402  
   403  func (opts LookbackLimitOptions) validate() error {
   404  	if opts.Limit < 0 {
   405  		return fmt.Errorf("query limit requires limit >= 0 (%d)", opts.Limit)
   406  	}
   407  	if opts.Lookback <= 0 {
   408  		return fmt.Errorf("query limit requires lookback > 0 (%d)", opts.Lookback)
   409  	}
   410  	return nil
   411  }