github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/drivers/shared/eventer/eventer.go (about)

     1  package eventer
     2  
     3  import (
     4  	"context"
     5  	"sync"
     6  	"time"
     7  
     8  	hclog "github.com/hashicorp/go-hclog"
     9  	"github.com/hashicorp/nomad/helper"
    10  	"github.com/hashicorp/nomad/plugins/drivers"
    11  )
    12  
    13  var (
    14  	// DefaultSendEventTimeout is the timeout used when publishing events to consumers
    15  	DefaultSendEventTimeout = 2 * time.Second
    16  
    17  	// ConsumerGCInterval is the interval at which garbage collection of consumers
    18  	// occures
    19  	ConsumerGCInterval = time.Minute
    20  )
    21  
    22  // Eventer is a utility to control broadcast of TaskEvents to multiple consumers.
    23  // It also implements the TaskEvents func in the DriverPlugin interface so that
    24  // it can be embedded in a implementing driver struct.
    25  type Eventer struct {
    26  
    27  	// events is a channel were events to be broadcasted are sent
    28  	// This channel is never closed, because it's lifetime is tied to the
    29  	// life of the driver and closing creates some subtile race conditions
    30  	// between closing it and emitting events.
    31  	events chan *drivers.TaskEvent
    32  
    33  	// consumers is a slice of eventConsumers to broadcast events to.
    34  	// access is gaurded by consumersLock RWMutex
    35  	consumers     []*eventConsumer
    36  	consumersLock sync.RWMutex
    37  
    38  	// ctx to allow control of event loop shutdown
    39  	ctx context.Context
    40  
    41  	logger hclog.Logger
    42  }
    43  
    44  type eventConsumer struct {
    45  	timeout time.Duration
    46  	ctx     context.Context
    47  	ch      chan *drivers.TaskEvent
    48  	logger  hclog.Logger
    49  }
    50  
    51  // NewEventer returns an Eventer with a running event loop that can be stopped
    52  // by closing the given stop channel
    53  func NewEventer(ctx context.Context, logger hclog.Logger) *Eventer {
    54  	e := &Eventer{
    55  		events: make(chan *drivers.TaskEvent),
    56  		ctx:    ctx,
    57  		logger: logger,
    58  	}
    59  	go e.eventLoop()
    60  	return e
    61  }
    62  
    63  // eventLoop is the main logic which pulls events from the channel and broadcasts
    64  // them to all consumers
    65  func (e *Eventer) eventLoop() {
    66  	timer, stop := helper.NewSafeTimer(ConsumerGCInterval)
    67  	defer stop()
    68  
    69  	for {
    70  		timer.Reset(ConsumerGCInterval)
    71  
    72  		select {
    73  		case <-e.ctx.Done():
    74  			e.logger.Trace("task event loop shutdown")
    75  			return
    76  		case event := <-e.events:
    77  			e.iterateConsumers(event)
    78  		case <-timer.C:
    79  			e.gcConsumers()
    80  		}
    81  	}
    82  }
    83  
    84  // iterateConsumers will iterate through all consumers and broadcast the event,
    85  // cleaning up any consumers that have closed their context
    86  func (e *Eventer) iterateConsumers(event *drivers.TaskEvent) {
    87  	e.consumersLock.Lock()
    88  	filtered := e.consumers[:0]
    89  	for _, consumer := range e.consumers {
    90  
    91  		// prioritize checking if context is cancelled prior
    92  		// to attempting to forwarding events
    93  		// golang select evaluations aren't predictable
    94  		if consumer.ctx.Err() != nil {
    95  			close(consumer.ch)
    96  			continue
    97  		}
    98  
    99  		select {
   100  		case <-time.After(consumer.timeout):
   101  			filtered = append(filtered, consumer)
   102  			e.logger.Warn("timeout sending event", "task_id", event.TaskID, "message", event.Message)
   103  		case <-consumer.ctx.Done():
   104  			// consumer context finished, filtering it out of loop
   105  			close(consumer.ch)
   106  		case consumer.ch <- event:
   107  			filtered = append(filtered, consumer)
   108  		}
   109  	}
   110  	e.consumers = filtered
   111  	e.consumersLock.Unlock()
   112  }
   113  
   114  func (e *Eventer) gcConsumers() {
   115  	e.consumersLock.Lock()
   116  	filtered := e.consumers[:0]
   117  	for _, consumer := range e.consumers {
   118  		select {
   119  		case <-consumer.ctx.Done():
   120  			// consumer context finished, filtering it out of loop
   121  		default:
   122  			filtered = append(filtered, consumer)
   123  		}
   124  	}
   125  	e.consumers = filtered
   126  	e.consumersLock.Unlock()
   127  }
   128  
   129  func (e *Eventer) newConsumer(ctx context.Context) *eventConsumer {
   130  	e.consumersLock.Lock()
   131  	defer e.consumersLock.Unlock()
   132  
   133  	consumer := &eventConsumer{
   134  		ch:      make(chan *drivers.TaskEvent),
   135  		ctx:     ctx,
   136  		timeout: DefaultSendEventTimeout,
   137  		logger:  e.logger,
   138  	}
   139  	e.consumers = append(e.consumers, consumer)
   140  
   141  	return consumer
   142  }
   143  
   144  // TaskEvents is an implementation of the DriverPlugin.TaskEvents function
   145  func (e *Eventer) TaskEvents(ctx context.Context) (<-chan *drivers.TaskEvent, error) {
   146  	consumer := e.newConsumer(ctx)
   147  	return consumer.ch, nil
   148  }
   149  
   150  // EmitEvent can be used to broadcast a new event
   151  func (e *Eventer) EmitEvent(event *drivers.TaskEvent) error {
   152  
   153  	select {
   154  	case <-e.ctx.Done():
   155  		return e.ctx.Err()
   156  	case e.events <- event:
   157  		if e.logger.IsTrace() {
   158  			e.logger.Trace("emitting event", "event", event)
   159  		}
   160  	}
   161  	return nil
   162  }