github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/worker/changestream/worker.go (about)

     1  // Copyright 2023 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package changestream
     5  
     6  import (
     7  	"github.com/juju/clock"
     8  	"github.com/juju/errors"
     9  	"github.com/juju/worker/v3"
    10  	"github.com/juju/worker/v3/catacomb"
    11  
    12  	"github.com/juju/juju/core/changestream"
    13  	coredatabase "github.com/juju/juju/core/database"
    14  	"github.com/juju/juju/worker/changestream/eventqueue"
    15  	"github.com/juju/juju/worker/changestream/stream"
    16  	"github.com/juju/juju/worker/filenotifywatcher"
    17  )
    18  
    19  // DBGetter describes the ability to supply a sql.DB
    20  // reference for a particular database.
    21  type DBGetter = coredatabase.DBGetter
    22  
    23  // FileNotifyWatcher is the interface that the worker uses to interact with the
    24  // file notify watcher.
    25  type FileNotifyWatcher = filenotifywatcher.FileNotifyWatcher
    26  
    27  // FileNotifier represents a way to watch for changes in a namespace folder
    28  // directory.
    29  type FileNotifier interface {
    30  	// Changes returns a channel if a file was created or deleted.
    31  	Changes() (<-chan bool, error)
    32  }
    33  
    34  // ChangeStream represents an interface for getting an event queue for
    35  // a particular namespace.
    36  type ChangeStream interface {
    37  	EventQueue(string) (EventQueue, error)
    38  }
    39  
    40  // EventQueue represents an interface for managing subscriptions to listen to
    41  // changes from the database change log.
    42  type EventQueue interface {
    43  	// Subscribe returns a new subscription to listen to changes from the
    44  	// database change log.
    45  	Subscribe(...changestream.SubscriptionOption) (changestream.Subscription, error)
    46  }
    47  
    48  // EventQueueWorker represents a worker for subscribing to events from the
    49  // database change log.
    50  type EventQueueWorker interface {
    51  	worker.Worker
    52  	EventQueue() EventQueue
    53  }
    54  
    55  // WorkerConfig encapsulates the configuration options for the
    56  // changestream worker.
    57  type WorkerConfig struct {
    58  	DBGetter            DBGetter
    59  	FileNotifyWatcher   FileNotifyWatcher
    60  	Clock               clock.Clock
    61  	Logger              Logger
    62  	NewEventQueueWorker EventQueueWorkerFn
    63  }
    64  
    65  // Validate ensures that the config values are valid.
    66  func (c *WorkerConfig) Validate() error {
    67  	if c.DBGetter == nil {
    68  		return errors.NotValidf("missing DBGetter")
    69  	}
    70  	if c.FileNotifyWatcher == nil {
    71  		return errors.NotValidf("missing FileNotifyWatcher")
    72  	}
    73  	if c.Clock == nil {
    74  		return errors.NotValidf("missing clock")
    75  	}
    76  	if c.Logger == nil {
    77  		return errors.NotValidf("missing logger")
    78  	}
    79  	if c.NewEventQueueWorker == nil {
    80  		return errors.NotValidf("missing NewEventQueueWorker")
    81  	}
    82  	return nil
    83  }
    84  
    85  type changeStreamWorker struct {
    86  	cfg      WorkerConfig
    87  	catacomb catacomb.Catacomb
    88  	runner   *worker.Runner
    89  }
    90  
    91  func newWorker(cfg WorkerConfig) (*changeStreamWorker, error) {
    92  	var err error
    93  	if err = cfg.Validate(); err != nil {
    94  		return nil, errors.Trace(err)
    95  	}
    96  
    97  	w := &changeStreamWorker{
    98  		cfg: cfg,
    99  		runner: worker.NewRunner(worker.RunnerParams{
   100  			// Prevent the runner from restarting the worker, if one of the
   101  			// workers dies, we want to stop the whole thing.
   102  			IsFatal: func(err error) bool { return false },
   103  			Clock:   cfg.Clock,
   104  		}),
   105  	}
   106  
   107  	if err = catacomb.Invoke(catacomb.Plan{
   108  		Site: &w.catacomb,
   109  		Work: w.loop,
   110  		Init: []worker.Worker{
   111  			w.runner,
   112  		},
   113  	}); err != nil {
   114  		return nil, errors.Trace(err)
   115  	}
   116  
   117  	return w, nil
   118  }
   119  
   120  func (w *changeStreamWorker) loop() (err error) {
   121  	defer w.runner.Kill()
   122  
   123  	for {
   124  		select {
   125  		case <-w.catacomb.Dying():
   126  			return w.catacomb.ErrDying()
   127  		}
   128  	}
   129  }
   130  
   131  // Kill is part of the worker.Worker interface.
   132  func (w *changeStreamWorker) Kill() {
   133  	w.catacomb.Kill(nil)
   134  }
   135  
   136  // Wait is part of the worker.Worker interface.
   137  func (w *changeStreamWorker) Wait() error {
   138  	return w.catacomb.Wait()
   139  }
   140  
   141  // EventQueue returns a new EventQueue for the given namespace. The EventQueue
   142  // will be subscribed to the given options.
   143  func (w *changeStreamWorker) EventQueue(namespace string) (EventQueue, error) {
   144  	if e, err := w.runner.Worker(namespace, w.catacomb.Dying()); err == nil {
   145  		return e.(EventQueueWorker).EventQueue(), nil
   146  	}
   147  
   148  	db, err := w.cfg.DBGetter.GetDB(namespace)
   149  	if err != nil {
   150  		return nil, errors.Trace(err)
   151  	}
   152  
   153  	eqWorker, err := w.cfg.NewEventQueueWorker(db, fileNotifyWatcher{
   154  		fileNotifier: w.cfg.FileNotifyWatcher,
   155  		fileName:     namespace,
   156  	}, w.cfg.Clock, w.cfg.Logger)
   157  	if err != nil {
   158  		return nil, errors.Trace(err)
   159  	}
   160  
   161  	if err := w.runner.StartWorker(namespace, func() (worker.Worker, error) {
   162  		return eqWorker, nil
   163  	}); err != nil {
   164  		return nil, errors.Trace(err)
   165  	}
   166  
   167  	return eqWorker.EventQueue(), nil
   168  }
   169  
   170  // fileNotifyWatcher is a wrapper around the FileNotifyWatcher that is used to
   171  // filter the events to only those that are for the given namespace.
   172  type fileNotifyWatcher struct {
   173  	fileNotifier FileNotifyWatcher
   174  	fileName     string
   175  }
   176  
   177  func (f fileNotifyWatcher) Changes() (<-chan bool, error) {
   178  	return f.fileNotifier.Changes(f.fileName)
   179  }
   180  
   181  // NewEventQueueWorker creates a new EventQueueWorker.
   182  func NewEventQueueWorker(db coredatabase.TrackedDB, fileNotifier FileNotifier, clock clock.Clock, logger Logger) (EventQueueWorker, error) {
   183  	stream := stream.New(db, fileNotifier, clock, logger)
   184  
   185  	eventQueue, err := eventqueue.New(stream, logger)
   186  	if err != nil {
   187  		return nil, errors.Trace(err)
   188  	}
   189  
   190  	w := &eventQueueWorker{
   191  		eventQueue: eventQueue,
   192  	}
   193  
   194  	if err := catacomb.Invoke(catacomb.Plan{
   195  		Site: &w.catacomb,
   196  		Work: w.loop,
   197  		Init: []worker.Worker{
   198  			stream,
   199  			eventQueue,
   200  		},
   201  	}); err != nil {
   202  		return nil, errors.Trace(err)
   203  	}
   204  
   205  	return w, nil
   206  }
   207  
   208  // eventQueueWorker is a worker that is responsible for managing the lifecycle
   209  // of both the DBStream and the EventQueue.
   210  type eventQueueWorker struct {
   211  	catacomb catacomb.Catacomb
   212  
   213  	eventQueue *eventqueue.EventQueue
   214  }
   215  
   216  // Kill is part of the worker.Worker interface.
   217  func (w *eventQueueWorker) Kill() {
   218  	w.catacomb.Kill(nil)
   219  }
   220  
   221  // Wait is part of the worker.Worker interface.
   222  func (w *eventQueueWorker) Wait() error {
   223  	return w.catacomb.Wait()
   224  }
   225  
   226  // EventQueue returns the event queue for this worker.
   227  func (w *eventQueueWorker) EventQueue() EventQueue {
   228  	return w.eventQueue
   229  }
   230  
   231  func (w *eventQueueWorker) loop() error {
   232  	select {
   233  	case <-w.catacomb.Dying():
   234  		return w.catacomb.ErrDying()
   235  	}
   236  }