internal/memorylimiter/config.go

// Copyright The OpenTelemetry Authors
// SPDX-License-Identifier: Apache-2.0

package memorylimiter // import "go.opentelemetry.io/collector/internal/memorylimiter"

import (
	"errors"
	"time"

	"go.opentelemetry.io/collector/component"
	"go.opentelemetry.io/collector/config/configoptional"
)

var (
	errCheckIntervalOutOfRange        = errors.New("'check_interval' must be greater than zero")
	errInconsistentGCMinInterval      = errors.New("'min_gc_interval_when_soft_limited' should be larger than 'min_gc_interval_when_hard_limited'")
	errInconsistentGCMaxInterval      = errors.New("'max_gc_interval_when_soft_limited' should be larger than or equal to 'max_gc_interval_when_hard_limited' (when both are set)")
	errInconsistentGCMaxSoftInterval  = errors.New("'max_gc_interval_when_soft_limited' must be greater than or equal to 'min_gc_interval_when_soft_limited' (or 0 to disable)")
	errInconsistentGCMaxHardInterval  = errors.New("'max_gc_interval_when_hard_limited' must be greater than or equal to 'min_gc_interval_when_hard_limited' (or 0 to disable)")
	errNegativeGCMaxSoftInterval      = errors.New("'max_gc_interval_when_soft_limited' must be non-negative")
	errNegativeGCMaxHardInterval      = errors.New("'max_gc_interval_when_hard_limited' must be non-negative")
	errLimitOutOfRange                = errors.New("'limit_mib' or 'limit_percentage' must be greater than zero")
	errSpikeLimitOutOfRange           = errors.New("'spike_limit_mib' must be smaller than 'limit_mib'")
	errSpikeLimitPercentageOutOfRange = errors.New("'spike_limit_percentage' must be smaller than 'limit_percentage'")
	errLimitPercentageOutOfRange      = errors.New(
		"'limit_percentage' and 'spike_limit_percentage' must be greater than zero and less than or equal to hundred",
	)
)

// GarbageCollectorConfig defines configuration for garbage collection.
type GarbageCollectorConfig struct{}

// Config defines configuration for memory memoryLimiter processor.
type Config struct {
	// CheckInterval is the time between measurements of memory usage for the
	// purposes of avoiding going over the limits. Defaults to zero, so no
	// checks will be performed.
	CheckInterval time.Duration `mapstructure:"check_interval"`

	// MinGCIntervalWhenSoftLimited minimum interval between forced GC when in soft (=limit_mib - spike_limit_mib) limited mode.
	// Zero value means no minimum interval.
	// GCs is a CPU-heavy operation and executing it too frequently may affect the recovery capabilities of the collector.
	MinGCIntervalWhenSoftLimited time.Duration `mapstructure:"min_gc_interval_when_soft_limited"`

	// MinGCIntervalWhenHardLimited minimum interval between forced GC when in hard (=limit_mib) limited mode.
	// Zero value means no minimum interval.
	// GCs is a CPU-heavy operation and executing it too frequently may affect the recovery capabilities of the collector.
	MinGCIntervalWhenHardLimited time.Duration `mapstructure:"min_gc_interval_when_hard_limited"`

	// MaxGCIntervalWhenSoftLimited caps the exponential backoff between forced
	// GC calls in soft-limited mode. When a forced GC fails to reclaim memory
	// (e.g., held by live references in exporter queues during a downstream
	// outage), the interval doubles starting from min_gc_interval_when_soft_limited
	// (or 95% of check_interval, whichever is larger) up to this cap. It resets
	// to zero whenever a GC is effective or memory drops on its own.
	// Set to 0 to disable the exponential backoff on this path; the interval
	// will not grow beyond the configured min.
	MaxGCIntervalWhenSoftLimited time.Duration `mapstructure:"max_gc_interval_when_soft_limited"`

	// MaxGCIntervalWhenHardLimited caps the exponential backoff between forced
	// GC calls in hard-limited mode. Same semantics as
	// MaxGCIntervalWhenSoftLimited but applies to the hard-limit path.
	// Set to 0 to disable the exponential backoff on this path.
	MaxGCIntervalWhenHardLimited time.Duration `mapstructure:"max_gc_interval_when_hard_limited"`

	// GarbageCollector configuration section. Use enabled: false to disable the memory limiter's forced garbage collection.
	GarbageCollector configoptional.Optional[GarbageCollectorConfig] `mapstructure:"garbage_collector"`

	// MemoryLimitMiB is the maximum amount of memory, in MiB, targeted to be
	// allocated by the process.
	MemoryLimitMiB uint32 `mapstructure:"limit_mib"`

	// MemorySpikeLimitMiB is the maximum, in MiB, spike expected between the
	// measurements of memory usage.
	MemorySpikeLimitMiB uint32 `mapstructure:"spike_limit_mib"`

	// MemoryLimitPercentage is the maximum amount of memory, in %, targeted to be
	// allocated by the process. The fixed memory settings MemoryLimitMiB has a higher precedence.
	MemoryLimitPercentage uint32 `mapstructure:"limit_percentage"`

	// MemorySpikePercentage is the maximum, in percents against the total memory,
	// spike expected between the measurements of memory usage.
	MemorySpikePercentage uint32 `mapstructure:"spike_limit_percentage"`
}

var _ component.Config = (*Config)(nil)

func NewDefaultConfig() *Config {
	return &Config{
		MinGCIntervalWhenSoftLimited: 10 * time.Second,
		MaxGCIntervalWhenSoftLimited: 30 * time.Second,
		MaxGCIntervalWhenHardLimited: 30 * time.Second,
		GarbageCollector:             configoptional.Some(GarbageCollectorConfig{}),
	}
}

// Validate checks if the processor configuration is valid
func (cfg *Config) Validate() error {
	if cfg.CheckInterval <= 0 {
		return errCheckIntervalOutOfRange
	}
	if cfg.MinGCIntervalWhenSoftLimited < cfg.MinGCIntervalWhenHardLimited {
		return errInconsistentGCMinInterval
	}
	if cfg.MaxGCIntervalWhenSoftLimited < 0 {
		return errNegativeGCMaxSoftInterval
	}
	if cfg.MaxGCIntervalWhenHardLimited < 0 {
		return errNegativeGCMaxHardInterval
	}
	if cfg.MaxGCIntervalWhenSoftLimited > 0 && cfg.MaxGCIntervalWhenSoftLimited < cfg.MinGCIntervalWhenSoftLimited {
		return errInconsistentGCMaxSoftInterval
	}
	if cfg.MaxGCIntervalWhenHardLimited > 0 && cfg.MaxGCIntervalWhenHardLimited < cfg.MinGCIntervalWhenHardLimited {
		return errInconsistentGCMaxHardInterval
	}
	// Mirror the MinSoft >= MinHard invariant for the max pair: when both
	// caps are enabled, the soft cap should not be tighter than the hard cap
	// (it would mean a smaller backoff ceiling for the less-urgent path,
	// which is almost always a typo).
	if cfg.MaxGCIntervalWhenSoftLimited > 0 && cfg.MaxGCIntervalWhenHardLimited > 0 &&
		cfg.MaxGCIntervalWhenSoftLimited < cfg.MaxGCIntervalWhenHardLimited {
		return errInconsistentGCMaxInterval
	}
	if cfg.MemoryLimitMiB == 0 && cfg.MemoryLimitPercentage == 0 {
		return errLimitOutOfRange
	}
	if cfg.MemoryLimitPercentage > 100 || cfg.MemorySpikePercentage > 100 {
		return errLimitPercentageOutOfRange
	}
	if cfg.MemoryLimitMiB > 0 && cfg.MemoryLimitMiB <= cfg.MemorySpikeLimitMiB {
		return errSpikeLimitOutOfRange
	}
	if cfg.MemoryLimitPercentage > 0 && cfg.MemoryLimitPercentage <= cfg.MemorySpikePercentage {
		return errSpikeLimitPercentageOutOfRange
	}
	return nil
}