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

     1  // Copyright 2021 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package uniter
     5  
     6  import (
     7  	"context"
     8  	"path"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/canonical/pebble/client"
    13  	"github.com/juju/clock"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/worker/v3"
    16  	"gopkg.in/tomb.v2"
    17  
    18  	"github.com/juju/juju/worker/uniter/container"
    19  )
    20  
    21  // PebbleClient describes the subset of github.com/canonical/pebble/client.Client that we
    22  // need for the PebblePoller.
    23  type PebbleClient interface {
    24  	CloseIdleConnections()
    25  	SysInfo() (*client.SysInfo, error)
    26  	WaitNotices(ctx context.Context, serverTimeout time.Duration, opts *client.NoticesOptions) ([]*client.Notice, error)
    27  }
    28  
    29  // NewPebbleClientFunc is the function type used to create a PebbleClient.
    30  type NewPebbleClientFunc func(*client.Config) (PebbleClient, error)
    31  
    32  type pebblePoller struct {
    33  	logger          Logger
    34  	clock           clock.Clock
    35  	tomb            tomb.Tomb
    36  	newPebbleClient NewPebbleClientFunc
    37  
    38  	workloadEventChan chan string
    39  	workloadEvents    container.WorkloadEvents
    40  
    41  	mut           sync.Mutex
    42  	pebbleBootIDs map[string]string
    43  }
    44  
    45  const (
    46  	pebblePollInterval = 5 * time.Second
    47  )
    48  
    49  // NewPebblePoller starts a worker that polls the pebble interfaces
    50  // of the supplied container list.
    51  func NewPebblePoller(logger Logger,
    52  	clock clock.Clock,
    53  	containerNames []string,
    54  	workloadEventChan chan string,
    55  	workloadEvents container.WorkloadEvents,
    56  	newPebbleClient NewPebbleClientFunc) worker.Worker {
    57  	if newPebbleClient == nil {
    58  		newPebbleClient = func(config *client.Config) (PebbleClient, error) {
    59  			return client.New(config)
    60  		}
    61  	}
    62  	p := &pebblePoller{
    63  		logger:            logger,
    64  		clock:             clock,
    65  		workloadEventChan: workloadEventChan,
    66  		workloadEvents:    workloadEvents,
    67  		newPebbleClient:   newPebbleClient,
    68  		pebbleBootIDs:     make(map[string]string),
    69  	}
    70  	for _, v := range containerNames {
    71  		containerName := v
    72  		p.tomb.Go(func() error {
    73  			return p.run(containerName)
    74  		})
    75  	}
    76  	return p
    77  }
    78  
    79  // Kill is part of the worker.Worker interface.
    80  func (p *pebblePoller) Kill() {
    81  	p.tomb.Kill(nil)
    82  }
    83  
    84  // Wait is part of the worker.Worker interface.
    85  func (p *pebblePoller) Wait() error {
    86  	return p.tomb.Wait()
    87  }
    88  
    89  func (p *pebblePoller) run(containerName string) error {
    90  	timer := p.clock.NewTimer(pebblePollInterval)
    91  	defer timer.Stop()
    92  	for {
    93  		select {
    94  		case <-p.tomb.Dying():
    95  			return tomb.ErrDying
    96  		case <-timer.Chan():
    97  			timer.Reset(pebblePollInterval)
    98  			err := p.poll(containerName)
    99  			var socketNotFound *client.SocketNotFoundError
   100  			if errors.As(err, &socketNotFound) {
   101  				p.logger.Debugf("pebble still starting up on container %q: %v", containerName, socketNotFound)
   102  			} else if err != nil && err != tomb.ErrDying {
   103  				p.logger.Errorf("pebble poll failed for container %q: %v", containerName, err)
   104  			}
   105  		}
   106  	}
   107  }
   108  
   109  func newPebbleConfig(containerName string) *client.Config {
   110  	return &client.Config{
   111  		Socket: path.Join("/charm/containers", containerName, "pebble.socket"),
   112  	}
   113  }
   114  
   115  func (p *pebblePoller) poll(containerName string) error {
   116  	config := newPebbleConfig(containerName)
   117  	pc, err := p.newPebbleClient(config)
   118  	if err != nil {
   119  		return errors.Annotate(err, "failed to create Pebble client")
   120  	}
   121  	defer pc.CloseIdleConnections()
   122  	info, err := pc.SysInfo()
   123  	if err != nil {
   124  		return errors.Annotate(err, "failed to get pebble info")
   125  	}
   126  
   127  	p.mut.Lock()
   128  	lastBootID, _ := p.pebbleBootIDs[containerName]
   129  	p.mut.Unlock()
   130  	if lastBootID == info.BootID {
   131  		// Boot ID is the same as last time, so it's normal poll and no
   132  		// pebble-ready event is needed.
   133  		return nil
   134  	}
   135  
   136  	// We've just started up, so send a pebble-ready event.
   137  	errChan := make(chan error, 1)
   138  	eid := p.workloadEvents.AddWorkloadEvent(container.WorkloadEvent{
   139  		Type:         container.ReadyEvent,
   140  		WorkloadName: containerName,
   141  	}, func(err error) {
   142  		errChan <- errors.Trace(err)
   143  	})
   144  	defer p.workloadEvents.RemoveWorkloadEvent(eid)
   145  
   146  	select {
   147  	case p.workloadEventChan <- eid:
   148  	case <-p.tomb.Dying():
   149  		return tomb.ErrDying
   150  	}
   151  
   152  	select {
   153  	case err := <-errChan:
   154  		if err != nil {
   155  			return errors.Annotate(err, "failed to send pebble-ready event")
   156  		}
   157  	case <-p.tomb.Dying():
   158  		return tomb.ErrDying
   159  	}
   160  
   161  	p.mut.Lock()
   162  	p.pebbleBootIDs[containerName] = info.BootID
   163  	p.mut.Unlock()
   164  
   165  	return nil
   166  }