// SPDX-License-Identifier: AGPL-3.0-only
// Provenance-includes-location: https://github.com/cortexproject/cortex/blob/master/pkg/storage/tsdb/config.go
// Provenance-includes-license: Apache-2.0
// Provenance-includes-copyright: The Cortex Authors.

package tsdb

import (
	"flag"
	"fmt"
	"path/filepath"
	"strings"
	"time"

	"github.com/alecthomas/units"
	"github.com/pkg/errors"
	"github.com/prometheus/prometheus/tsdb"
	"github.com/prometheus/prometheus/tsdb/chunks"
	"github.com/prometheus/prometheus/tsdb/wlog"
	"github.com/prometheus/prometheus/util/compression"

	"github.com/grafana/mimir/pkg/ingester/activeseries"
	"github.com/grafana/mimir/pkg/ingester/lookupplan"
	"github.com/grafana/mimir/pkg/storage/bucket"
	"github.com/grafana/mimir/pkg/storage/indexheader"
)

const (
	// DefaultCloseIdleTSDBInterval is how often are open TSDBs checked for being idle and closed.
	DefaultCloseIdleTSDBInterval = 5 * time.Minute

	// DeletionMarkCheckInterval is how often to check for tenant deletion mark.
	DeletionMarkCheckInterval = 1 * time.Hour

	// EstimatedMaxChunkSize is average max of chunk size. This can be exceeded though in very rare (valid) cases.
	// This changed in prometheus as of https://github.com/prometheus/prometheus/commit/8ef7dfdeebf0a7491973303c7fb6b68ec5cc065b
	// which capped the max XOR and histogram chunk size to 1KiB.
	// Once block with this limit become more common this constant can be revisited.
	EstimatedMaxChunkSize = 16000

	// EstimatedSeriesP99Size is the size in bytes of a single series in the TSDB index. This includes the symbol IDs in
	// the symbols table (not the actual label strings) and the chunk refs (min time, max time, offset).
	// This is an estimation that should cover >99% of series with less than 30 labels and around 50 chunks per series.
	EstimatedSeriesP99Size = 512

	// MaxSeriesSize is an estimated worst case for a series size in the index. A valid series may still be larger than this.
	// This was calculated assuming a rate of 1 sample/sec, in a 24h block we have 744 chunks per series.
	// The worst case scenario of each meta ref is 8*3=24 bytes, so 744*24 = 17856 bytes, which is 448 bytes away form 17 KiB.
	MaxSeriesSize = 17 * 1024

	// BytesPerPostingInAPostingList is the number of bytes that each posting (series ID) takes in a
	// posting list in the index. Each posting is 4 bytes (uint32) which are the offset of the series in the index file.
	BytesPerPostingInAPostingList = 4

	// DefaultPostingOffsetInMemorySampling represents default value for -blocks-storage.bucket-store.posting-offsets-in-mem-sampling.
	// 32 value is chosen as it's a good balance for common setups. Sampling that is not too large (too many CPU cycles) and
	// not too small (too much memory).
	//
	// This also matches the in-memory sampling of postings offsets of the prometheus TSDB for on-disk blocks.
	// At the time of writing, this is the symbolFactor (misnomer) in /prometheus/tsdb/index/index.go.
	DefaultPostingOffsetInMemorySampling = 32

	// DefaultPostingsForMatchersCacheMaxBytes setting puts a limit cap on the per-TSDB head/block max PostingsForMatchers() cache size in bytes.
	// This limit is *in addition* to the max size in items (default to 100 items).
	//
	// This value is most useful on Mimir clusters with 1 tenant. If the tenant runs very high cardinality
	// queries (e.g. a query touching 1M series / ingester) then with the previous default cache
	// size of 10MB we may not be able to effectively use the cache.
	//
	// A single cached posting takes about 9 bytes in the cache, on average. The default max cache size as number of items
	// is 100, so having a 100MB cache per-tenant for the TSDB Head means we can cache 100MB / 100 / 9 = 116k postings
	// per cached entry on average.
	DefaultPostingsForMatchersCacheMaxBytes = 100 * 1024 * 1024

	// DefaultPartitionerMaxGapSize is the default max size - in bytes - of a gap for which the store-gateway
	// partitioner aggregates together two bucket GET object requests.
	DefaultPartitionerMaxGapSize = uint64(512 * 1024)

	// NewBlockDiscoveryDelayMultiplier is the factor used (multiplied with BucketStoreConfig.SyncInterval) to determine
	// when querying a newly uploaded block is required.
	// For example, if BucketStoreConfig.SyncInterval is 15 minutes, then a querier will require that it can query all
	// blocks uploaded at least 45 minutes ago, and not fail queries when a store-gateway does not respond to queries for
	// newer blocks (those uploaded in the last 45 minutes).
	// This gives store-gateways time to discover and load newly created blocks.
	NewBlockDiscoveryDelayMultiplier = 3

	headChunksEndTimeVarianceHelp = "How much variance (as percentage between 0 and 1) should be applied to the chunk end time, to spread chunks writing across time. Doesn't apply to the last chunk of the chunk range. 0 means no variance."
	headStripeSizeHelp            = "The number of shards of series to use in TSDB (must be a power of 2). Reducing this will decrease memory footprint, but can negatively impact performance."
	headChunksWriteQueueSizeHelp  = "The size of the write queue used by the head chunks mapper. Lower values reduce memory utilisation at the cost of potentially higher ingest latency. Value of 0 switches chunks mapper to implementation without a queue."

	headCompactionIntervalFlag = "blocks-storage.tsdb.head-compaction-interval"

	DefaultMaxTSDBOpeningConcurrencyOnStartup = 10

	bucketIndexFlagPrefix = "blocks-storage.bucket-store.bucket-index."

	syncIntervalFlag                           = "blocks-storage.bucket-store.sync-interval"
	ignoreDeletionMarksInStoreGatewayDelayFlag = "blocks-storage.bucket-store.ignore-deletion-marks-delay"
	ignoreDeletionMarksWhileQueryingDelayFlag  = "blocks-storage.bucket-store.ignore-deletion-marks-while-querying-delay"
)

// Validation errors
var (
	errInvalidShipConcurrency                       = errors.New("invalid TSDB ship concurrency")
	errInvalidCompactionInterval                    = errors.New("invalid TSDB compaction interval")
	errInvalidCompactionConcurrency                 = errors.New("invalid TSDB compaction concurrency")
	errInvalidWALSegmentSizeBytes                   = errors.New("invalid TSDB WAL segment size bytes")
	errInvalidWALReplayConcurrency                  = errors.New("invalid TSDB WAL replay concurrency")
	errInvalidStripeSize                            = errors.New("invalid TSDB stripe size")
	errInvalidStreamingBatchSize                    = errors.New("invalid store-gateway streaming batch size")
	errInvalidEarlyHeadCompactionMinSeriesReduction = errors.New("early compaction minimum series reduction percentage must be a value between 0 and 100 (included)")
	errEarlyCompactionRequiresActiveSeries          = fmt.Errorf("early compaction requires -%s to be enabled", activeseries.EnabledFlag)
	errEmptyBlockranges                             = errors.New("empty block ranges for TSDB")
	errInvalidIgnoreDeletionMarksDelayConfig        = fmt.Errorf("value for -%s must be less than -%s", ignoreDeletionMarksWhileQueryingDelayFlag, ignoreDeletionMarksInStoreGatewayDelayFlag)
	errIgnoreDeletionMarksDelayTooShort             = fmt.Errorf("value for -%s must be greater than %v× -%s to ensure that newly compacted blocks are queried before old blocks are ignored", ignoreDeletionMarksWhileQueryingDelayFlag, NewBlockDiscoveryDelayMultiplier, syncIntervalFlag)
)

// BlocksStorageConfig holds the config information for the blocks storage.
type BlocksStorageConfig struct {
	Bucket      bucket.Config     `yaml:",inline"`
	BucketStore BucketStoreConfig `yaml:"bucket_store" doc:"description=This configures how the querier and store-gateway discover and synchronize blocks stored in the bucket."`
	TSDB        TSDBConfig        `yaml:"tsdb"`
}

// DurationList is the block ranges for a tsdb
type DurationList []time.Duration

// String implements the flag.Value interface
func (d *DurationList) String() string {
	values := make([]string, 0, len(*d))
	for _, v := range *d {
		values = append(values, v.String())
	}

	return strings.Join(values, ",")
}

// Set implements the flag.Value interface
func (d *DurationList) Set(s string) error {
	values := strings.Split(s, ",")
	*d = make([]time.Duration, 0, len(values)) // flag.Parse may be called twice, so overwrite instead of append
	for _, v := range values {
		t, err := time.ParseDuration(v)
		if err != nil {
			return err
		}
		*d = append(*d, t)
	}
	return nil
}

// ToMilliseconds returns the duration list in milliseconds
func (d *DurationList) ToMilliseconds() []int64 {
	values := make([]int64, 0, len(*d))
	for _, t := range *d {
		values = append(values, t.Milliseconds())
	}

	return values
}

// RegisterFlags registers the TSDB flags
func (cfg *BlocksStorageConfig) RegisterFlags(f *flag.FlagSet) {
	cfg.Bucket.RegisterFlagsWithPrefixAndDefaultDirectory("blocks-storage.", "blocks", f)
	cfg.BucketStore.RegisterFlags(f)
	cfg.TSDB.RegisterFlags(f)
}

// Validate the config.
func (cfg *BlocksStorageConfig) Validate(activeSeriesCfg activeseries.Config) error {
	if err := cfg.Bucket.Validate(); err != nil {
		return err
	}

	if err := cfg.TSDB.Validate(activeSeriesCfg); err != nil {
		return err
	}

	return cfg.BucketStore.Validate()
}

// TSDBConfig holds the config for TSDB opened in the ingesters.
//
//nolint:revive
type TSDBConfig struct {
	Dir                                 string        `yaml:"dir"`
	BlockRanges                         DurationList  `yaml:"block_ranges_period" category:"experimental" doc:"hidden"`
	Retention                           time.Duration `yaml:"retention_period"`
	ShipInterval                        time.Duration `yaml:"ship_interval" category:"advanced"`
	ShipConcurrency                     int           `yaml:"ship_concurrency" category:"advanced"`
	HeadCompactionInterval              time.Duration `yaml:"head_compaction_interval" category:"advanced"`
	HeadCompactionConcurrency           int           `yaml:"head_compaction_concurrency" category:"advanced"`
	HeadCompactionIdleTimeout           time.Duration `yaml:"head_compaction_idle_timeout" category:"advanced"`
	HeadChunksWriteBufferSize           int           `yaml:"head_chunks_write_buffer_size_bytes" category:"advanced"`
	HeadChunksEndTimeVariance           float64       `yaml:"head_chunks_end_time_variance" category:"experimental"`
	StripeSize                          int           `yaml:"stripe_size" category:"advanced"`
	WALCompressionEnabled               bool          `yaml:"wal_compression_enabled" category:"advanced"`
	WALSegmentSizeBytes                 int           `yaml:"wal_segment_size_bytes" category:"advanced"`
	WALReplayConcurrency                int           `yaml:"wal_replay_concurrency" category:"advanced"`
	FlushBlocksOnShutdown               bool          `yaml:"flush_blocks_on_shutdown" category:"advanced"`
	CloseIdleTSDBTimeout                time.Duration `yaml:"close_idle_tsdb_timeout" category:"advanced"`
	MemorySnapshotOnShutdown            bool          `yaml:"memory_snapshot_on_shutdown" category:"experimental"`
	HeadChunksWriteQueueSize            int           `yaml:"head_chunks_write_queue_size" category:"advanced"`
	BiggerOutOfOrderBlocksForOldSamples bool          `yaml:"bigger_out_of_order_blocks_for_old_samples" category:"experimental"`

	// Series hash cache.
	SeriesHashCacheMaxBytes uint64 `yaml:"series_hash_cache_max_size_bytes" category:"advanced"`

	// If true, user TSDBs are not closed on shutdown. Only for testing.
	// If false (default), user TSDBs are closed to make sure all resources are released and closed properly.
	KeepUserTSDBOpenOnShutdown bool `yaml:"-"`

	// How often to check for idle TSDBs for closing. DefaultCloseIdleTSDBInterval is not suitable for testing, so tests can override.
	CloseIdleTSDBInterval time.Duration `yaml:"-"`

	// For experimental out of order metrics support.
	OutOfOrderCapacityMax int `yaml:"out_of_order_capacity_max" category:"experimental"`

	// SharedPostingsForMatchersCache indicates whether the PostingsForMatchersCache should be shared across blocks, as
	// opposed to instantiated per block. With a shared cache, one cache is created for head blocks, and one for non-head blocks.
	SharedPostingsForMatchersCache bool `yaml:"shared_postings_for_matchers_cache" category:"experimental"`

	// HeadPostingsForMatchersCacheInvalidation indicates whether postings should be tracked and invalidated when they change.
	HeadPostingsForMatchersCacheInvalidation bool `yaml:"head_postings_for_matchers_cache_invalidation" category:"experimental"`

	// HeadPostingsForMatchersCacheVersions is the number of metricVersions to store in the cache
	HeadPostingsForMatchersCacheVersions int `yaml:"head_postings_for_matchers_cache_versions" category:"experimental"`

	// HeadPostingsForMatchersCacheTTL is the TTL of the postings for matchers cache in the Head.
	// If it's 0, the cache will only deduplicate in-flight requests, deleting the results once the first request has finished.
	HeadPostingsForMatchersCacheTTL time.Duration `yaml:"head_postings_for_matchers_cache_ttl" category:"advanced"`

	// HeadPostingsForMatchersCacheMaxItems is the maximum size (in number of items) of cached postings for matchers elements in the Head.
	// It's ignored used when HeadPostingsForMatchersCacheTTL is 0.
	// Deprecated: use max bytes limit instead.
	HeadPostingsForMatchersCacheMaxItems int `yaml:"head_postings_for_matchers_cache_size" category:"deprecated"`

	// HeadPostingsForMatchersCacheMaxBytes is the maximum size (in bytes) of cached postings for matchers elements in the Head.
	// It's ignored used when HeadPostingsForMatchersCacheTTL is 0.
	HeadPostingsForMatchersCacheMaxBytes int64 `yaml:"head_postings_for_matchers_cache_max_bytes" category:"advanced"`

	// HeadPostingsForMatchersCacheForce forces the usage of postings for matchers cache for all calls on Head and OOOHead regardless of the `concurrent` param.
	HeadPostingsForMatchersCacheForce bool `yaml:"head_postings_for_matchers_cache_force" category:"advanced"`

	// BlockPostingsForMatchersCacheTTL is the TTL of the postings for matchers cache in each compacted block.
	// If it's 0, the cache will only deduplicate in-flight requests, deleting the results once the first request has finished.
	BlockPostingsForMatchersCacheTTL time.Duration `yaml:"block_postings_for_matchers_cache_ttl" category:"advanced"`

	// BlockPostingsForMatchersCacheMaxItems is the maximum size of cached postings for matchers elements in each compacted block.
	// It's ignored used when BlockPostingsForMatchersCacheTTL is 0.
	// Deprecated: use max bytes limit instead.
	BlockPostingsForMatchersCacheMaxItems int `yaml:"block_postings_for_matchers_cache_size" category:"deprecated"`

	// BlockPostingsForMatchersCacheMaxBytes is the maximum size (in bytes) of cached postings for matchers elements in each compacted block.
	// It's ignored used when BlockPostingsForMatchersCacheTTL is 0.
	BlockPostingsForMatchersCacheMaxBytes int64 `yaml:"block_postings_for_matchers_cache_max_bytes" category:"advanced"`

	// BlockPostingsForMatchersCacheForce forces the usage of postings for matchers cache for all calls compacted blocks
	// regardless of the `concurrent` param.
	BlockPostingsForMatchersCacheForce bool `yaml:"block_postings_for_matchers_cache_force" category:"advanced"`

	EarlyHeadCompactionMinInMemorySeries                     int64 `yaml:"early_head_compaction_min_in_memory_series" category:"experimental"`
	EarlyHeadCompactionMinEstimatedSeriesReductionPercentage int   `yaml:"early_head_compaction_min_estimated_series_reduction_percentage" category:"experimental"`

	// HeadCompactionIntervalJitterEnabled is enabled by default, but allows to disable it in tests.
	HeadCompactionIntervalJitterEnabled bool `yaml:"-"`

	// HeadCompactionIntervalWhileStarting setting is hardcoded, but allowed to overwrite it in tests.
	HeadCompactionIntervalWhileStarting time.Duration `yaml:"-"`

	// TimelyHeadCompaction allows head compaction to happen when min block range can no longer be appended,
	// without requiring 1.5x the chunk range worth of data in the head.
	TimelyHeadCompaction bool `yaml:"timely_head_compaction_enabled" category:"experimental"`

	IndexLookupPlanning struct {
		lookupplan.CostConfig `yaml:",inline"`

		// Enabled controls the collection of statistics and whether to defer some vector selector matchers to sequential scans.
		// This leads to better performance.
		Enabled bool `yaml:"index_lookup_planning_enabled" category:"experimental"`

		// ComparisonPortion controls the portion of queries where a mirrored chunk querier is used to compare
		// results between queries with and without index lookup planning enabled. The value must be between 0 and 1, where 0
		// disables comparison and 1 enables it for all queries.
		ComparisonPortion float64 `yaml:"index_lookup_planning_comparison_portion" category:"experimental"`

		StatisticsCollectionFrequency time.Duration `yaml:"statistics_collection_frequency" category:"experimental"`
	} `yaml:"index_lookup_planning" category:"experimental"`
}

// RegisterFlags registers the TSDBConfig flags.
func (cfg *TSDBConfig) RegisterFlags(f *flag.FlagSet) {
	if len(cfg.BlockRanges) == 0 {
		cfg.BlockRanges = []time.Duration{2 * time.Hour} // Default 2h block
	}

	f.StringVar(&cfg.Dir, "blocks-storage.tsdb.dir", "./tsdb/", "Directory to store TSDBs (including WAL) in the ingesters. This directory is required to be persisted between restarts.")
	f.Var(&cfg.BlockRanges, "blocks-storage.tsdb.block-ranges-period", "TSDB blocks range period.")
	f.DurationVar(&cfg.Retention, "blocks-storage.tsdb.retention-period", 13*time.Hour, "TSDB blocks retention in the ingester before a block is removed. If shipping is enabled, the retention will be relative to the time when the block was uploaded to storage. If shipping is disabled then its relative to the creation time of the block. This should be larger than the -blocks-storage.tsdb.block-ranges-period, -querier.query-store-after and large enough to give store-gateways and queriers enough time to discover newly uploaded blocks.")
	f.DurationVar(&cfg.ShipInterval, "blocks-storage.tsdb.ship-interval", 1*time.Minute, "How frequently the TSDB blocks are scanned and new ones are shipped to the storage. 0 means shipping is disabled.")
	f.IntVar(&cfg.ShipConcurrency, "blocks-storage.tsdb.ship-concurrency", 10, "Maximum number of tenants concurrently shipping blocks to the storage.")

	// This cache is only used when querying compacted blocks. The default cache size is enough to store the hashes for
	// all series in all queryable blocks, assuming 2M series per ingester (and default retention):
	//
	// ceil(13h retention - 2h head) / 2h blocks * 2M series * 28 bytes = 320MB, rounded to 350M
	f.Uint64Var(&cfg.SeriesHashCacheMaxBytes, "blocks-storage.tsdb.series-hash-cache-max-size-bytes", uint64(350*units.Mebibyte), "Max size - in bytes - of the in-memory series hash cache. The cache is shared across all tenants and it's used only when query sharding is enabled.")

	f.DurationVar(&cfg.HeadCompactionInterval, headCompactionIntervalFlag, 1*time.Minute, "How frequently the ingester checks whether the TSDB head should be compacted and, if so, triggers the compaction. Mimir applies a jitter to the first check, and subsequent checks will happen at the configured interval. A block is only created if data covers the smallest block range. The configured interval must be between 0 and 15 minutes.")
	f.IntVar(&cfg.HeadCompactionConcurrency, "blocks-storage.tsdb.head-compaction-concurrency", 1, "Maximum number of tenants concurrently compacting TSDB head into a new block")
	f.DurationVar(&cfg.HeadCompactionIdleTimeout, "blocks-storage.tsdb.head-compaction-idle-timeout", 1*time.Hour, "If TSDB head is idle for this duration, it is compacted. Note that up to 25% jitter is added to the value to avoid ingesters compacting concurrently. 0 means disabled.")
	f.IntVar(&cfg.HeadChunksWriteBufferSize, "blocks-storage.tsdb.head-chunks-write-buffer-size-bytes", chunks.DefaultWriteBufferSize, fmt.Sprintf("The write buffer size used by the head chunks mapper. Lower values reduce memory utilisation on clusters with a large number of tenants at the cost of increased disk I/O operations. The configured buffer size must be between %d and %d.", chunks.MinWriteBufferSize, chunks.MaxWriteBufferSize))
	f.Float64Var(&cfg.HeadChunksEndTimeVariance, "blocks-storage.tsdb.head-chunks-end-time-variance", 0, headChunksEndTimeVarianceHelp)
	f.IntVar(&cfg.StripeSize, "blocks-storage.tsdb.stripe-size", 16384, headStripeSizeHelp)
	f.BoolVar(&cfg.WALCompressionEnabled, "blocks-storage.tsdb.wal-compression-enabled", false, "True to enable TSDB WAL compression.")
	f.IntVar(&cfg.WALSegmentSizeBytes, "blocks-storage.tsdb.wal-segment-size-bytes", wlog.DefaultSegmentSize, "TSDB WAL segments files max size (bytes).")
	f.IntVar(&cfg.WALReplayConcurrency, "blocks-storage.tsdb.wal-replay-concurrency", 0, "Maximum number of CPUs that can simultaneously processes WAL replay. If it is set to 0, then each TSDB is replayed with a concurrency equal to the number of CPU cores available on the machine.")
	f.BoolVar(&cfg.FlushBlocksOnShutdown, "blocks-storage.tsdb.flush-blocks-on-shutdown", false, "True to flush blocks to storage on shutdown. If false, incomplete blocks will be reused after restart.")
	f.DurationVar(&cfg.CloseIdleTSDBTimeout, "blocks-storage.tsdb.close-idle-tsdb-timeout", 13*time.Hour, "If TSDB has not received any data for this duration, and all blocks from TSDB have been shipped, TSDB is closed and deleted from local disk. If set to positive value, this value should be equal or higher than -querier.query-ingesters-within flag to make sure that TSDB is not closed prematurely, which could cause partial query results. 0 or negative value disables closing of idle TSDB.")
	f.BoolVar(&cfg.MemorySnapshotOnShutdown, "blocks-storage.tsdb.memory-snapshot-on-shutdown", false, "True to enable snapshotting of in-memory TSDB data on disk when shutting down.")
	f.IntVar(&cfg.HeadChunksWriteQueueSize, "blocks-storage.tsdb.head-chunks-write-queue-size", 1000000, headChunksWriteQueueSizeHelp)
	f.IntVar(&cfg.OutOfOrderCapacityMax, "blocks-storage.tsdb.out-of-order-capacity-max", 32, "Maximum capacity for out of order chunks, in samples between 1 and 255.")
	f.BoolVar(&cfg.SharedPostingsForMatchersCache, "blocks-storage.tsdb.shared-postings-for-matchers-cache", false, "Whether postings for matchers cache should be shared across blocks, as opposed to instantiated per block. With a shared cache, one cache is created for head blocks, and one for compacted blocks.")
	f.BoolVar(&cfg.HeadPostingsForMatchersCacheInvalidation, "blocks-storage.tsdb.head-postings-for-matchers-cache-invalidation", false, "Whether head block postings should be tracked and invalidated when they change, allowing higher TTLs to be used. When not using invalidation, cache entries will be used until removed.")
	f.IntVar(&cfg.HeadPostingsForMatchersCacheVersions, "blocks-storage.tsdb.head-postings-for-matchers-cache-versions", tsdb.DefaultPostingsForMatchersCacheVersions, "The size of the metric versions cache in each ingester when invalidation is enabled.")
	f.DurationVar(&cfg.HeadPostingsForMatchersCacheTTL, "blocks-storage.tsdb.head-postings-for-matchers-cache-ttl", tsdb.DefaultPostingsForMatchersCacheTTL, "How long to cache postings for matchers in the Head and OOOHead. Set to 0 to disable the cache and only deduplicate in-flight calls.")
	f.IntVar(&cfg.HeadPostingsForMatchersCacheMaxItems, "blocks-storage.tsdb.head-postings-for-matchers-cache-size", tsdb.DefaultPostingsForMatchersCacheMaxItems, "Maximum number of entries in the cache for postings for matchers in the Head and OOOHead when TTL is greater than 0.")
	f.Int64Var(&cfg.HeadPostingsForMatchersCacheMaxBytes, "blocks-storage.tsdb.head-postings-for-matchers-cache-max-bytes", DefaultPostingsForMatchersCacheMaxBytes, "Maximum size, in bytes, of the cache for postings for matchers in each compacted block when the TTL is greater than 0.")
	f.BoolVar(&cfg.HeadPostingsForMatchersCacheForce, "blocks-storage.tsdb.head-postings-for-matchers-cache-force", tsdb.DefaultPostingsForMatchersCacheForce, "Force the cache to be used for postings for matchers in the Head and OOOHead, even if it's not a concurrent (query-sharding) call.")
	f.DurationVar(&cfg.BlockPostingsForMatchersCacheTTL, "blocks-storage.tsdb.block-postings-for-matchers-cache-ttl", tsdb.DefaultPostingsForMatchersCacheTTL, "How long to cache postings for matchers in each compacted block queried from the ingester. 0 disables the cache and just deduplicates the in-flight calls.")
	f.IntVar(&cfg.BlockPostingsForMatchersCacheMaxItems, "blocks-storage.tsdb.block-postings-for-matchers-cache-size", tsdb.DefaultPostingsForMatchersCacheMaxItems, "Maximum number of entries in the cache for postings for matchers in each compacted block when TTL is greater than 0.")
	f.Int64Var(&cfg.BlockPostingsForMatchersCacheMaxBytes, "blocks-storage.tsdb.block-postings-for-matchers-cache-max-bytes", DefaultPostingsForMatchersCacheMaxBytes, "Maximum size in bytes of the cache for postings for matchers in each compacted block when TTL is greater than 0.")
	f.BoolVar(&cfg.BlockPostingsForMatchersCacheForce, "blocks-storage.tsdb.block-postings-for-matchers-cache-force", tsdb.DefaultPostingsForMatchersCacheForce, "Force the cache to be used for postings for matchers in compacted blocks, even if it's not a concurrent (query-sharding) call.")
	f.Int64Var(&cfg.EarlyHeadCompactionMinInMemorySeries, "blocks-storage.tsdb.early-head-compaction-min-in-memory-series", 0, fmt.Sprintf("When the number of in-memory series in the ingester is equal to or greater than this setting, the ingester tries to compact the TSDB Head. The early compaction removes from the memory all samples and inactive series up until -%s time ago. After an early compaction, the ingester will not accept any sample with a timestamp older than -%s time ago (unless out of order ingestion is enabled). The ingester checks every -%s whether an early compaction is required. Use 0 to disable it.", activeseries.IdleTimeoutFlag, activeseries.IdleTimeoutFlag, headCompactionIntervalFlag))
	f.IntVar(&cfg.EarlyHeadCompactionMinEstimatedSeriesReductionPercentage, "blocks-storage.tsdb.early-head-compaction-min-estimated-series-reduction-percentage", 15, "When the early compaction is enabled, the early compaction is triggered only if the estimated series reduction is at least the configured percentage (0-100).")
	f.BoolVar(&cfg.TimelyHeadCompaction, "blocks-storage.tsdb.timely-head-compaction-enabled", false, "Allows head compaction to happen when the min block range can no longer be appended, without requiring 1.5x the chunk range worth of data in the head.")
	f.BoolVar(&cfg.BiggerOutOfOrderBlocksForOldSamples, "blocks-storage.tsdb.bigger-out-of-order-blocks-for-old-samples", false, "When enabled, ingester produces 24h blocks for out-of-order data that is before the current day, instead of the usual 2h blocks.")
	f.BoolVar(&cfg.IndexLookupPlanning.Enabled, "blocks-storage.tsdb.index-lookup-planning.enabled", false, "Controls the collection of statistics and whether to defer some vector selector matchers to sequential scans. This leads to better performance.")
	f.DurationVar(&cfg.IndexLookupPlanning.StatisticsCollectionFrequency, "blocks-storage.tsdb.index-lookup-planning.statistics-collection-frequency", time.Hour, "How frequently to collect block statistics, which are used in query execution optimization. 0 to disable.")
	f.Float64Var(&cfg.IndexLookupPlanning.ComparisonPortion, "blocks-storage.tsdb.index-lookup-planning.comparison-portion", 0.0, "Portion of queries where a mirrored chunk querier compares results with and without index lookup planning. Value between 0 (disabled) and 1 (all queries).")
	cfg.IndexLookupPlanning.RegisterFlags(f, "blocks-storage.tsdb.index-lookup-planning.")

	cfg.HeadCompactionIntervalJitterEnabled = true
	cfg.HeadCompactionIntervalWhileStarting = 30 * time.Second
}

// Validate the config.
func (cfg *TSDBConfig) Validate(activeSeriesCfg activeseries.Config) error {
	if cfg.ShipInterval > 0 && cfg.ShipConcurrency <= 0 {
		return errInvalidShipConcurrency
	}

	if cfg.HeadCompactionInterval <= 0 || cfg.HeadCompactionInterval > 15*time.Minute {
		return errInvalidCompactionInterval
	}

	if cfg.HeadCompactionConcurrency <= 0 {
		return errInvalidCompactionConcurrency
	}

	if cfg.HeadChunksWriteBufferSize < chunks.MinWriteBufferSize || cfg.HeadChunksWriteBufferSize > chunks.MaxWriteBufferSize || cfg.HeadChunksWriteBufferSize%1024 != 0 {
		return errors.Errorf("head chunks write buffer size must be a multiple of 1024 between %d and %d", chunks.MinWriteBufferSize, chunks.MaxWriteBufferSize)
	}

	if cfg.StripeSize <= 1 || (cfg.StripeSize&(cfg.StripeSize-1)) != 0 { // ensure stripe size is a positive power of 2
		return errInvalidStripeSize
	}

	if len(cfg.BlockRanges) == 0 {
		return errEmptyBlockranges
	}

	if cfg.WALSegmentSizeBytes <= 0 {
		return errInvalidWALSegmentSizeBytes
	}

	if cfg.WALReplayConcurrency < 0 {
		return errInvalidWALReplayConcurrency
	}

	if cfg.EarlyHeadCompactionMinInMemorySeries > 0 && !activeSeriesCfg.Enabled {
		return errEarlyCompactionRequiresActiveSeries
	}

	if cfg.EarlyHeadCompactionMinEstimatedSeriesReductionPercentage < 0 || cfg.EarlyHeadCompactionMinEstimatedSeriesReductionPercentage > 100 {
		return errInvalidEarlyHeadCompactionMinSeriesReduction
	}

	if cfg.IndexLookupPlanning.Enabled {
		if err := cfg.IndexLookupPlanning.Validate(); err != nil {
			return err
		}
		if cfg.IndexLookupPlanning.StatisticsCollectionFrequency <= 0 {
			return errors.Errorf("head statistics collection frequency must be a non-negative duration. 0 to disable")
		}
	}

	return nil
}

func (cfg *TSDBConfig) WALCompressionType() compression.Type {
	if cfg.WALCompressionEnabled {
		return compression.Snappy
	}

	return compression.None
}

// BlocksDir returns the directory path where TSDB blocks and wal should be
// stored by the ingester
func (cfg *TSDBConfig) BlocksDir(userID string) string {
	return filepath.Join(cfg.Dir, userID)
}

// IsShippingEnabled returns whether blocks shipping is enabled.
func (cfg *TSDBConfig) IsBlocksShippingEnabled() bool {
	return cfg.ShipInterval > 0
}

// BucketStoreConfig holds the config information for Bucket Stores used by the querier and store-gateway.
type BucketStoreConfig struct {
	SyncDir                                string              `yaml:"sync_dir"`
	SyncInterval                           time.Duration       `yaml:"sync_interval" category:"advanced"`
	MaxConcurrent                          int                 `yaml:"max_concurrent" category:"advanced"`
	MaxConcurrentQueueTimeout              time.Duration       `yaml:"max_concurrent_queue_timeout" category:"advanced"`
	TenantSyncConcurrency                  int                 `yaml:"tenant_sync_concurrency" category:"advanced"`
	BlockSyncConcurrency                   int                 `yaml:"block_sync_concurrency" category:"advanced"`
	MetaSyncConcurrency                    int                 `yaml:"meta_sync_concurrency" category:"advanced"`
	IndexCache                             IndexCacheConfig    `yaml:"index_cache"`
	ChunksCache                            ChunksCacheConfig   `yaml:"chunks_cache"`
	MetadataCache                          MetadataCacheConfig `yaml:"metadata_cache"`
	IgnoreDeletionMarksInStoreGatewayDelay time.Duration       `yaml:"ignore_deletion_mark_delay" category:"advanced"`
	IgnoreDeletionMarksWhileQueryingDelay  time.Duration       `yaml:"ignore_deletion_mark_while_querying_delay" category:"experimental"`
	BucketIndex                            BucketIndexConfig   `yaml:"bucket_index"`
	IgnoreBlocksWithin                     time.Duration       `yaml:"ignore_blocks_within" category:"advanced"`

	// Series hash cache.
	SeriesHashCacheMaxBytes uint64 `yaml:"series_hash_cache_max_size_bytes" category:"advanced"`

	// Controls the partitioner, used to aggregate multiple GET object API requests.
	PartitionerMaxGapBytes uint64 `yaml:"partitioner_max_gap_bytes" category:"advanced"`

	// Controls what is the ratio of postings offsets store will hold in memory.
	// Larger value will keep less offsets, which will increase CPU cycles needed for query touching those postings.
	// It's meant for setups that want low baseline memory pressure and where less traffic is expected.
	// On the contrary, smaller value will increase baseline memory usage, but improve latency slightly.
	// 1 will keep all in memory. Default value is the same as in Prometheus which gives a good balance.
	PostingOffsetsInMemSampling int `yaml:"postings_offsets_in_mem_sampling" category:"advanced"`

	// Controls advanced options for index-header file reading.
	IndexHeader indexheader.Config `yaml:"index_header" category:"advanced"`

	StreamingBatchSize    int     `yaml:"streaming_series_batch_size" category:"advanced"`
	SeriesFetchPreference float64 `yaml:"series_fetch_preference" category:"advanced"`
}

// RegisterFlags registers the BucketStore flags
func (cfg *BucketStoreConfig) RegisterFlags(f *flag.FlagSet) {
	cfg.IndexCache.RegisterFlagsWithPrefix(f, "blocks-storage.bucket-store.index-cache.")
	cfg.ChunksCache.RegisterFlagsWithPrefix(f, "blocks-storage.bucket-store.chunks-cache.")
	cfg.MetadataCache.RegisterFlagsWithPrefix(f, "blocks-storage.bucket-store.metadata-cache.")
	cfg.BucketIndex.RegisterFlagsWithPrefix(f, bucketIndexFlagPrefix)
	cfg.IndexHeader.RegisterFlagsWithPrefix(f, "blocks-storage.bucket-store.index-header.")

	f.StringVar(&cfg.SyncDir, "blocks-storage.bucket-store.sync-dir", "./tsdb-sync/", "Directory to store synchronized TSDB index headers. This directory is not required to be persisted between restarts, but it's highly recommended in order to improve the store-gateway startup time.")
	f.DurationVar(&cfg.SyncInterval, syncIntervalFlag, 15*time.Minute, "How frequently to scan the bucket, or to refresh the bucket index (if enabled), in order to look for changes (new blocks shipped by ingesters and blocks deleted by retention or compaction).")
	f.Uint64Var(&cfg.SeriesHashCacheMaxBytes, "blocks-storage.bucket-store.series-hash-cache-max-size-bytes", uint64(1*units.Gibibyte), "Max size - in bytes - of the in-memory series hash cache. The cache is shared across all tenants and it's used only when query sharding is enabled.")
	f.IntVar(&cfg.MaxConcurrent, "blocks-storage.bucket-store.max-concurrent", 200, "Max number of concurrent queries to execute against the long-term storage. The limit is shared across all tenants.")
	f.DurationVar(&cfg.MaxConcurrentQueueTimeout, "blocks-storage.bucket-store.max-concurrent-queue-timeout", 5*time.Second, "Timeout for the queue of queries waiting for execution. If the queue is full and the timeout is reached, the query will be retried on another store-gateway. 0 means no timeout and all queries will wait indefinitely for their turn.")
	f.IntVar(&cfg.TenantSyncConcurrency, "blocks-storage.bucket-store.tenant-sync-concurrency", 1, "Maximum number of concurrent tenants synching blocks.")
	f.IntVar(&cfg.BlockSyncConcurrency, "blocks-storage.bucket-store.block-sync-concurrency", 4, "Maximum number of concurrent blocks synching per tenant.")
	f.IntVar(&cfg.MetaSyncConcurrency, "blocks-storage.bucket-store.meta-sync-concurrency", 20, "Number of goroutines to use when syncing block meta files from object storage per tenant.")
	f.DurationVar(&cfg.IgnoreDeletionMarksInStoreGatewayDelay, ignoreDeletionMarksInStoreGatewayDelayFlag, time.Hour*1, "Duration after which the blocks marked for deletion will be filtered out while fetching blocks. "+
		"The idea of ignore-deletion-marks-delay is to ignore blocks that are marked for deletion with some delay. This ensures store can still serve blocks that are meant to be deleted but do not have a replacement yet.")
	f.DurationVar(&cfg.IgnoreDeletionMarksWhileQueryingDelay, ignoreDeletionMarksWhileQueryingDelayFlag, 50*time.Minute, "Duration after which blocks marked for deletion will still be queried. "+
		"This ensures queriers still query blocks that are meant to be deleted but do not have a replacement yet.")
	f.DurationVar(&cfg.IgnoreBlocksWithin, "blocks-storage.bucket-store.ignore-blocks-within", 10*time.Hour, "Blocks with minimum time within this duration are ignored, and not loaded by store-gateway. Useful when used together with -querier.query-store-after to prevent loading young blocks, because there are usually many of them (depending on number of ingesters) and they are not yet compacted. Negative values or 0 disable the filter.")
	f.IntVar(&cfg.PostingOffsetsInMemSampling, "blocks-storage.bucket-store.posting-offsets-in-mem-sampling", DefaultPostingOffsetInMemorySampling, "Controls what is the ratio of postings offsets that the store will hold in memory.")
	f.Uint64Var(&cfg.PartitionerMaxGapBytes, "blocks-storage.bucket-store.partitioner-max-gap-bytes", DefaultPartitionerMaxGapSize, "Max size - in bytes - of a gap for which the partitioner aggregates together two bucket GET object requests.")
	f.IntVar(&cfg.StreamingBatchSize, "blocks-storage.bucket-store.batch-series-size", 5000, "This option controls how many series to fetch per batch. The batch size must be greater than 0.")
	f.Float64Var(&cfg.SeriesFetchPreference, "blocks-storage.bucket-store.series-fetch-preference", 0.75, "This parameter controls the trade-off in fetching series versus fetching postings to fulfill a series request. Increasing the series preference results in fetching more series and reducing the volume of postings fetched. Reducing the series preference results in the opposite. Increase this parameter to reduce the rate of fetched series bytes (see \"Mimir / Queries\" dashboard) or API calls to the object store. Must be a positive floating point number.")
}

// Validate the config.
func (cfg *BucketStoreConfig) Validate() error {
	if cfg.StreamingBatchSize <= 0 {
		return errInvalidStreamingBatchSize
	}
	if cfg.IgnoreDeletionMarksWhileQueryingDelay >= cfg.IgnoreDeletionMarksInStoreGatewayDelay {
		// If we ignore deletion marks for longer while querying, we'll try to query blocks that store-gateways have
		// already unloaded, which will cause consistency check failures.
		return errInvalidIgnoreDeletionMarksDelayConfig
	}
	if cfg.IgnoreDeletionMarksWhileQueryingDelay <= NewBlockDiscoveryDelayMultiplier*cfg.SyncInterval {
		// If we ignore deletion marks for less time while querying, we may skip querying both newly compacted blocks
		// and their source blocks, and so missing querying some series.
		return errIgnoreDeletionMarksDelayTooShort
	}
	if err := cfg.IndexCache.Validate(); err != nil {
		return errors.Wrap(err, "index-cache configuration")
	}
	if err := cfg.ChunksCache.Validate(); err != nil {
		return errors.Wrap(err, "chunks-cache configuration")
	}
	if err := cfg.MetadataCache.Validate(); err != nil {
		return errors.Wrap(err, "metadata-cache configuration")
	}
	if err := cfg.BucketIndex.Validate(); err != nil {
		return errors.Wrap(err, "bucket-index configuration")
	}
	if cfg.SeriesFetchPreference <= 0 {
		return errors.New("invalid worst-case series preference; must be positive")
	}
	if err := cfg.IndexHeader.Validate(); err != nil {
		return errors.Wrap(err, "index-header configuration")
	}
	return nil
}

type BucketIndexConfig struct {
	UpdateOnErrorInterval time.Duration `yaml:"update_on_error_interval" category:"advanced"`
	IdleTimeout           time.Duration `yaml:"idle_timeout" category:"advanced"`
	MaxStalePeriod        time.Duration `yaml:"max_stale_period" category:"advanced"`
}

func (cfg *BucketIndexConfig) RegisterFlagsWithPrefix(f *flag.FlagSet, prefix string) {
	f.DurationVar(&cfg.UpdateOnErrorInterval, prefix+"update-on-error-interval", time.Minute, "How frequently a bucket index, which previously failed to load, should be tried to load again. This option is used only by querier.")
	f.DurationVar(&cfg.IdleTimeout, prefix+"idle-timeout", time.Hour, "How long a unused bucket index should be cached. Once this timeout expires, the unused bucket index is removed from the in-memory cache. This option is used only by querier.")
	f.DurationVar(&cfg.MaxStalePeriod, prefix+"max-stale-period", time.Hour, "The maximum allowed age of a bucket index (last updated) before queries start failing because the bucket index is too old. The bucket index is periodically updated by the compactor, and this check is enforced in the querier (at query time).")
}

// Validate the config.
func (cfg *BucketIndexConfig) Validate() error {
	return nil
}
