Files
nomad/plugins/shared/singleton/singleton.go
Alex Dadgar 57f40c7e3e Device manager
Introduce a device manager that manages the lifecycle of device plugins
on the client. It fingerprints, collects stats, and forwards Reserve
requests to the correct plugin. The manager, also handles device plugins
failing and validates their output.
2018-11-07 10:43:15 -08:00

126 lines
4.0 KiB
Go

package singleton
import (
"fmt"
"sync"
log "github.com/hashicorp/go-hclog"
plugin "github.com/hashicorp/go-plugin"
"github.com/hashicorp/nomad/plugins/base"
"github.com/hashicorp/nomad/plugins/shared/loader"
)
var (
// SingletonPluginExited is returned when the dispense is called and the
// existing plugin has exited. The caller should retry, and this will issue
// a new plugin instance.
SingletonPluginExited = fmt.Errorf("singleton plugin exited")
)
// SingletonLoader is used to only load a single external plugin at a time.
type SingletonLoader struct {
// Loader is the underlying plugin loader that we wrap to give a singleton
// behavior.
loader loader.PluginCatalog
// instances is a mapping of the plugin to a future which holds a plugin
// instance
instances map[loader.PluginID]*future
instanceLock sync.Mutex
// logger is the logger used by the singleton
logger log.Logger
}
// NewSingletonLoader wraps a plugin catalog and provides singleton behavior on
// top by caching running instances.
func NewSingletonLoader(logger log.Logger, catalog loader.PluginCatalog) *SingletonLoader {
return &SingletonLoader{
loader: catalog,
logger: logger.Named("singleton_plugin_loader"),
instances: make(map[loader.PluginID]*future, 4),
}
}
// Catalog returns the catalog of all plugins keyed by plugin type
func (s *SingletonLoader) Catalog() map[string][]*base.PluginInfoResponse {
return s.loader.Catalog()
}
// Dispense returns the plugin given its name and type. This will also
// configure the plugin. If there is an instance of an already running plugin,
// this is used.
func (s *SingletonLoader) Dispense(name, pluginType string, config *base.ClientAgentConfig, logger log.Logger) (loader.PluginInstance, error) {
return s.getPlugin(false, name, pluginType, logger, config, nil)
}
// Reattach is used to reattach to a previously launched external plugin.
func (s *SingletonLoader) Reattach(name, pluginType string, config *plugin.ReattachConfig) (loader.PluginInstance, error) {
return s.getPlugin(true, name, pluginType, nil, nil, config)
}
// getPlugin is a helper that either dispenses or reattaches to a plugin using
// futures to ensure only a single instance is retrieved
func (s *SingletonLoader) getPlugin(reattach bool, name, pluginType string, logger log.Logger,
nomadConfig *base.ClientAgentConfig, config *plugin.ReattachConfig) (loader.PluginInstance, error) {
// Lock the instance map to prevent races
s.instanceLock.Lock()
// Check if there is a future already
id := loader.PluginID{Name: name, PluginType: pluginType}
f, ok := s.instances[id]
// Create the future and go get a plugin
if !ok {
f = newFuture()
s.instances[id] = f
if reattach {
go s.reattach(f, name, pluginType, config)
} else {
go s.dispense(f, name, pluginType, nomadConfig, logger)
}
}
// Unlock so that the created future can be shared
s.instanceLock.Unlock()
i, err := f.wait().result()
if err != nil {
s.clearFuture(id, f)
return nil, err
}
if i.Exited() {
s.clearFuture(id, f)
return nil, SingletonPluginExited
}
return i, nil
}
// dispense should be called in a go routine to not block and creates the
// desired plugin, setting the results in the future.
func (s *SingletonLoader) dispense(f *future, name, pluginType string, config *base.ClientAgentConfig, logger log.Logger) {
i, err := s.loader.Dispense(name, pluginType, config, logger)
f.set(i, err)
}
// reattach should be called in a go routine to not block and reattaches to the
// desired plugin, setting the results in the future.
func (s *SingletonLoader) reattach(f *future, name, pluginType string, config *plugin.ReattachConfig) {
i, err := s.loader.Reattach(name, pluginType, config)
f.set(i, err)
}
// clearFuture clears the future from the instances map only if the futures
// match. This prevents clearing the unintented instance.
func (s *SingletonLoader) clearFuture(id loader.PluginID, f *future) {
s.instanceLock.Lock()
defer s.instanceLock.Unlock()
if f.equal(s.instances[id]) {
delete(s.instances, id)
}
}