github.com/thanos-io/thanos@v0.32.5/pkg/receive/limiter.go (about)

     1  // Copyright (c) The Thanos Authors.
     2  // Licensed under the Apache License 2.0.
     3  
     4  package receive
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/go-kit/log"
    13  	"github.com/go-kit/log/level"
    14  	"github.com/prometheus/client_golang/prometheus/promauto"
    15  	"github.com/thanos-io/thanos/pkg/extkingpin"
    16  
    17  	"github.com/pkg/errors"
    18  	"github.com/prometheus/client_golang/prometheus"
    19  	"github.com/thanos-io/thanos/pkg/extprom"
    20  	"github.com/thanos-io/thanos/pkg/gate"
    21  )
    22  
    23  // Limiter is responsible for managing the configuration and initialization of
    24  // different types that apply limits to the Receive instance.
    25  type Limiter struct {
    26  	sync.RWMutex
    27  	requestLimiter            requestLimiter
    28  	HeadSeriesLimiter         headSeriesLimiter
    29  	writeGate                 gate.Gate
    30  	registerer                prometheus.Registerer
    31  	configPathOrContent       fileContent
    32  	logger                    log.Logger
    33  	configReloadCounter       prometheus.Counter
    34  	configReloadFailedCounter prometheus.Counter
    35  	receiverMode              ReceiverMode
    36  	configReloadTimer         time.Duration
    37  }
    38  
    39  // headSeriesLimiter encompasses active/head series limiting logic.
    40  type headSeriesLimiter interface {
    41  	QueryMetaMonitoring(context.Context) error
    42  	isUnderLimit(tenant string) (bool, error)
    43  }
    44  
    45  type requestLimiter interface {
    46  	AllowSizeBytes(tenant string, contentLengthBytes int64) bool
    47  	AllowSeries(tenant string, amount int64) bool
    48  	AllowSamples(tenant string, amount int64) bool
    49  }
    50  
    51  // fileContent is an interface to avoid a direct dependency on kingpin or extkingpin.
    52  type fileContent interface {
    53  	Content() ([]byte, error)
    54  	Path() string
    55  }
    56  
    57  // NewLimiter creates a new *Limiter given a configuration and prometheus
    58  // registerer.
    59  func NewLimiter(configFile fileContent, reg prometheus.Registerer, r ReceiverMode, logger log.Logger, configReloadTimer time.Duration) (*Limiter, error) {
    60  	limiter := &Limiter{
    61  		writeGate:         gate.NewNoop(),
    62  		requestLimiter:    &noopRequestLimiter{},
    63  		HeadSeriesLimiter: NewNopSeriesLimit(),
    64  		logger:            logger,
    65  		receiverMode:      r,
    66  		configReloadTimer: configReloadTimer,
    67  	}
    68  
    69  	if reg != nil {
    70  		limiter.registerer = NewUnRegisterer(reg)
    71  		limiter.configReloadCounter = promauto.With(limiter.registerer).NewCounter(
    72  			prometheus.CounterOpts{
    73  				Namespace: "thanos",
    74  				Subsystem: "receive",
    75  				Name:      "limits_config_reload_total",
    76  				Help:      "How many times the limit configuration was reloaded",
    77  			},
    78  		)
    79  		limiter.configReloadFailedCounter = promauto.With(limiter.registerer).NewCounter(
    80  			prometheus.CounterOpts{
    81  				Namespace: "thanos",
    82  				Subsystem: "receive",
    83  				Name:      "limits_config_reload_err_total",
    84  				Help:      "How many times the limit configuration failed to reload.",
    85  			},
    86  		)
    87  	}
    88  
    89  	if configFile == nil {
    90  		return limiter, nil
    91  	}
    92  
    93  	limiter.configPathOrContent = configFile
    94  	if err := limiter.loadConfig(); err != nil {
    95  		return nil, errors.Wrap(err, "load tenant limits config")
    96  	}
    97  
    98  	return limiter, nil
    99  }
   100  
   101  // StartConfigReloader starts the automatic configuration reloader based off of
   102  // the file indicated by pathOrContent. It starts a Go routine in the given
   103  // *run.Group.
   104  func (l *Limiter) StartConfigReloader(ctx context.Context) error {
   105  	if !l.CanReload() {
   106  		return nil
   107  	}
   108  
   109  	return extkingpin.PathContentReloader(ctx, l.configPathOrContent, l.logger, func() {
   110  		level.Info(l.logger).Log("msg", "reloading limit config")
   111  		if err := l.loadConfig(); err != nil {
   112  			if failedReload := l.configReloadCounter; failedReload != nil {
   113  				failedReload.Inc()
   114  			}
   115  			errMsg := fmt.Sprintf("error reloading tenant limits config from %s", l.configPathOrContent.Path())
   116  			level.Error(l.logger).Log("msg", errMsg, "err", err)
   117  		}
   118  		if reloadCounter := l.configReloadCounter; reloadCounter != nil {
   119  			reloadCounter.Inc()
   120  		}
   121  	}, l.configReloadTimer)
   122  }
   123  
   124  func (l *Limiter) CanReload() bool {
   125  	if l.configPathOrContent == nil {
   126  		return false
   127  	}
   128  	if l.configPathOrContent.Path() == "" {
   129  		return false
   130  	}
   131  	return true
   132  }
   133  
   134  func (l *Limiter) loadConfig() error {
   135  	config, err := ParseLimitConfigContent(l.configPathOrContent)
   136  	if err != nil {
   137  		return err
   138  	}
   139  	l.Lock()
   140  	defer l.Unlock()
   141  	maxWriteConcurrency := config.WriteLimits.GlobalLimits.MaxConcurrency
   142  	if maxWriteConcurrency > 0 {
   143  		l.writeGate = gate.New(
   144  			extprom.WrapRegistererWithPrefix(
   145  				"thanos_receive_write_request_concurrent_",
   146  				l.registerer,
   147  			),
   148  			int(maxWriteConcurrency),
   149  			gate.WriteRequests,
   150  		)
   151  	}
   152  	l.requestLimiter = newConfigRequestLimiter(
   153  		l.registerer,
   154  		&config.WriteLimits,
   155  	)
   156  	seriesLimitIsActivated := func() bool {
   157  		if config.WriteLimits.DefaultLimits.HeadSeriesLimit != 0 {
   158  			return true
   159  		}
   160  		for _, tenant := range config.WriteLimits.TenantsLimits {
   161  			if tenant.HeadSeriesLimit != nil && *tenant.HeadSeriesLimit != 0 {
   162  				return true
   163  			}
   164  		}
   165  		return false
   166  	}
   167  	if (l.receiverMode == RouterOnly || l.receiverMode == RouterIngestor) && seriesLimitIsActivated() {
   168  		l.HeadSeriesLimiter = NewHeadSeriesLimit(config.WriteLimits, l.registerer, l.logger)
   169  	}
   170  	return nil
   171  }
   172  
   173  // RequestLimiter is a safe getter for the request limiter.
   174  func (l *Limiter) RequestLimiter() requestLimiter {
   175  	l.RLock()
   176  	defer l.RUnlock()
   177  	return l.requestLimiter
   178  }
   179  
   180  // WriteGate is a safe getter for the write gate.
   181  func (l *Limiter) WriteGate() gate.Gate {
   182  	l.RLock()
   183  	defer l.RUnlock()
   184  	return l.writeGate
   185  }
   186  
   187  // ParseLimitConfigContent parses the limit configuration from the path or
   188  // content.
   189  func ParseLimitConfigContent(limitsConfig fileContent) (*RootLimitsConfig, error) {
   190  	if limitsConfig == nil {
   191  		return &RootLimitsConfig{}, nil
   192  	}
   193  	limitsContentYaml, err := limitsConfig.Content()
   194  	if err != nil {
   195  		return nil, errors.Wrap(err, "get content of limit configuration")
   196  	}
   197  	parsedConfig, err := ParseRootLimitConfig(limitsContentYaml)
   198  	if err != nil {
   199  		return nil, errors.Wrap(err, "parse limit configuration")
   200  	}
   201  	return parsedConfig, nil
   202  }
   203  
   204  type nopConfigContent struct{}
   205  
   206  var _ fileContent = (*nopConfigContent)(nil)
   207  
   208  // Content returns no content and no error.
   209  func (n nopConfigContent) Content() ([]byte, error) {
   210  	return nil, nil
   211  }
   212  
   213  // Path returns an empty path.
   214  func (n nopConfigContent) Path() string {
   215  	return ""
   216  }
   217  
   218  // NewNopConfig creates a no-op config content (no configuration).
   219  func NewNopConfig() nopConfigContent {
   220  	return nopConfigContent{}
   221  }