Files
76c2c85b97 [Generator] Add DRAIN span_name sanitizer (#6098)
* [Generator] Add DRAIN span_name sanitizer

* Fix linter

* Fix formatting

* Update default manifest

* Fix build error in update-fixtures

* Remove bad test

* Remove misleading comment

* Add per-tenant override for drain mode

* Do a machines job

* Address PR feedback

* Address PR review: rename drain_mode config and improve sanitizer

- Rename `drain_mode` to `span_name_sanitization` across all config,
  legacy config, interfaces, and overrides for better user-facing clarity
- Add explicit default value "disabled" in RegisterFlagsAndApplyDefaults
- Add ValidateSpanNameSanitization function in validation/fields.go
- Rename metrics to remove DRAIN reference:
  `drain_spans_compacted_total` -> `spans_sanitized_total`
  `post_drain_demand_estimate` -> `post_sanitization_demand_estimate`
- Pass configured staleDuration to NewDrainSanitizer instead of
  hardcoded 15min, consistent with other registry components
- Extract "span_name" magic string to labelSpanName constant
- Update all tests with new names and signatures

* Update manifest

* Use empty string as disabled

* Update CHANGELOG.md

Co-authored-by: Suraj Nath <[email protected]>

* Update manifest

---------

Co-authored-by: Suraj Nath <[email protected]>
2026-02-22 00:55:22 -08:00

132 lines
3.9 KiB
Go

// Copyright (c) 2024 Alexey Mayshev and contributors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// This is a port of lossy buffers from Caffeine.
// https://github.com/ben-manes/caffeine/blob/master/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedBuffer.java
package lossy
import (
"sync/atomic"
"unsafe"
"github.com/maypok86/otter/v2/internal/generated/node"
"github.com/maypok86/otter/v2/internal/xruntime"
)
// Status is the result of adding a node to the buffer.
type Status int8
const (
// Success means that the node was added.
Success Status = 0
// Failed means that the CAS failed.
Failed Status = -1
// Full means that the buffer is full.
Full Status = 1
)
const (
// The maximum number of elements per buffer.
bufferSize = 16
mask = uint64(bufferSize - 1)
)
// ring is a circular ring buffer stores the elements being transferred by the producers to the consumer.
// the monotonically increasing count of reads and writes allow indexing sequentially to the next
// element location based upon a power-of-two sizing.
//
// The producers race to read the counts, check if there is available capacity, and if so then try
// once to CAS to the next write count. If the increment is successful then the producer lazily
// publishes the element. The producer does not retry or block when unsuccessful due to a failed
// CAS or the buffer being full.
//
// The consumer reads the counts and takes the available elements. The clearing of the elements
// and the next read count are lazily set.
//
// This implementation is striped to further increase concurrency by rehashing and dynamically
// adding new buffers when contention is detected, up to an internal maximum. When rehashing in
// order to discover an available buffer, the producer may retry adding its element to determine
// whether it found a satisfactory buffer or if resizing is necessary.
type ring[K comparable, V any] struct {
head atomic.Uint64
_ [xruntime.CacheLineSize - unsafe.Sizeof(atomic.Uint64{})]byte
tail atomic.Uint64
_ [xruntime.CacheLineSize - unsafe.Sizeof(atomic.Uint64{})]byte
nodeManager *node.Manager[K, V]
buffer [bufferSize]unsafe.Pointer // node.Node[K, V]
}
func newRing[K comparable, V any](nodeManager *node.Manager[K, V], n node.Node[K, V]) *ring[K, V] {
r := &ring[K, V]{
nodeManager: nodeManager,
}
r.buffer[0] = n.AsPointer()
r.tail.Store(1)
return r
}
func (r *ring[K, V]) add(n node.Node[K, V]) Status {
head := r.head.Load()
tail := r.tail.Load()
size := tail - head
if size >= bufferSize {
return Full
}
if r.tail.CompareAndSwap(tail, tail+1) {
atomic.StorePointer(&r.buffer[tail&mask], n.AsPointer())
return Success
}
return Failed
}
func (r *ring[K, V]) drainTo(consumer func(n node.Node[K, V])) {
head := r.head.Load()
tail := r.tail.Load()
size := tail - head
if size == 0 {
return
}
nm := r.nodeManager
for head != tail {
index := head & mask
ptr := atomic.LoadPointer(&r.buffer[index])
if ptr == nil {
// not published.
break
}
atomic.StorePointer(&r.buffer[index], nil)
consumer(nm.FromPointer(ptr))
head++
}
r.head.Store(head)
}
func (r *ring[K, V]) len() int {
//nolint:gosec // there is no overflow
return int(r.tail.Load() - r.head.Load())
}
/*
func (r *ring[K, V]) clear() {
for i := 0; i < bufferSize; i++ {
atomic.StorePointer(&r.buffer[i], nil)
}
r.head.Store(0)
r.tail.Store(0)
}
*/