github.com/hernad/nomad@v1.6.112/drivers/shared/eventer/eventer.go (about)

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