github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/uniter/pebblenotices.go (about)

     1  // Copyright 2023 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package uniter
     5  
     6  import (
     7  	"time"
     8  
     9  	"github.com/canonical/pebble/client"
    10  	"github.com/juju/clock"
    11  	"github.com/juju/errors"
    12  	"github.com/juju/worker/v3"
    13  	"gopkg.in/tomb.v2"
    14  
    15  	"github.com/juju/juju/worker/uniter/container"
    16  )
    17  
    18  type pebbleNoticer struct {
    19  	logger            Logger
    20  	clock             clock.Clock
    21  	workloadEventChan chan string
    22  	workloadEvents    container.WorkloadEvents
    23  	newPebbleClient   NewPebbleClientFunc
    24  
    25  	tomb tomb.Tomb
    26  }
    27  
    28  // NewPebbleNoticer starts a worker that watches for Pebble notices on the
    29  // specified containers.
    30  func NewPebbleNoticer(
    31  	logger Logger,
    32  	clock clock.Clock,
    33  	containerNames []string,
    34  	workloadEventChan chan string,
    35  	workloadEvents container.WorkloadEvents,
    36  	newPebbleClient NewPebbleClientFunc,
    37  ) worker.Worker {
    38  	if newPebbleClient == nil {
    39  		newPebbleClient = func(config *client.Config) (PebbleClient, error) {
    40  			return client.New(config)
    41  		}
    42  	}
    43  	noticer := &pebbleNoticer{
    44  		logger:            logger,
    45  		clock:             clock,
    46  		workloadEventChan: workloadEventChan,
    47  		workloadEvents:    workloadEvents,
    48  		newPebbleClient:   newPebbleClient,
    49  	}
    50  	for _, name := range containerNames {
    51  		name := name
    52  		noticer.tomb.Go(func() error {
    53  			return noticer.run(name)
    54  		})
    55  	}
    56  	return noticer
    57  }
    58  
    59  // Kill is part of the worker.Worker interface.
    60  func (n *pebbleNoticer) Kill() {
    61  	n.tomb.Kill(nil)
    62  }
    63  
    64  // Wait is part of the worker.Worker interface.
    65  func (n *pebbleNoticer) Wait() error {
    66  	return n.tomb.Wait()
    67  }
    68  
    69  func (n *pebbleNoticer) run(containerName string) (err error) {
    70  	const (
    71  		waitTimeout = 30 * time.Second
    72  		errorDelay  = time.Second
    73  	)
    74  
    75  	n.logger.Debugf("container %q: pebbleNoticer starting", containerName)
    76  	defer n.logger.Debugf("container %q: pebbleNoticer stopped, error %v", containerName, err)
    77  
    78  	config := newPebbleConfig(containerName)
    79  	pebbleClient, err := n.newPebbleClient(config)
    80  	if err != nil {
    81  		return errors.Trace(err)
    82  	}
    83  	defer pebbleClient.CloseIdleConnections()
    84  
    85  	var after time.Time
    86  	ctx := n.tomb.Context(nil)
    87  	for {
    88  		// Wait up to a timeout for new notices to arrive (also stop when
    89  		// tomb's context is cancelled).
    90  		options := &client.NoticesOptions{After: after}
    91  		notices, err := pebbleClient.WaitNotices(ctx, waitTimeout, options)
    92  
    93  		// Return early if the worker was killed.
    94  		select {
    95  		case <-n.tomb.Dying():
    96  			return tomb.ErrDying
    97  		default:
    98  		}
    99  
   100  		// If an error occurred, wait a bit and try again.
   101  		if err != nil {
   102  			var socketNotFound *client.SocketNotFoundError
   103  			if errors.As(err, &socketNotFound) {
   104  				// Pebble has probably not started yet -- not an error.
   105  				n.logger.Debugf("container %q: socket %q not found, waiting %s",
   106  					containerName, socketNotFound.Path, errorDelay)
   107  			} else {
   108  				n.logger.Errorf("container %q: WaitNotices error, waiting %s: %v",
   109  					containerName, errorDelay, err)
   110  			}
   111  			select {
   112  			case <-n.clock.After(errorDelay):
   113  			case <-n.tomb.Dying():
   114  				return tomb.ErrDying
   115  			}
   116  			continue
   117  		}
   118  
   119  		// Send any notices as Juju events.
   120  		for _, notice := range notices {
   121  			err := n.processNotice(containerName, notice)
   122  			if err != nil {
   123  				// Avoid wrapping or tracing this error, as processNotice can
   124  				// return tomb.ErrDying, and tomb doesn't use errors.Is yet.
   125  				return err
   126  			}
   127  
   128  			// Update the next "after" query time to the latest LastRepeated value.
   129  			after = notice.LastRepeated
   130  		}
   131  	}
   132  }
   133  
   134  func (n *pebbleNoticer) processNotice(containerName string, notice *client.Notice) error {
   135  	var eventType container.WorkloadEventType
   136  	switch notice.Type {
   137  	case client.CustomNotice:
   138  		eventType = container.CustomNoticeEvent
   139  	default:
   140  		n.logger.Debugf("container %q: ignoring %s notice", containerName, notice.Type)
   141  		return nil
   142  	}
   143  
   144  	n.logger.Debugf("container %q: processing %s notice, key %q", containerName, notice.Type, notice.Key)
   145  
   146  	errChan := make(chan error, 1)
   147  	eventID := n.workloadEvents.AddWorkloadEvent(container.WorkloadEvent{
   148  		Type:         eventType,
   149  		WorkloadName: containerName,
   150  		NoticeID:     notice.ID,
   151  		NoticeType:   string(notice.Type),
   152  		NoticeKey:    notice.Key,
   153  	}, func(err error) {
   154  		errChan <- errors.Trace(err)
   155  	})
   156  	defer n.workloadEvents.RemoveWorkloadEvent(eventID)
   157  
   158  	// Send the event to the charm!
   159  	select {
   160  	case n.workloadEventChan <- eventID:
   161  	case <-n.tomb.Dying():
   162  		return tomb.ErrDying
   163  	}
   164  
   165  	select {
   166  	case err := <-errChan:
   167  		if err != nil {
   168  			return errors.Annotatef(err, "failed to send event for %s notice, key %q",
   169  				notice.Type, notice.Key)
   170  		}
   171  	case <-n.tomb.Dying():
   172  		return tomb.ErrDying
   173  	}
   174  
   175  	return nil
   176  }