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) {}