fix(go.sum): update ResolveSpec dependency to v1.0.87
This commit is contained in:
+419
@@ -0,0 +1,419 @@
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go/internal/protocol"
|
||||
"github.com/getsentry/sentry-go/internal/ratelimit"
|
||||
"github.com/getsentry/sentry-go/report"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBucketedCapacity = 100
|
||||
perBucketItemLimit = 100
|
||||
)
|
||||
|
||||
type Bucket[T any] struct {
|
||||
traceID string
|
||||
items []T
|
||||
createdAt time.Time
|
||||
lastUpdatedAt time.Time
|
||||
}
|
||||
|
||||
// BucketedBuffer groups items by trace id, flushing per bucket.
|
||||
type BucketedBuffer[T any] struct {
|
||||
mu sync.RWMutex
|
||||
|
||||
buckets []*Bucket[T]
|
||||
traceIndex map[string]int
|
||||
|
||||
head int
|
||||
tail int
|
||||
|
||||
itemCapacity int
|
||||
bucketCapacity int
|
||||
|
||||
totalItems int
|
||||
bucketCount int
|
||||
|
||||
category ratelimit.Category
|
||||
priority ratelimit.Priority
|
||||
overflowPolicy OverflowPolicy
|
||||
recorder report.ClientReportRecorder
|
||||
batchSize int
|
||||
timeout time.Duration
|
||||
lastFlushTime time.Time
|
||||
|
||||
offered int64
|
||||
dropped int64
|
||||
onDropped func(item T, reason string)
|
||||
}
|
||||
|
||||
func NewBucketedBuffer[T any](
|
||||
category ratelimit.Category,
|
||||
capacity int,
|
||||
overflowPolicy OverflowPolicy,
|
||||
batchSize int,
|
||||
timeout time.Duration,
|
||||
recorder report.ClientReportRecorder,
|
||||
) *BucketedBuffer[T] {
|
||||
if capacity <= 0 {
|
||||
capacity = defaultBucketedCapacity
|
||||
}
|
||||
if batchSize <= 0 {
|
||||
batchSize = 1
|
||||
}
|
||||
if timeout < 0 {
|
||||
timeout = 0
|
||||
}
|
||||
|
||||
if recorder == nil {
|
||||
recorder = report.NoopRecorder()
|
||||
}
|
||||
|
||||
bucketCapacity := capacity / 10
|
||||
if bucketCapacity < 10 {
|
||||
bucketCapacity = 10
|
||||
}
|
||||
|
||||
return &BucketedBuffer[T]{
|
||||
buckets: make([]*Bucket[T], bucketCapacity),
|
||||
traceIndex: make(map[string]int),
|
||||
itemCapacity: capacity,
|
||||
bucketCapacity: bucketCapacity,
|
||||
category: category,
|
||||
priority: category.GetPriority(),
|
||||
overflowPolicy: overflowPolicy,
|
||||
recorder: recorder,
|
||||
batchSize: batchSize,
|
||||
timeout: timeout,
|
||||
lastFlushTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BucketedBuffer[T]) Offer(item T) bool {
|
||||
atomic.AddInt64(&b.offered, 1)
|
||||
|
||||
traceID := ""
|
||||
if ta, ok := any(item).(TraceAware); ok {
|
||||
if tid, hasTrace := ta.GetTraceID(); hasTrace {
|
||||
traceID = tid
|
||||
}
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.offerToBucket(item, traceID)
|
||||
}
|
||||
|
||||
func (b *BucketedBuffer[T]) offerToBucket(item T, traceID string) bool {
|
||||
if traceID != "" {
|
||||
if idx, exists := b.traceIndex[traceID]; exists {
|
||||
bucket := b.buckets[idx]
|
||||
if len(bucket.items) >= perBucketItemLimit {
|
||||
delete(b.traceIndex, traceID)
|
||||
} else {
|
||||
bucket.items = append(bucket.items, item)
|
||||
bucket.lastUpdatedAt = time.Now()
|
||||
b.totalItems++
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if b.totalItems >= b.itemCapacity {
|
||||
return b.handleOverflow(item, traceID)
|
||||
}
|
||||
if b.bucketCount >= b.bucketCapacity {
|
||||
return b.handleOverflow(item, traceID)
|
||||
}
|
||||
|
||||
bucket := &Bucket[T]{
|
||||
traceID: traceID,
|
||||
items: []T{item},
|
||||
createdAt: time.Now(),
|
||||
lastUpdatedAt: time.Now(),
|
||||
}
|
||||
b.buckets[b.tail] = bucket
|
||||
if traceID != "" {
|
||||
b.traceIndex[traceID] = b.tail
|
||||
}
|
||||
b.tail = (b.tail + 1) % b.bucketCapacity
|
||||
b.bucketCount++
|
||||
b.totalItems++
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *BucketedBuffer[T]) handleOverflow(item T, traceID string) bool {
|
||||
switch b.overflowPolicy {
|
||||
case OverflowPolicyDropOldest:
|
||||
oldestBucket := b.buckets[b.head]
|
||||
if oldestBucket == nil {
|
||||
b.recordDroppedItem(item)
|
||||
atomic.AddInt64(&b.dropped, 1)
|
||||
if b.onDropped != nil {
|
||||
b.onDropped(item, "buffer_full_invalid_state")
|
||||
}
|
||||
return false
|
||||
}
|
||||
if oldestBucket.traceID != "" {
|
||||
delete(b.traceIndex, oldestBucket.traceID)
|
||||
}
|
||||
droppedCount := len(oldestBucket.items)
|
||||
atomic.AddInt64(&b.dropped, int64(droppedCount))
|
||||
for _, di := range oldestBucket.items {
|
||||
b.recordDroppedItem(di)
|
||||
if b.onDropped != nil {
|
||||
b.onDropped(di, "buffer_full_drop_oldest_bucket")
|
||||
}
|
||||
}
|
||||
b.totalItems -= droppedCount
|
||||
b.bucketCount--
|
||||
b.head = (b.head + 1) % b.bucketCapacity
|
||||
// add new bucket
|
||||
bucket := &Bucket[T]{traceID: traceID, items: []T{item}, createdAt: time.Now(), lastUpdatedAt: time.Now()}
|
||||
b.buckets[b.tail] = bucket
|
||||
if traceID != "" {
|
||||
b.traceIndex[traceID] = b.tail
|
||||
}
|
||||
b.tail = (b.tail + 1) % b.bucketCapacity
|
||||
b.bucketCount++
|
||||
b.totalItems++
|
||||
return true
|
||||
case OverflowPolicyDropNewest:
|
||||
atomic.AddInt64(&b.dropped, 1)
|
||||
b.recordDroppedItem(item)
|
||||
if b.onDropped != nil {
|
||||
b.onDropped(item, "buffer_full_drop_newest")
|
||||
}
|
||||
return false
|
||||
default:
|
||||
atomic.AddInt64(&b.dropped, 1)
|
||||
b.recordDroppedItem(item)
|
||||
if b.onDropped != nil {
|
||||
b.onDropped(item, "unknown_overflow_policy")
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BucketedBuffer[T]) Poll() (T, bool) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
var zero T
|
||||
if b.bucketCount == 0 {
|
||||
return zero, false
|
||||
}
|
||||
bucket := b.buckets[b.head]
|
||||
if bucket == nil || len(bucket.items) == 0 {
|
||||
return zero, false
|
||||
}
|
||||
item := bucket.items[0]
|
||||
bucket.items = bucket.items[1:]
|
||||
b.totalItems--
|
||||
if len(bucket.items) == 0 {
|
||||
if bucket.traceID != "" {
|
||||
delete(b.traceIndex, bucket.traceID)
|
||||
}
|
||||
b.buckets[b.head] = nil
|
||||
b.head = (b.head + 1) % b.bucketCapacity
|
||||
b.bucketCount--
|
||||
}
|
||||
return item, true
|
||||
}
|
||||
|
||||
func (b *BucketedBuffer[T]) PollBatch(maxItems int) []T {
|
||||
if maxItems <= 0 {
|
||||
return nil
|
||||
}
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.bucketCount == 0 {
|
||||
return nil
|
||||
}
|
||||
res := make([]T, 0, maxItems)
|
||||
for len(res) < maxItems && b.bucketCount > 0 {
|
||||
bucket := b.buckets[b.head]
|
||||
if bucket == nil {
|
||||
break
|
||||
}
|
||||
n := maxItems - len(res)
|
||||
if n > len(bucket.items) {
|
||||
n = len(bucket.items)
|
||||
}
|
||||
res = append(res, bucket.items[:n]...)
|
||||
bucket.items = bucket.items[n:]
|
||||
b.totalItems -= n
|
||||
if len(bucket.items) == 0 {
|
||||
if bucket.traceID != "" {
|
||||
delete(b.traceIndex, bucket.traceID)
|
||||
}
|
||||
b.buckets[b.head] = nil
|
||||
b.head = (b.head + 1) % b.bucketCapacity
|
||||
b.bucketCount--
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (b *BucketedBuffer[T]) PollIfReady() []T {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.bucketCount == 0 {
|
||||
return nil
|
||||
}
|
||||
ready := b.totalItems >= b.batchSize || (b.timeout > 0 && time.Since(b.lastFlushTime) >= b.timeout)
|
||||
if !ready {
|
||||
return nil
|
||||
}
|
||||
oldest := b.buckets[b.head]
|
||||
if oldest == nil {
|
||||
return nil
|
||||
}
|
||||
items := oldest.items
|
||||
if oldest.traceID != "" {
|
||||
delete(b.traceIndex, oldest.traceID)
|
||||
}
|
||||
b.buckets[b.head] = nil
|
||||
b.head = (b.head + 1) % b.bucketCapacity
|
||||
b.totalItems -= len(items)
|
||||
b.bucketCount--
|
||||
b.lastFlushTime = time.Now()
|
||||
return items
|
||||
}
|
||||
|
||||
func (b *BucketedBuffer[T]) Drain() []T {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.bucketCount == 0 {
|
||||
return nil
|
||||
}
|
||||
res := make([]T, 0, b.totalItems)
|
||||
for i := 0; i < b.bucketCount; i++ {
|
||||
idx := (b.head + i) % b.bucketCapacity
|
||||
bucket := b.buckets[idx]
|
||||
if bucket != nil {
|
||||
res = append(res, bucket.items...)
|
||||
b.buckets[idx] = nil
|
||||
}
|
||||
}
|
||||
b.traceIndex = make(map[string]int)
|
||||
b.head = 0
|
||||
b.tail = 0
|
||||
b.totalItems = 0
|
||||
b.bucketCount = 0
|
||||
return res
|
||||
}
|
||||
|
||||
func (b *BucketedBuffer[T]) Peek() (T, bool) {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
var zero T
|
||||
if b.bucketCount == 0 {
|
||||
return zero, false
|
||||
}
|
||||
bucket := b.buckets[b.head]
|
||||
if bucket == nil || len(bucket.items) == 0 {
|
||||
return zero, false
|
||||
}
|
||||
return bucket.items[0], true
|
||||
}
|
||||
|
||||
func (b *BucketedBuffer[T]) Size() int { b.mu.RLock(); defer b.mu.RUnlock(); return b.totalItems }
|
||||
func (b *BucketedBuffer[T]) Capacity() int { b.mu.RLock(); defer b.mu.RUnlock(); return b.itemCapacity }
|
||||
func (b *BucketedBuffer[T]) Category() ratelimit.Category {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
return b.category
|
||||
}
|
||||
func (b *BucketedBuffer[T]) Priority() ratelimit.Priority {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
return b.priority
|
||||
}
|
||||
func (b *BucketedBuffer[T]) IsEmpty() bool {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
return b.bucketCount == 0
|
||||
}
|
||||
func (b *BucketedBuffer[T]) IsFull() bool {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
return b.totalItems >= b.itemCapacity
|
||||
}
|
||||
func (b *BucketedBuffer[T]) Utilization() float64 {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
if b.itemCapacity == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(b.totalItems) / float64(b.itemCapacity)
|
||||
}
|
||||
func (b *BucketedBuffer[T]) OfferedCount() int64 { return atomic.LoadInt64(&b.offered) }
|
||||
func (b *BucketedBuffer[T]) DroppedCount() int64 { return atomic.LoadInt64(&b.dropped) }
|
||||
func (b *BucketedBuffer[T]) AcceptedCount() int64 { return b.OfferedCount() - b.DroppedCount() }
|
||||
func (b *BucketedBuffer[T]) DropRate() float64 {
|
||||
off := b.OfferedCount()
|
||||
if off == 0 {
|
||||
return 0
|
||||
}
|
||||
return float64(b.DroppedCount()) / float64(off)
|
||||
}
|
||||
|
||||
func (b *BucketedBuffer[T]) GetMetrics() BufferMetrics {
|
||||
b.mu.RLock()
|
||||
size := b.totalItems
|
||||
util := 0.0
|
||||
if b.itemCapacity > 0 {
|
||||
util = float64(b.totalItems) / float64(b.itemCapacity)
|
||||
}
|
||||
b.mu.RUnlock()
|
||||
return BufferMetrics{Category: b.category, Priority: b.priority, Capacity: b.itemCapacity, Size: size, Utilization: util, OfferedCount: b.OfferedCount(), DroppedCount: b.DroppedCount(), AcceptedCount: b.AcceptedCount(), DropRate: b.DropRate(), LastUpdated: time.Now()}
|
||||
}
|
||||
|
||||
func (b *BucketedBuffer[T]) SetDroppedCallback(callback func(item T, reason string)) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.onDropped = callback
|
||||
}
|
||||
func (b *BucketedBuffer[T]) Clear() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
for i := 0; i < b.bucketCapacity; i++ {
|
||||
b.buckets[i] = nil
|
||||
}
|
||||
b.traceIndex = make(map[string]int)
|
||||
b.head = 0
|
||||
b.tail = 0
|
||||
b.totalItems = 0
|
||||
b.bucketCount = 0
|
||||
}
|
||||
func (b *BucketedBuffer[T]) IsReadyToFlush() bool {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
if b.bucketCount == 0 {
|
||||
return false
|
||||
}
|
||||
if b.totalItems >= b.batchSize {
|
||||
return true
|
||||
}
|
||||
if b.timeout > 0 && time.Since(b.lastFlushTime) >= b.timeout {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
func (b *BucketedBuffer[T]) MarkFlushed() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.lastFlushTime = time.Now()
|
||||
}
|
||||
|
||||
func (b *BucketedBuffer[T]) recordDroppedItem(item T) {
|
||||
if ti, ok := any(item).(protocol.TelemetryItem); ok {
|
||||
b.recorder.RecordItem(report.ReasonBufferOverflow, ti)
|
||||
} else {
|
||||
b.recorder.RecordOne(report.ReasonBufferOverflow, b.category)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user