github.com/grafana/synthetic-monitoring-agent@v0.64.2/internal/pusher/v2/queue.go (about)

     1  package v2
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/http"
     7  	"sort"
     8  	"strconv"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/grafana/synthetic-monitoring-agent/internal/pkg/prom"
    13  	"github.com/grafana/synthetic-monitoring-agent/internal/pusher"
    14  	sm "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring"
    15  )
    16  
    17  type queue struct {
    18  	options   *pusherOptions
    19  	dataMutex sync.Mutex
    20  	data      []queueEntry
    21  	pending   condition
    22  }
    23  
    24  func newQueue(options *pusherOptions) queue {
    25  	return queue{
    26  		options: options,
    27  		pending: newCondition(),
    28  	}
    29  }
    30  
    31  func (q *queue) push(ctx context.Context, remote *sm.RemoteInfo) error {
    32  	cfg, err := pusher.ClientFromRemoteInfo(remote)
    33  	if err != nil {
    34  		return err
    35  	}
    36  
    37  	client, err := prom.NewClient(remote.Name, cfg, unusedRetryCounterFn)
    38  	if err != nil {
    39  		q.options.logger.Error().Err(err).Msg("get client failed")
    40  		q.options.metrics.FailedCounter.WithLabelValues(pusher.LabelValueClient).Inc()
    41  
    42  		return pushError{
    43  			kind:  errKindFatal,
    44  			inner: fmt.Errorf("creating client: %w", err),
    45  		}
    46  	}
    47  
    48  	var (
    49  		retries  = q.options.retriesCounter()
    50  		backoff  = q.options.backOffer()
    51  		retrying = false
    52  	)
    53  
    54  	for {
    55  		select {
    56  		case <-ctx.Done():
    57  			return ctx.Err()
    58  
    59  		case <-q.WaitC():
    60  			records := q.get()
    61  			if len(records) == 0 {
    62  				continue
    63  			}
    64  
    65  			numRecords := float64(len(records))
    66  
    67  			if !retrying {
    68  				retries.reset()
    69  				backoff.reset()
    70  			}
    71  
    72  			retrying = false
    73  
    74  			concatReader := newConcatReader(records)
    75  			httpStatusCode, pushErr := parsePublishError(client.StoreStream(ctx, &concatReader))
    76  			statusCodeStr := strconv.Itoa(httpStatusCode)
    77  
    78  			// TODO(mem): this might be a problem, we are keeping a
    79  			// tally of the status codes received for each
    80  			// individual tenant. It might be better to send this
    81  			// to logs.
    82  			q.options.metrics.ResponseCounter.WithLabelValues(statusCodeStr).Inc()
    83  
    84  			if pushErr.IsRetriable() {
    85  				q.options.metrics.ErrorCounter.WithLabelValues(statusCodeStr).Add(numRecords)
    86  
    87  				if retrying = retries.retry(); retrying {
    88  					q.options.metrics.RetriesCounter.WithLabelValues().Add(numRecords)
    89  					// This causes each retry to use all pending records (up to maxPushBytes).
    90  					// Is this what we want?
    91  					// Conceptually it can make more sense to always retry with the same packet?
    92  					// In the case where something in the packet is causing the 500
    93  					q.requeue(records)
    94  
    95  					if err := backoff.wait(ctx); err != nil {
    96  						return err // ctx was cancelled
    97  					}
    98  
    99  					continue
   100  				}
   101  			}
   102  
   103  			// We are not retrying anymore or the error is not retryable. Log the error.
   104  			if httpStatusCode != http.StatusOK {
   105  				q.options.logger.Error().
   106  					Err(pushErr.inner).
   107  					Int("status_code", httpStatusCode).
   108  					Stringer("kind", pushErr.Kind()).
   109  					Msg("store stream failed")
   110  			}
   111  
   112  			// Drop the records by returning their buffers to the pool. They were either published or we
   113  			// gave up on them.
   114  			size := q.options.pool.returnAll(records)
   115  
   116  			switch pushErr.Kind() {
   117  			case errKindNoError:
   118  				q.options.metrics.PushCounter.WithLabelValues().Add(numRecords)
   119  				q.options.metrics.BytesOut.WithLabelValues().Add(float64(size))
   120  
   121  				continue
   122  
   123  			case errKindNetwork:
   124  				q.options.metrics.FailedCounter.WithLabelValues(pusher.LabelValueRetryExhausted).Add(numRecords)
   125  
   126  			case errKindPayload:
   127  				// This is not necessarily errors! Possibly most of the data was ingested and only
   128  				// a sample was discarded.
   129  				q.options.metrics.ErrorCounter.WithLabelValues(statusCodeStr).Add(numRecords)
   130  
   131  			case errKindLimit:
   132  				// Some (?) of the data was ingested, but we don't have a way to know which part.
   133  				// Retrying won't help. Keep going.
   134  				q.options.metrics.ErrorCounter.WithLabelValues(statusCodeStr).Add(numRecords)
   135  
   136  			case errKindTenant, errKindFatal, errKindWait:
   137  				// Terminate publisher.
   138  				q.options.metrics.ErrorCounter.WithLabelValues(statusCodeStr).Add(numRecords)
   139  				return pushErr
   140  
   141  			case errKindTerminated:
   142  				// This can't really happen as client.StoreStream uses context.Background with a timeout.
   143  				return pushErr
   144  
   145  			default:
   146  				// What kind of error is this?
   147  				panic(pushErr)
   148  			}
   149  		}
   150  	}
   151  }
   152  
   153  func (q *queue) insert(data *[]byte) {
   154  	q.dataMutex.Lock()
   155  	defer q.dataMutex.Unlock()
   156  
   157  	q.data = append(q.data, queueEntry{
   158  		data: data,
   159  		ts:   time.Now(),
   160  	})
   161  	q.applyLimits()
   162  	q.pending.Signal()
   163  }
   164  
   165  func (q *queue) applyLimits() {
   166  	numDropped := q.limitBytes(q.options.maxQueuedBytes) + q.limitAge(q.options.maxQueuedTime)
   167  	if numDropped > 0 {
   168  		q.options.metrics.DroppedCounter.WithLabelValues().Add(float64(numDropped))
   169  	}
   170  }
   171  
   172  func (q *queue) limitBytes(max uint64) (numRemoved int) {
   173  	n := len(q.data)
   174  
   175  	if max <= 0 {
   176  		return 0
   177  	}
   178  
   179  	for i, numBytes := n-1, uint64(0); i >= 0; i-- {
   180  		if numBytes += uint64(len(*q.data[i].data)); numBytes > max {
   181  			q.options.pool.returnAll(q.data[:i+1])
   182  			q.data = q.data[i+1:]
   183  
   184  			return i + 1
   185  		}
   186  	}
   187  
   188  	return 0
   189  }
   190  
   191  func (q *queue) limitAge(maxAge time.Duration) (numRemoved int) {
   192  	n := len(q.data)
   193  	if n == 0 || maxAge <= 0 {
   194  		return 0
   195  	}
   196  
   197  	limit := time.Now().Add(-maxAge)
   198  	if !q.data[0].ts.Before(limit) {
   199  		return 0
   200  	}
   201  
   202  	// As data is chronologically sorted (q.data[i].ts <= q.data[i+1].ts)
   203  	// find all items older than limit using binary search.
   204  	idx := sort.Search(n, func(i int) bool {
   205  		return !q.data[i].ts.Before(limit)
   206  	})
   207  	if idx < n {
   208  		q.options.pool.returnAll(q.data[:idx])
   209  		q.data = q.data[idx:]
   210  	}
   211  
   212  	return n - idx
   213  }
   214  
   215  func (q *queue) WaitC() <-chan struct{} {
   216  	return q.pending.C()
   217  }
   218  
   219  func (q *queue) get() []queueEntry {
   220  	q.dataMutex.Lock()
   221  	defer q.dataMutex.Unlock()
   222  
   223  	totalQueued := len(q.data)
   224  	if totalQueued == 0 {
   225  		return nil
   226  	}
   227  
   228  	limit, numBytes := 1, uint64(len(*q.data[0].data))
   229  	for limit < totalQueued {
   230  		thisSize := uint64(len(*q.data[limit].data))
   231  		if numBytes+thisSize > q.options.maxPushBytes {
   232  			break
   233  		}
   234  
   235  		numBytes += thisSize
   236  		limit++
   237  	}
   238  
   239  	// Copy the entries to return out of the slice, and move the remaining
   240  	// entries to the front of the slice. In this way we avoid growing the
   241  	// slice in case we add new data.
   242  	take := make([]queueEntry, limit)
   243  	copy(take, q.data[:limit])
   244  	copy(q.data, q.data[limit:])
   245  	q.data = q.data[:len(q.data[limit:])]
   246  
   247  	if limit < totalQueued {
   248  		q.pending.Signal()
   249  	}
   250  
   251  	return take
   252  }
   253  
   254  func (q *queue) requeue(data []queueEntry) {
   255  	if len(data) == 0 {
   256  		return
   257  	}
   258  
   259  	q.dataMutex.Lock()
   260  	defer q.dataMutex.Unlock()
   261  
   262  	q.data = append(data, q.data...)
   263  	q.applyLimits()
   264  	q.pending.Signal()
   265  }
   266  
   267  type queueEntry struct {
   268  	data *[]byte
   269  	ts   time.Time
   270  }
   271  
   272  func newConcatReader(records []queueEntry) SnappyConcatReader {
   273  	streams := make([][]byte, len(records))
   274  	for idx, rec := range records {
   275  		streams[idx] = *rec.data
   276  	}
   277  
   278  	return SnappyConcatReader{
   279  		Streams: streams,
   280  	}
   281  }
   282  
   283  func unusedRetryCounterFn(float64) {}