github.com/hernad/nomad@v1.6.112/helper/pluginutils/singleton/singleton.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package singleton
     5  
     6  import (
     7  	"fmt"
     8  	"sync"
     9  
    10  	log "github.com/hashicorp/go-hclog"
    11  	plugin "github.com/hashicorp/go-plugin"
    12  	"github.com/hernad/nomad/helper/pluginutils/loader"
    13  	"github.com/hernad/nomad/plugins/base"
    14  )
    15  
    16  var (
    17  	// SingletonPluginExited is returned when the dispense is called and the
    18  	// existing plugin has exited. The caller should retry, and this will issue
    19  	// a new plugin instance.
    20  	SingletonPluginExited = fmt.Errorf("singleton plugin exited")
    21  )
    22  
    23  // SingletonLoader is used to only load a single external plugin at a time.
    24  type SingletonLoader struct {
    25  	// Loader is the underlying plugin loader that we wrap to give a singleton
    26  	// behavior.
    27  	loader loader.PluginCatalog
    28  
    29  	// instances is a mapping of the plugin to a future which holds a plugin
    30  	// instance
    31  	instances    map[loader.PluginID]*future
    32  	instanceLock sync.Mutex
    33  
    34  	// logger is the logger used by the singleton
    35  	logger log.Logger
    36  }
    37  
    38  // NewSingletonLoader wraps a plugin catalog and provides singleton behavior on
    39  // top by caching running instances.
    40  func NewSingletonLoader(logger log.Logger, catalog loader.PluginCatalog) *SingletonLoader {
    41  	return &SingletonLoader{
    42  		loader:    catalog,
    43  		logger:    logger.Named("singleton_plugin_loader"),
    44  		instances: make(map[loader.PluginID]*future, 4),
    45  	}
    46  }
    47  
    48  // Catalog returns the catalog of all plugins keyed by plugin type
    49  func (s *SingletonLoader) Catalog() map[string][]*base.PluginInfoResponse {
    50  	return s.loader.Catalog()
    51  }
    52  
    53  // Dispense returns the plugin given its name and type. This will also
    54  // configure the plugin. If there is an instance of an already running plugin,
    55  // this is used.
    56  func (s *SingletonLoader) Dispense(name, pluginType string, config *base.AgentConfig, logger log.Logger) (loader.PluginInstance, error) {
    57  	return s.getPlugin(false, name, pluginType, logger, config, nil)
    58  }
    59  
    60  // Reattach is used to reattach to a previously launched external plugin.
    61  func (s *SingletonLoader) Reattach(name, pluginType string, config *plugin.ReattachConfig) (loader.PluginInstance, error) {
    62  	return s.getPlugin(true, name, pluginType, nil, nil, config)
    63  }
    64  
    65  // getPlugin is a helper that either dispenses or reattaches to a plugin using
    66  // futures to ensure only a single instance is retrieved
    67  func (s *SingletonLoader) getPlugin(reattach bool, name, pluginType string, logger log.Logger,
    68  	nomadConfig *base.AgentConfig, config *plugin.ReattachConfig) (loader.PluginInstance, error) {
    69  
    70  	// Lock the instance map to prevent races
    71  	s.instanceLock.Lock()
    72  
    73  	// Check if there is a future already
    74  	id := loader.PluginID{Name: name, PluginType: pluginType}
    75  	f, ok := s.instances[id]
    76  
    77  	// Create the future and go get a plugin
    78  	if !ok {
    79  		f = newFuture()
    80  		s.instances[id] = f
    81  
    82  		if reattach {
    83  			go s.reattach(f, name, pluginType, config)
    84  		} else {
    85  			go s.dispense(f, name, pluginType, nomadConfig, logger)
    86  		}
    87  	}
    88  
    89  	// Unlock so that the created future can be shared
    90  	s.instanceLock.Unlock()
    91  
    92  	i, err := f.wait().result()
    93  	if err != nil {
    94  		s.clearFuture(id, f)
    95  		return nil, err
    96  	}
    97  
    98  	if i.Exited() {
    99  		s.clearFuture(id, f)
   100  		return nil, SingletonPluginExited
   101  	}
   102  
   103  	return i, nil
   104  }
   105  
   106  // dispense should be called in a go routine to not block and creates the
   107  // desired plugin, setting the results in the future.
   108  func (s *SingletonLoader) dispense(f *future, name, pluginType string, config *base.AgentConfig, logger log.Logger) {
   109  	i, err := s.loader.Dispense(name, pluginType, config, logger)
   110  	f.set(i, err)
   111  }
   112  
   113  // reattach should be called in a go routine to not block and reattaches to the
   114  // desired plugin, setting the results in the future.
   115  func (s *SingletonLoader) reattach(f *future, name, pluginType string, config *plugin.ReattachConfig) {
   116  	i, err := s.loader.Reattach(name, pluginType, config)
   117  	f.set(i, err)
   118  }
   119  
   120  // clearFuture clears the future from the instances map only if the futures
   121  // match. This prevents clearing the unintented instance.
   122  func (s *SingletonLoader) clearFuture(id loader.PluginID, f *future) {
   123  	s.instanceLock.Lock()
   124  	defer s.instanceLock.Unlock()
   125  	if f.equal(s.instances[id]) {
   126  		delete(s.instances, id)
   127  	}
   128  }