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

     1  package v2
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"math/rand"
     7  	"sync/atomic"
     8  	"time"
     9  
    10  	"github.com/gogo/protobuf/proto"
    11  	"github.com/golang/snappy"
    12  	"github.com/prometheus/client_golang/prometheus"
    13  	"github.com/prometheus/prometheus/prompb"
    14  	"golang.org/x/sync/errgroup"
    15  
    16  	logproto "github.com/grafana/loki/pkg/push"
    17  	"github.com/grafana/synthetic-monitoring-agent/internal/model"
    18  	"github.com/grafana/synthetic-monitoring-agent/internal/pusher"
    19  	sm "github.com/grafana/synthetic-monitoring-agent/pkg/pb/synthetic_monitoring"
    20  )
    21  
    22  // tenantPusher is in charge of pushing changes for a specific tenant.
    23  type tenantPusher struct {
    24  	tenantID       model.GlobalID
    25  	pushCounter    uint64 // FIXME(mem)
    26  	logs, metrics  queue
    27  	tenantProvider pusher.TenantProvider
    28  	options        pusherOptions
    29  }
    30  
    31  var _ payloadHandler = &tenantPusher{}
    32  
    33  func newTenantPusher(tenantID model.GlobalID, tenantProvider pusher.TenantProvider, options pusherOptions) *tenantPusher {
    34  	mOptions := options.withType(pusher.LabelValueMetrics)
    35  	eOptions := options.withType(pusher.LabelValueLogs)
    36  	tp := &tenantPusher{
    37  		tenantID:       tenantID,
    38  		tenantProvider: tenantProvider,
    39  		options:        options,
    40  		logs:           newQueue(&eOptions),
    41  		metrics:        newQueue(&mOptions),
    42  	}
    43  
    44  	return tp
    45  }
    46  
    47  func (p *tenantPusher) run(ctx context.Context) payloadHandler {
    48  	p.options.logger.Info().Msg("starting pusher")
    49  	err := p.runPushers(ctx)
    50  	p.options.logger.Info().Err(err).Msg("pusher terminated")
    51  
    52  	if err == nil || err == context.Canceled || err == errTenantIdle {
    53  		p.options.logger.Info().Err(err).Msg("clearing tenant slot")
    54  		return nil
    55  	}
    56  
    57  	var pErr pushError
    58  	if !errors.As(err, &pErr) {
    59  		p.options.logger.Warn().Err(err).Msg("unexpected error, clearing slot")
    60  		return nil
    61  	}
    62  
    63  	switch pErr.kind {
    64  	case errKindWait:
    65  		// Some failure forces us to stop pushing for a while, but keep accumulating metrics meanwhile.
    66  		p.options.logger.Info().Dur("delay", p.options.waitPeriod).Msg("delaying metrics publishing")
    67  
    68  		return &delayPusher{
    69  			next:  p,
    70  			delay: p.options.waitPeriod,
    71  		}
    72  
    73  	case errKindTenant:
    74  		p.options.logger.Debug().Msg("refreshing tenant")
    75  		// The tenant could be stale. Let's just restart the pusher with a minimal delay.
    76  		return &delayPusher{
    77  			next:  p,
    78  			delay: p.options.tenantDelay,
    79  		}
    80  
    81  	case errKindFatal:
    82  		// Possibly a situation where we can't push metrics and won't be able to push them
    83  		// in the future. Discard metrics for this tenant for an extended period of time.
    84  		p.options.logger.Warn().Dur("duration", p.options.discardPeriod).Msg("discarding metrics")
    85  
    86  		return &discardPusher{
    87  			duration: p.options.discardPeriod,
    88  			options:  p.options,
    89  		}
    90  
    91  	default:
    92  		p.options.logger.Warn().Err(err).Msg("unexpected error, clearing slot")
    93  		return nil
    94  	}
    95  }
    96  
    97  func (p *tenantPusher) runPushers(ctx context.Context) error {
    98  	// TODO(adrian): If tenant had the plan in here, we could have different retention for paid tenants.
    99  	tenant, err := p.tenantProvider.GetTenant(ctx, &sm.TenantInfo{
   100  		Id: int64(p.tenantID),
   101  	})
   102  	if err != nil {
   103  		p.options.metrics.FailedCounter.With(prometheus.Labels{"type": pusher.LabelValueMetrics, "reason": pusher.LabelValueTenant}).Inc()
   104  		p.options.metrics.FailedCounter.With(prometheus.Labels{"type": pusher.LabelValueLogs, "reason": pusher.LabelValueTenant}).Inc()
   105  
   106  		return pushError{
   107  			kind:  errKindFatal,
   108  			inner: err,
   109  		}
   110  	}
   111  
   112  	g, gCtx := errgroup.WithContext(ctx)
   113  
   114  	g.Go(func() error {
   115  		return p.metrics.push(gCtx, tenant.MetricsRemote)
   116  	})
   117  
   118  	g.Go(func() error {
   119  		return p.logs.push(gCtx, tenant.EventsRemote)
   120  	})
   121  
   122  	// Optionally perform some validations.
   123  
   124  	if p.options.maxIdleTime > 0 {
   125  		g.Go(idleChecker(gCtx, p.options.maxIdleTime, &p.pushCounter))
   126  	}
   127  
   128  	if p.options.maxLifetime > 0 {
   129  		// generate a random number [-maxLifetimeJitter/2, maxLifetimeJitter/2)
   130  		jitter := (rand.Float64() - 0.5) * p.options.maxLifetimeJitter
   131  		// adjust maxLifetime by that amount
   132  		maxLifetime := p.options.maxLifetime + time.Duration(jitter*float64(p.options.maxLifetime))
   133  		g.Go(maxLifetimeChecker(gCtx, maxLifetime))
   134  	}
   135  
   136  	return g.Wait()
   137  }
   138  
   139  var (
   140  	errMaxLifeTimeExceeded = pushError{
   141  		kind:  errKindTenant,
   142  		inner: errors.New("max life time exceeded"),
   143  	}
   144  	errTenantIdle = errors.New("tenant is idle")
   145  )
   146  
   147  func idleChecker(ctx context.Context, interval time.Duration, ptr *uint64) func() error {
   148  	return func() error {
   149  		t := time.NewTicker(interval)
   150  		defer t.Stop()
   151  
   152  		lastValue := atomic.LoadUint64(ptr)
   153  
   154  		for {
   155  			select {
   156  			case <-ctx.Done():
   157  				return ctx.Err()
   158  			case <-t.C:
   159  				// check that source has changed
   160  				curValue := atomic.LoadUint64(ptr)
   161  				if curValue == lastValue {
   162  					return errTenantIdle
   163  				}
   164  
   165  				lastValue = curValue
   166  			}
   167  		}
   168  	}
   169  }
   170  
   171  func maxLifetimeChecker(ctx context.Context, maxLifetime time.Duration) func() error {
   172  	return func() error {
   173  		t := time.NewTimer(maxLifetime)
   174  		select {
   175  		case <-ctx.Done():
   176  			t.Stop()
   177  			return ctx.Err()
   178  		case <-t.C:
   179  			return errMaxLifeTimeExceeded
   180  		}
   181  	}
   182  }
   183  
   184  func (p *tenantPusher) publish(payload pusher.Payload) {
   185  	atomic.AddUint64(&p.pushCounter, 1)
   186  
   187  	if len(payload.Metrics()) > 0 {
   188  		p.metrics.insert(toRequest(&prompb.WriteRequest{Timeseries: payload.Metrics()}, p.options.pool))
   189  	}
   190  
   191  	if len(payload.Streams()) > 0 {
   192  		p.logs.insert(toRequest(&logproto.PushRequest{Streams: payload.Streams()}, p.options.pool))
   193  	}
   194  }
   195  
   196  func toRequest(m proto.Marshaler, p bufferPool) *[]byte {
   197  	data, err := m.Marshal()
   198  	if err != nil {
   199  		panic(err)
   200  	}
   201  
   202  	bufPtr := p.get()
   203  	*bufPtr = (*bufPtr)[0:cap(*bufPtr)]
   204  	encoded := snappy.Encode(*bufPtr, data)
   205  
   206  	return &encoded
   207  }
   208  
   209  type delayPusher struct {
   210  	next  payloadHandler
   211  	delay time.Duration
   212  }
   213  
   214  var _ payloadHandler = delayPusher{}
   215  
   216  func (p delayPusher) run(ctx context.Context) payloadHandler {
   217  	err := sleepCtx(ctx, p.delay)
   218  	if err != nil {
   219  		// Context cancelled
   220  		return nil
   221  	}
   222  
   223  	return p.next
   224  }
   225  
   226  func (p delayPusher) publish(payloads pusher.Payload) {
   227  	p.next.publish(payloads)
   228  }
   229  
   230  type discardPusher struct {
   231  	duration time.Duration
   232  	options  pusherOptions
   233  }
   234  
   235  var _ payloadHandler = discardPusher{}
   236  
   237  func (p discardPusher) run(ctx context.Context) payloadHandler {
   238  	// error can be ignored (only possible is context.Canceled)
   239  	// as it's returning a nil handler anyway.
   240  	_ = sleepCtx(ctx, p.duration)
   241  	return nil
   242  }
   243  
   244  func (p discardPusher) publish(payloads pusher.Payload) {
   245  	if len(payloads.Metrics()) > 0 {
   246  		p.options.metrics.DroppedCounter.WithLabelValues(pusher.LabelValueMetrics).Inc()
   247  	}
   248  
   249  	if len(payloads.Streams()) > 0 {
   250  		p.options.metrics.DroppedCounter.WithLabelValues(pusher.LabelValueLogs).Inc()
   251  	}
   252  }