fix(go.sum): update ResolveSpec dependency to v1.0.87
This commit is contained in:
+339
@@ -0,0 +1,339 @@
|
||||
package telemetry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go/internal/debuglog"
|
||||
"github.com/getsentry/sentry-go/internal/protocol"
|
||||
"github.com/getsentry/sentry-go/internal/ratelimit"
|
||||
"github.com/getsentry/sentry-go/report"
|
||||
)
|
||||
|
||||
// Scheduler implements a weighted round-robin scheduler for processing buffered events.
|
||||
type Scheduler struct {
|
||||
buffers map[ratelimit.Category]Buffer[protocol.TelemetryItem]
|
||||
transport protocol.TelemetryTransport
|
||||
dsn *protocol.Dsn
|
||||
sdkInfo func() *protocol.SdkInfo
|
||||
recorder report.ClientReportRecorder
|
||||
|
||||
currentCycle []ratelimit.Priority
|
||||
cyclePos int
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
processingWg sync.WaitGroup
|
||||
|
||||
mu sync.Mutex
|
||||
cond *sync.Cond
|
||||
startOnce sync.Once
|
||||
finishOnce sync.Once
|
||||
}
|
||||
|
||||
func NewScheduler(
|
||||
buffers map[ratelimit.Category]Buffer[protocol.TelemetryItem],
|
||||
transport protocol.TelemetryTransport,
|
||||
dsn *protocol.Dsn,
|
||||
sdkInfo func() *protocol.SdkInfo,
|
||||
recorder report.ClientReportRecorder,
|
||||
) *Scheduler {
|
||||
if recorder == nil {
|
||||
recorder = report.NoopRecorder()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background()) //nolint:gosec // G118: cancel is stored in s.cancel and called in Shutdown()
|
||||
|
||||
priorityWeights := map[ratelimit.Priority]int{
|
||||
ratelimit.PriorityCritical: 5,
|
||||
ratelimit.PriorityHigh: 4,
|
||||
ratelimit.PriorityMedium: 3,
|
||||
ratelimit.PriorityLow: 2,
|
||||
ratelimit.PriorityLowest: 1,
|
||||
}
|
||||
|
||||
var currentCycle []ratelimit.Priority
|
||||
for priority, weight := range priorityWeights {
|
||||
hasBuffers := false
|
||||
for _, buffer := range buffers {
|
||||
if buffer.Priority() == priority {
|
||||
hasBuffers = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hasBuffers {
|
||||
for i := 0; i < weight; i++ {
|
||||
currentCycle = append(currentCycle, priority)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s := &Scheduler{
|
||||
buffers: buffers,
|
||||
transport: transport,
|
||||
dsn: dsn,
|
||||
sdkInfo: sdkInfo,
|
||||
recorder: recorder,
|
||||
currentCycle: currentCycle,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
s.cond = sync.NewCond(&s.mu)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Scheduler) resolveSdkInfo() *protocol.SdkInfo {
|
||||
if s.sdkInfo == nil {
|
||||
return &protocol.SdkInfo{}
|
||||
}
|
||||
return s.sdkInfo()
|
||||
}
|
||||
|
||||
func (s *Scheduler) Start() {
|
||||
s.startOnce.Do(func() {
|
||||
s.processingWg.Add(1)
|
||||
go s.run()
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Scheduler) Stop(timeout time.Duration) {
|
||||
s.finishOnce.Do(func() {
|
||||
s.Flush(timeout)
|
||||
|
||||
s.cancel()
|
||||
s.cond.Broadcast()
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
s.processingWg.Wait()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(timeout):
|
||||
debuglog.Printf("scheduler stop timed out after %v", timeout)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Scheduler) Signal() {
|
||||
s.cond.Signal()
|
||||
}
|
||||
|
||||
func (s *Scheduler) Add(item protocol.TelemetryItem) bool {
|
||||
category := item.GetCategory()
|
||||
buffer, exists := s.buffers[category]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
accepted := buffer.Offer(item)
|
||||
if accepted {
|
||||
s.Signal()
|
||||
}
|
||||
return accepted
|
||||
}
|
||||
|
||||
func (s *Scheduler) Flush(timeout time.Duration) bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
return s.FlushWithContext(ctx)
|
||||
}
|
||||
|
||||
func (s *Scheduler) FlushWithContext(ctx context.Context) bool {
|
||||
s.flushBuffers()
|
||||
return s.transport.FlushWithContext(ctx)
|
||||
}
|
||||
|
||||
func (s *Scheduler) run() {
|
||||
defer s.processingWg.Done()
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(100 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
s.cond.Broadcast()
|
||||
case <-s.ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
s.mu.Lock()
|
||||
|
||||
for !s.hasWork() && s.ctx.Err() == nil {
|
||||
s.cond.Wait()
|
||||
}
|
||||
|
||||
if s.ctx.Err() != nil {
|
||||
s.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.Unlock()
|
||||
s.processNextBatch()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scheduler) hasWork() bool {
|
||||
for _, buffer := range s.buffers {
|
||||
if buffer.IsReadyToFlush() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Scheduler) processNextBatch() {
|
||||
if len(s.currentCycle) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
priority := s.currentCycle[s.cyclePos]
|
||||
s.cyclePos = (s.cyclePos + 1) % len(s.currentCycle)
|
||||
|
||||
var bufferToProcess Buffer[protocol.TelemetryItem]
|
||||
var categoryToProcess ratelimit.Category
|
||||
for category, buffer := range s.buffers {
|
||||
if buffer.Priority() == priority && buffer.IsReadyToFlush() {
|
||||
bufferToProcess = buffer
|
||||
categoryToProcess = category
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if bufferToProcess != nil {
|
||||
s.processItems(bufferToProcess, categoryToProcess, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scheduler) processItems(buffer Buffer[protocol.TelemetryItem], category ratelimit.Category, force bool) {
|
||||
var items []protocol.TelemetryItem
|
||||
|
||||
if force {
|
||||
items = buffer.Drain()
|
||||
} else {
|
||||
items = buffer.PollIfReady()
|
||||
}
|
||||
|
||||
if len(items) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if s.isRateLimited(category) {
|
||||
for _, item := range items {
|
||||
s.recorder.RecordItem(report.ReasonRateLimitBackoff, item)
|
||||
}
|
||||
return
|
||||
}
|
||||
if !s.transport.HasCapacity() {
|
||||
for _, item := range items {
|
||||
s.recorder.RecordItem(report.ReasonQueueOverflow, item)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch category {
|
||||
case ratelimit.CategoryLog:
|
||||
logs := protocol.Logs(items)
|
||||
header := &protocol.EnvelopeHeader{EventID: protocol.GenerateEventID(), SentAt: time.Now(), Dsn: s.dsn, Sdk: s.resolveSdkInfo()}
|
||||
envelope := protocol.NewEnvelope(header)
|
||||
item, err := logs.ToEnvelopeItem()
|
||||
if err != nil {
|
||||
debuglog.Printf("error creating log batch envelope item: %v", err)
|
||||
return
|
||||
}
|
||||
envelope.AddItem(item)
|
||||
if err := s.transport.SendEnvelope(envelope); err != nil {
|
||||
debuglog.Printf("error sending envelope: %v", err)
|
||||
}
|
||||
return
|
||||
case ratelimit.CategoryTraceMetric:
|
||||
metrics := protocol.Metrics(items)
|
||||
header := &protocol.EnvelopeHeader{EventID: protocol.GenerateEventID(), SentAt: time.Now(), Dsn: s.dsn, Sdk: s.resolveSdkInfo()}
|
||||
envelope := protocol.NewEnvelope(header)
|
||||
item, err := metrics.ToEnvelopeItem()
|
||||
if err != nil {
|
||||
debuglog.Printf("error creating trace metric batch envelope item: %v", err)
|
||||
return
|
||||
}
|
||||
envelope.AddItem(item)
|
||||
if err := s.transport.SendEnvelope(envelope); err != nil {
|
||||
debuglog.Printf("error sending envelope: %v", err)
|
||||
}
|
||||
return
|
||||
default:
|
||||
// if the buffers are properly configured, buffer.PollIfReady should return a single item for every category
|
||||
// other than logs. We still iterate over the items just in case, because we don't want to send broken envelopes.
|
||||
for _, it := range items {
|
||||
convertible, ok := it.(protocol.EnvelopeItemConvertible)
|
||||
if !ok {
|
||||
debuglog.Printf("item does not implement EnvelopeItemConvertible: %T", it)
|
||||
continue
|
||||
}
|
||||
s.sendItem(convertible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scheduler) sendItem(item protocol.EnvelopeItemConvertible) {
|
||||
header := &protocol.EnvelopeHeader{
|
||||
EventID: item.GetEventID(),
|
||||
SentAt: time.Now(),
|
||||
Dsn: s.dsn,
|
||||
Trace: item.GetDynamicSamplingContext(),
|
||||
Sdk: s.resolveSdkInfo(),
|
||||
}
|
||||
if header.EventID == "" {
|
||||
header.EventID = protocol.GenerateEventID()
|
||||
}
|
||||
|
||||
// TODO: need to restructure the abstraction layer to actually return envelopes instead of
|
||||
// envelope items. The problem with envelope items, is that for events and batches we might
|
||||
// need envelopes (eg. attachments need to be added separately and splitting the `Add` path
|
||||
// is problematic). Currently this is a workaround for adding attachments to events without
|
||||
// adding a circular dependency on telemetry/scheduler.
|
||||
var envelope *protocol.Envelope
|
||||
if convertible, ok := item.(protocol.EnvelopeConvertible); ok {
|
||||
var err error
|
||||
envelope, err = convertible.ToEnvelope(header)
|
||||
if err != nil {
|
||||
debuglog.Printf("error while converting to envelope: %v", err)
|
||||
s.recorder.RecordItem(report.ReasonInternalError, item)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
envItem, err := item.ToEnvelopeItem()
|
||||
if err != nil {
|
||||
debuglog.Printf("error while converting to envelope item: %v", err)
|
||||
s.recorder.RecordItem(report.ReasonInternalError, item)
|
||||
return
|
||||
}
|
||||
envelope = protocol.NewEnvelope(header)
|
||||
envelope.AddItem(envItem)
|
||||
}
|
||||
|
||||
if err := s.transport.SendEnvelope(envelope); err != nil {
|
||||
debuglog.Printf("error sending envelope: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scheduler) flushBuffers() {
|
||||
for category, buffer := range s.buffers {
|
||||
if !buffer.IsEmpty() {
|
||||
s.processItems(buffer, category, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Scheduler) isRateLimited(category ratelimit.Category) bool {
|
||||
return s.transport.IsRateLimited(category)
|
||||
}
|
||||
Reference in New Issue
Block a user