github.com/muhammadn/cortex@v1.9.1-0.20220510110439-46bb7000d03d/pkg/util/limiter/query_limiter.go (about)

     1  package limiter
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sync"
     7  
     8  	"github.com/prometheus/common/model"
     9  	"go.uber.org/atomic"
    10  
    11  	"github.com/cortexproject/cortex/pkg/cortexpb"
    12  	"github.com/cortexproject/cortex/pkg/ingester/client"
    13  )
    14  
    15  type queryLimiterCtxKey struct{}
    16  
    17  var (
    18  	ctxKey                    = &queryLimiterCtxKey{}
    19  	ErrMaxSeriesHit           = "the query hit the max number of series limit (limit: %d series)"
    20  	ErrMaxChunkBytesHit       = "the query hit the aggregated chunks size limit (limit: %d bytes)"
    21  	ErrMaxChunksPerQueryLimit = "the query hit the max number of chunks limit (limit: %d chunks)"
    22  )
    23  
    24  type QueryLimiter struct {
    25  	uniqueSeriesMx sync.Mutex
    26  	uniqueSeries   map[model.Fingerprint]struct{}
    27  
    28  	chunkBytesCount atomic.Int64
    29  	chunkCount      atomic.Int64
    30  
    31  	maxSeriesPerQuery     int
    32  	maxChunkBytesPerQuery int
    33  	maxChunksPerQuery     int
    34  }
    35  
    36  // NewQueryLimiter makes a new per-query limiter. Each query limiter
    37  // is configured using the `maxSeriesPerQuery` limit.
    38  func NewQueryLimiter(maxSeriesPerQuery, maxChunkBytesPerQuery int, maxChunksPerQuery int) *QueryLimiter {
    39  	return &QueryLimiter{
    40  		uniqueSeriesMx: sync.Mutex{},
    41  		uniqueSeries:   map[model.Fingerprint]struct{}{},
    42  
    43  		maxSeriesPerQuery:     maxSeriesPerQuery,
    44  		maxChunkBytesPerQuery: maxChunkBytesPerQuery,
    45  		maxChunksPerQuery:     maxChunksPerQuery,
    46  	}
    47  }
    48  
    49  func AddQueryLimiterToContext(ctx context.Context, limiter *QueryLimiter) context.Context {
    50  	return context.WithValue(ctx, ctxKey, limiter)
    51  }
    52  
    53  // QueryLimiterFromContextWithFallback returns a QueryLimiter from the current context.
    54  // If there is not a QueryLimiter on the context it will return a new no-op limiter.
    55  func QueryLimiterFromContextWithFallback(ctx context.Context) *QueryLimiter {
    56  	ql, ok := ctx.Value(ctxKey).(*QueryLimiter)
    57  	if !ok {
    58  		// If there's no limiter return a new unlimited limiter as a fallback
    59  		ql = NewQueryLimiter(0, 0, 0)
    60  	}
    61  	return ql
    62  }
    63  
    64  // AddSeries adds the input series and returns an error if the limit is reached.
    65  func (ql *QueryLimiter) AddSeries(seriesLabels []cortexpb.LabelAdapter) error {
    66  	// If the max series is unlimited just return without managing map
    67  	if ql.maxSeriesPerQuery == 0 {
    68  		return nil
    69  	}
    70  	fingerprint := client.FastFingerprint(seriesLabels)
    71  
    72  	ql.uniqueSeriesMx.Lock()
    73  	defer ql.uniqueSeriesMx.Unlock()
    74  
    75  	ql.uniqueSeries[fingerprint] = struct{}{}
    76  	if len(ql.uniqueSeries) > ql.maxSeriesPerQuery {
    77  		// Format error with max limit
    78  		return fmt.Errorf(ErrMaxSeriesHit, ql.maxSeriesPerQuery)
    79  	}
    80  	return nil
    81  }
    82  
    83  // uniqueSeriesCount returns the count of unique series seen by this query limiter.
    84  func (ql *QueryLimiter) uniqueSeriesCount() int {
    85  	ql.uniqueSeriesMx.Lock()
    86  	defer ql.uniqueSeriesMx.Unlock()
    87  	return len(ql.uniqueSeries)
    88  }
    89  
    90  // AddChunkBytes adds the input chunk size in bytes and returns an error if the limit is reached.
    91  func (ql *QueryLimiter) AddChunkBytes(chunkSizeInBytes int) error {
    92  	if ql.maxChunkBytesPerQuery == 0 {
    93  		return nil
    94  	}
    95  	if ql.chunkBytesCount.Add(int64(chunkSizeInBytes)) > int64(ql.maxChunkBytesPerQuery) {
    96  		return fmt.Errorf(ErrMaxChunkBytesHit, ql.maxChunkBytesPerQuery)
    97  	}
    98  	return nil
    99  }
   100  
   101  func (ql *QueryLimiter) AddChunks(count int) error {
   102  	if ql.maxChunksPerQuery == 0 {
   103  		return nil
   104  	}
   105  
   106  	if ql.chunkCount.Add(int64(count)) > int64(ql.maxChunksPerQuery) {
   107  		return fmt.Errorf(fmt.Sprintf(ErrMaxChunksPerQueryLimit, ql.maxChunksPerQuery))
   108  	}
   109  	return nil
   110  }