Files
Joe ElliottandGitHub 65ae24f720 BugFix: Correctly apply trace idle period in ingesters and add a max live trace period (#5346)
* Add concept of live trace max lifetime and use in local blocks

Signed-off-by: Joe Elliott <[email protected]>

* remove dead code

Signed-off-by: Joe Elliott <[email protected]>

* add live cut time to ingesters

Signed-off-by: Joe Elliott <[email protected]>

* live trace period

Signed-off-by: Joe Elliott <[email protected]>

* docs

Signed-off-by: Joe Elliott <[email protected]>

* changelog

Signed-off-by: Joe Elliott <[email protected]>

* clean up search tags e2e

Signed-off-by: Joe Elliott <[email protected]>

* more e2e patches

Signed-off-by: Joe Elliott <[email protected]>

* remove unnecessary asserts

Signed-off-by: Joe Elliott <[email protected]>

---------

Signed-off-by: Joe Elliott <[email protected]>
2025-07-02 08:45:50 -04:00

106 lines
3.0 KiB
Go

package ingester
import (
"fmt"
"math"
"github.com/grafana/tempo/modules/overrides"
)
const (
errMaxTracesPerUserLimitExceeded = "per-user traces limit (local: %d global: %d actual local: %d) exceeded"
)
// RingCount is the interface exposed by a ring implementation which allows
// to count members
type RingCount interface {
HealthyInstancesCount() int
}
type Limiter interface {
AssertMaxTracesPerUser(userID string, traces int) error
Limits() overrides.Interface
}
// Limiter implements primitives to get the maximum number of traces
// an ingester can handle for a specific tenant
type limiter struct {
limits overrides.Interface
ring RingCount
replicationFactor int
}
// NewLimiter makes a new limiter
func NewLimiter(limits overrides.Interface, ring RingCount, replicationFactor int) Limiter {
return &limiter{
limits: limits,
ring: ring,
replicationFactor: replicationFactor,
}
}
func (l *limiter) Limits() overrides.Interface {
return l.limits
}
// AssertMaxTracesPerUser ensures limit has not been reached compared to the current
// number of streams in input and returns an error if so.
func (l *limiter) AssertMaxTracesPerUser(userID string, traces int) error {
actualLimit := l.maxTracesPerUser(userID)
if traces < actualLimit {
return nil
}
localLimit := l.limits.MaxLocalTracesPerUser(userID)
globalLimit := l.limits.MaxGlobalTracesPerUser(userID)
return fmt.Errorf(errMaxTracesPerUserLimitExceeded, localLimit, globalLimit, actualLimit)
}
func (l *limiter) maxTracesPerUser(userID string) int {
localLimit := l.limits.MaxLocalTracesPerUser(userID)
// We can assume that traces are evenly distributed across ingesters
// so we do convert the global limit into a local limit
globalLimit := l.limits.MaxGlobalTracesPerUser(userID)
localLimit = l.minNonZero(localLimit, l.convertGlobalToLocalLimit(userID, globalLimit))
// If both the local and global limits are disabled, we just
// use the largest int value
if localLimit == 0 {
localLimit = math.MaxInt32
}
return localLimit
}
func (l *limiter) convertGlobalToLocalLimit(userID string, globalLimit int) int {
if globalLimit == 0 {
return 0
}
// Given we don't need a super accurate count (ie. when the ingesters
// topology changes) and we prefer to always be in favor of the tenant,
// we can use a per-ingester limit equal to:
// (global limit / number of ingesters) * replication factor
ingestionShardSize := l.limits.IngestionTenantShardSize(userID)
totalIngesters := l.ring.HealthyInstancesCount()
numIngesters := l.minNonZero(ingestionShardSize, totalIngesters)
// May happen because the number of ingesters is asynchronously updated.
// If happens, we just temporarily ignore the global limit.
if numIngesters > 0 {
return int((float64(globalLimit) / float64(numIngesters)) * float64(l.replicationFactor))
}
return 0
}
func (l *limiter) minNonZero(first, second int) int {
if first == 0 || (second != 0 && first > second) {
return second
}
return first
}