3b57df33e3
Before this change, Client had 2 copies of the config object: config and configCopy. There was no guidance around which to use where (other than configCopy's comment to pass it to alloc runners), both are shared among goroutines and mutated in data racy ways. At least at one point I think the idea was to have `config` be mutable and then grab a lock to overwrite `configCopy`'s pointer atomically. This would have allowed alloc runners to read their config copies in data race safe ways, but this isn't how the current implementation worked. This change takes the following approach to safely handling configs in the client: 1. `Client.config` is the only copy of the config and all access must go through the `Client.configLock` mutex 2. Since the mutex *only protects the config pointer itself and not fields inside the Config struct:* all config mutation must be done on a *copy* of the config, and then Client's config pointer is overwritten while the mutex is acquired. Alloc runners and other goroutines with the old config pointer will not see config updates. 3. Deep copying is implemented on the Config struct to satisfy the previous approach. The TLS Keyloader is an exception because it has its own internal locking to support mutating in place. An unfortunate complication but one I couldn't find a way to untangle in a timely fashion. 4. To facilitate deep copying I made an *internally backward incompatible API change:* our `helper/funcs` used to turn containers (slices and maps) with 0 elements into nils. This probably saves a few memory allocations but makes it very easy to cause panics. Since my new config handling approach uses more copying, it became very difficult to ensure all code that used containers on configs could handle nils properly. Since this code has caused panics in the past, I fixed it: nil containers are copied as nil, but 0-element containers properly return a new 0-element container. No more "downgrading to nil!"
133 lines
3.8 KiB
Go
133 lines
3.8 KiB
Go
package client
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/rpc"
|
|
"time"
|
|
|
|
"github.com/hashicorp/nomad/client/config"
|
|
"github.com/hashicorp/nomad/client/fingerprint"
|
|
"github.com/hashicorp/nomad/client/servers"
|
|
"github.com/hashicorp/nomad/client/serviceregistration/mock"
|
|
agentconsul "github.com/hashicorp/nomad/command/agent/consul"
|
|
"github.com/hashicorp/nomad/helper/pluginutils/catalog"
|
|
"github.com/hashicorp/nomad/helper/pluginutils/singleton"
|
|
"github.com/hashicorp/nomad/helper/pool"
|
|
"github.com/hashicorp/nomad/helper/testlog"
|
|
testing "github.com/mitchellh/go-testing-interface"
|
|
)
|
|
|
|
// TestClient creates an in-memory client for testing purposes and returns a
|
|
// cleanup func to shutdown the client and remove the alloc and state dirs.
|
|
//
|
|
// There is no need to override the AllocDir or StateDir as they are randomized
|
|
// and removed in the returned cleanup function. If they are overridden in the
|
|
// callback then the caller still must run the returned cleanup func.
|
|
func TestClient(t testing.T, cb func(c *config.Config)) (*Client, func() error) {
|
|
return TestClientWithRPCs(t, cb, nil)
|
|
}
|
|
|
|
func TestClientWithRPCs(t testing.T, cb func(c *config.Config), rpcs map[string]interface{}) (*Client, func() error) {
|
|
conf, cleanup := config.TestClientConfig(t)
|
|
|
|
// Tighten the fingerprinter timeouts (must be done in client package
|
|
// to avoid circular dependencies)
|
|
if conf.Options == nil {
|
|
conf.Options = make(map[string]string)
|
|
}
|
|
conf.Options[fingerprint.TightenNetworkTimeoutsConfig] = "true"
|
|
|
|
logger := testlog.HCLogger(t)
|
|
conf.Logger = logger
|
|
|
|
if cb != nil {
|
|
cb(conf)
|
|
}
|
|
|
|
// Set the plugin loaders
|
|
if conf.PluginLoader == nil {
|
|
conf.PluginLoader = catalog.TestPluginLoaderWithOptions(t, "", conf.Options, nil)
|
|
}
|
|
if conf.PluginSingletonLoader == nil {
|
|
conf.PluginSingletonLoader = singleton.NewSingletonLoader(logger, conf.PluginLoader)
|
|
}
|
|
mockCatalog := agentconsul.NewMockCatalog(logger)
|
|
mockService := mock.NewServiceRegistrationHandler(logger)
|
|
client, err := NewClient(conf, mockCatalog, nil, mockService, rpcs)
|
|
if err != nil {
|
|
cleanup()
|
|
t.Fatalf("err: %v", err)
|
|
}
|
|
return client, func() error {
|
|
ch := make(chan error)
|
|
|
|
go func() {
|
|
defer close(ch)
|
|
|
|
// Shutdown client
|
|
err := client.Shutdown()
|
|
if err != nil {
|
|
ch <- fmt.Errorf("failed to shutdown client: %v", err)
|
|
}
|
|
|
|
// Call TestClientConfig cleanup
|
|
cleanup()
|
|
}()
|
|
|
|
select {
|
|
case e := <-ch:
|
|
return e
|
|
case <-time.After(1 * time.Minute):
|
|
return fmt.Errorf("timed out while shutting down client")
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestRPCOnlyClient is a client that only pings to establish a connection
|
|
// with the server and then returns mock RPC responses for those interfaces
|
|
// passed in the `rpcs` parameter. Useful for testing client RPCs from the
|
|
// server. Returns the Client, a shutdown function, and any error.
|
|
func TestRPCOnlyClient(t testing.T, srvAddr net.Addr, rpcs map[string]interface{}) (*Client, func() error, error) {
|
|
var err error
|
|
conf, cleanup := config.TestClientConfig(t)
|
|
|
|
client := &Client{config: conf, logger: testlog.HCLogger(t)}
|
|
client.servers = servers.New(client.logger, client.shutdownCh, client)
|
|
|
|
client.rpcServer = rpc.NewServer()
|
|
for name, rpc := range rpcs {
|
|
client.rpcServer.RegisterName(name, rpc)
|
|
}
|
|
|
|
client.connPool = pool.NewPool(testlog.HCLogger(t), 10*time.Second, 10, nil)
|
|
|
|
cancelFunc := func() error {
|
|
ch := make(chan error)
|
|
|
|
go func() {
|
|
defer close(ch)
|
|
client.connPool.Shutdown()
|
|
client.shutdownGroup.Wait()
|
|
cleanup()
|
|
}()
|
|
|
|
select {
|
|
case <-ch:
|
|
return nil
|
|
case <-time.After(1 * time.Minute):
|
|
return fmt.Errorf("timed out while shutting down client")
|
|
}
|
|
}
|
|
|
|
go client.rpcConnListener()
|
|
|
|
_, err = client.SetServers([]string{srvAddr.String()})
|
|
if err != nil {
|
|
return nil, cancelFunc, fmt.Errorf("could not set servers: %v", err)
|
|
}
|
|
client.shutdownGroup.Go(client.registerAndHeartbeat)
|
|
|
|
return client, cancelFunc, nil
|
|
}
|