github.com/newrelic/newrelic-client-go@v1.1.0/pkg/events/events_batch.go (about)

     1  package events
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"time"
     8  )
     9  
    10  // BatchMode enables the Events client to accept, queue, and post
    11  // Events on behalf of the consuming application
    12  func (e *Events) BatchMode(ctx context.Context, accountID int, opts ...BatchConfigOption) (err error) {
    13  	if e.eventQueue != nil {
    14  		return errors.New("the Events client is already in batch mode")
    15  	}
    16  
    17  	// Loop through config options
    18  	for _, fn := range opts {
    19  		if nil != fn {
    20  			if err := fn(e); err != nil {
    21  				return err
    22  			}
    23  		}
    24  	}
    25  
    26  	e.accountID = accountID
    27  	e.eventQueue = make(chan []byte, e.batchSize)
    28  	e.flushQueue = make([]chan bool, e.batchWorkers)
    29  	e.eventTimer = time.NewTimer(e.batchTimeout)
    30  
    31  	// Handle timer based flushing
    32  	go func() {
    33  		err := e.watchdog(ctx)
    34  		if err != nil {
    35  			e.logger.Error("watchdog returned error", "error", err)
    36  		}
    37  	}()
    38  
    39  	// Spin up some workers
    40  	for x := range e.flushQueue {
    41  		e.flushQueue[x] = make(chan bool, 1)
    42  
    43  		go func(id int) {
    44  			err := e.batchWorker(ctx, id)
    45  			if err != nil {
    46  				e.logger.Error("batch worker returned error", "error", err)
    47  			}
    48  		}(x)
    49  	}
    50  
    51  	return nil
    52  }
    53  
    54  type BatchConfigOption func(*Events) error
    55  
    56  // BatchConfigWorkers sets how many background workers will process
    57  // events as they are queued
    58  func BatchConfigWorkers(count int) BatchConfigOption {
    59  	return func(e *Events) error {
    60  		if count <= 0 {
    61  			return errors.New("events: invalid worker count specified")
    62  		}
    63  
    64  		e.batchWorkers = count
    65  
    66  		return nil
    67  	}
    68  }
    69  
    70  // BatchConfigQueueSize is how many events to queue before sending
    71  // to New Relic.  If this limit is hit before the Timeout, the queue
    72  // is flushed.
    73  func BatchConfigQueueSize(size int) BatchConfigOption {
    74  	return func(e *Events) error {
    75  		if size <= 0 {
    76  			return errors.New("events: invalid queue size specified")
    77  		}
    78  
    79  		e.batchSize = size
    80  		return nil
    81  	}
    82  }
    83  
    84  // BatchConfigTimeout is the maximum amount of time to queue events
    85  // before sending to New Relic.  If this is reached before the Size
    86  // limit, the queue is flushed.
    87  func BatchConfigTimeout(seconds int) BatchConfigOption {
    88  	return func(e *Events) error {
    89  		if seconds <= 0 {
    90  			return errors.New("events: invalid timeout specified")
    91  		}
    92  
    93  		e.batchTimeout = time.Duration(seconds) * time.Second
    94  		return nil
    95  	}
    96  }
    97  
    98  // EnqueueEventContext handles the queueing. Only works in batch mode. If you wish to be able to avoid blocking
    99  // forever until the event can be queued, provide a ctx with a deadline or timeout as this function will
   100  // bail when ctx.Done() is closed and return and error.
   101  func (e *Events) EnqueueEvent(ctx context.Context, event interface{}) (err error) {
   102  	if e.eventQueue == nil {
   103  		return errors.New("queueing not enabled for this client")
   104  	}
   105  
   106  	jsonData, err := e.marshalEvent(event)
   107  	if err != nil {
   108  		return err
   109  	}
   110  	if jsonData == nil {
   111  		return errors.New("events: EnqueueEvent marhal returned nil data")
   112  	}
   113  
   114  	select {
   115  	case e.eventQueue <- *jsonData:
   116  		return nil
   117  	case <-ctx.Done():
   118  		e.logger.Trace("EnqueueEvent: exiting per context Done")
   119  		return ctx.Err()
   120  	}
   121  }
   122  
   123  // Flush gives the user a way to manually flush the queue in the foreground.
   124  // This is also used by watchdog when the timer expires.
   125  func (e *Events) Flush() error {
   126  	if e.flushQueue == nil {
   127  		return errors.New("queueing not enabled for this client")
   128  	}
   129  
   130  	e.logger.Debug("flushing events")
   131  
   132  	for x := range e.flushQueue {
   133  		e.flushQueue[x] <- true
   134  	}
   135  
   136  	return nil
   137  }
   138  
   139  //
   140  // batchWorker reads []byte from the queue until a threshold is passed,
   141  // then copies the []byte it has read and sends that batch along to Insights
   142  // in its own goroutine.
   143  //
   144  func (e *Events) batchWorker(ctx context.Context, id int) (err error) {
   145  	if e == nil {
   146  		return errors.New("batchWorker: invalid Events, unable to start worker")
   147  	}
   148  	if id < 0 || len(e.flushQueue) < id {
   149  		return errors.New("batchWorker: invalid worker id specified")
   150  	}
   151  
   152  	eventBuf := make([][]byte, e.batchSize)
   153  	count := 0
   154  
   155  	for {
   156  		select {
   157  		case item := <-e.eventQueue:
   158  			eventBuf[count] = item
   159  			count++
   160  			if count >= e.batchSize {
   161  				e.grabAndConsumeEvents(count, eventBuf)
   162  				count = 0
   163  			}
   164  		case <-e.flushQueue[id]:
   165  			if count > 0 {
   166  				e.grabAndConsumeEvents(count, eventBuf)
   167  				count = 0
   168  			}
   169  		case <-ctx.Done():
   170  			e.logger.Trace("batchWorker[", id, "]: exiting per context Done")
   171  			return ctx.Err()
   172  		}
   173  	}
   174  }
   175  
   176  //
   177  // watchdog has a Timer that will send the results once the
   178  // it has expired.
   179  //
   180  func (e *Events) watchdog(ctx context.Context) (err error) {
   181  	if e.eventTimer == nil {
   182  		return errors.New("invalid timer for watchdog()")
   183  	}
   184  
   185  	for {
   186  		select {
   187  		case <-e.eventTimer.C:
   188  			e.logger.Debug("Timeout expired, flushing queued events")
   189  			if err = e.Flush(); err != nil {
   190  				return
   191  			}
   192  			e.eventTimer.Reset(e.batchTimeout)
   193  		case <-ctx.Done():
   194  			e.logger.Trace("watchdog exiting: context finished")
   195  			return ctx.Err()
   196  		}
   197  	}
   198  }
   199  
   200  // grabAndConsumeEvents makes a copy of the event handles,
   201  // and asynchronously writes those events in its own goroutine.
   202  func (e *Events) grabAndConsumeEvents(count int, eventBuf [][]byte) {
   203  	saved := make([][]byte, count)
   204  	for i := 0; i < count; i++ {
   205  		saved[i] = eventBuf[i]
   206  		eventBuf[i] = nil
   207  	}
   208  
   209  	go func(count int, saved [][]byte) {
   210  		if sendErr := e.sendEvents(saved[0:count]); sendErr != nil {
   211  			e.logger.Error("failed to send events")
   212  		}
   213  	}(count, saved)
   214  }
   215  
   216  func (e *Events) sendEvents(events [][]byte) error {
   217  	var buf bytes.Buffer
   218  
   219  	// Since we already marshalled all of the data into JSON, let's make a
   220  	// hand-crafted, artisanal JSON array
   221  	buf.WriteString("[")
   222  	eventCount := len(events) - 1
   223  	for e := range events {
   224  		buf.Write(events[e])
   225  		if e < eventCount {
   226  			buf.WriteString(",")
   227  		}
   228  	}
   229  	buf.WriteString("]")
   230  
   231  	resp := &createEventResponse{}
   232  
   233  	_, err := e.client.Post(e.config.Region().InsightsURL(e.accountID), nil, buf.Bytes(), resp)
   234  
   235  	if err != nil {
   236  		return err
   237  	}
   238  
   239  	if !resp.Success {
   240  		return errors.New("failed creating custom event")
   241  	}
   242  
   243  	return nil
   244  }