github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/m3em/integration/harness.go (about)

     1  // Copyright (c) 2017 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  // +build integration
    22  
    23  package integration
    24  
    25  import (
    26  	"io"
    27  	"net"
    28  	"os"
    29  	"strings"
    30  	"sync/atomic"
    31  	"testing"
    32  	"time"
    33  
    34  	"github.com/m3db/m3/src/cluster/placement"
    35  	"github.com/m3db/m3/src/m3em/agent"
    36  	hb "github.com/m3db/m3/src/m3em/generated/proto/heartbeat"
    37  	"github.com/m3db/m3/src/m3em/generated/proto/m3em"
    38  	"github.com/m3db/m3/src/m3em/integration/resources"
    39  	"github.com/m3db/m3/src/m3em/node"
    40  	xgrpc "github.com/m3db/m3/src/m3em/x/grpc"
    41  	xerrors "github.com/m3db/m3/src/x/errors"
    42  	"github.com/m3db/m3/src/x/instrument"
    43  	xtest "github.com/m3db/m3/src/x/test"
    44  
    45  	"github.com/stretchr/testify/require"
    46  	"go.uber.org/zap"
    47  	"google.golang.org/grpc"
    48  	"google.golang.org/grpc/credentials"
    49  )
    50  
    51  type closeFn func() error
    52  
    53  type testHarness struct {
    54  	io.Closer
    55  
    56  	closers           []closeFn
    57  	t                 *testing.T
    58  	scriptNum         int
    59  	harnessDir        string
    60  	iopts             instrument.Options
    61  	logger            *zap.Logger
    62  	agentListener     net.Listener
    63  	agentOptions      agent.Options
    64  	agentService      agent.Agent
    65  	agentServer       *grpc.Server
    66  	agentStopped      int32
    67  	nodeOptions       node.Options
    68  	nodeService       node.ServiceNode
    69  	heartbeatListener net.Listener
    70  	heartbeatServer   *grpc.Server
    71  	heartbeatStopped  int32
    72  }
    73  
    74  func newTestHarnessWithHearbeat(t *testing.T) *testHarness {
    75  	return newTestHarnessWithHearbeatOptions(t, testHeartbeatOptions())
    76  }
    77  
    78  func newTestHarness(t *testing.T) *testHarness {
    79  	return newTestHarnessWithHearbeatOptions(t, nil)
    80  }
    81  
    82  func newTestHarnessWithHearbeatOptions(t *testing.T, hbOpts node.HeartbeatOptions) *testHarness {
    83  	logger := xtest.NewLogger(t)
    84  	useTLS := strings.ToLower(os.Getenv("TEST_TLS_COMMUNICATION")) == "true"
    85  	if useTLS {
    86  		logger.Info("using TLS for RPC")
    87  	} else {
    88  		logger.Info("not using TLS for RPC")
    89  	}
    90  
    91  	th := &testHarness{
    92  		t:      t,
    93  		logger: logger,
    94  		iopts:  instrument.NewOptions().SetLogger(logger),
    95  	}
    96  
    97  	th.harnessDir = newTempDir(t)
    98  	th.addCloser(func() error {
    99  		return os.RemoveAll(th.harnessDir)
   100  	})
   101  
   102  	// create agent listener
   103  	agentListener, err := net.Listen("tcp", "127.0.0.1:0")
   104  	require.NoError(t, err)
   105  	th.agentListener = agentListener
   106  	th.addCloser(func() error {
   107  		return agentListener.Close()
   108  	})
   109  
   110  	var serverCreds credentials.TransportCredentials
   111  	if useTLS {
   112  		serverCreds, err = resources.ServerTransportCredentials()
   113  		require.NoError(th.t, err)
   114  	}
   115  
   116  	// create agent (service|server)
   117  	th.agentOptions = agent.NewOptions(th.iopts).
   118  		SetWorkingDirectory(newSubDir(t, th.harnessDir, "agent-wd")).
   119  		SetExecGenFn(testExecGenFn)
   120  	service, err := agent.New(th.agentOptions)
   121  	require.NoError(t, err)
   122  	th.agentService = service
   123  	th.agentServer = xgrpc.NewServer(serverCreds)
   124  	m3em.RegisterOperatorServer(th.agentServer, service)
   125  	th.addCloser(func() error {
   126  		th.agentServer.GracefulStop()
   127  		return nil
   128  	})
   129  
   130  	// if provided valid heartbeat options, create heartbeating resources
   131  	if hbOpts != nil {
   132  		heartbeatListener, err := net.Listen("tcp", "127.0.0.1:0")
   133  		require.NoError(t, err)
   134  		th.heartbeatListener = heartbeatListener
   135  		th.addCloser(func() error {
   136  			return heartbeatListener.Close()
   137  		})
   138  
   139  		hbRouter := node.NewHeartbeatRouter(heartbeatListener.Addr().String())
   140  		hbServer := xgrpc.NewServer(nil)
   141  		hb.RegisterHeartbeaterServer(hbServer, hbRouter)
   142  		th.heartbeatServer = hbServer
   143  		th.addCloser(func() error {
   144  			th.heartbeatServer.GracefulStop()
   145  			return nil
   146  		})
   147  		hbOpts = hbOpts.SetEnabled(true).SetHeartbeatRouter(hbRouter)
   148  
   149  	} else {
   150  		hbOpts = node.NewHeartbeatOptions().SetEnabled(false)
   151  	}
   152  
   153  	// create options to communicate with agent
   154  	th.nodeOptions = th.newNodeOptions(hbOpts, useTLS)
   155  	svc := placement.NewInstance()
   156  	node, err := node.New(svc, th.nodeOptions)
   157  	require.NoError(t, err)
   158  	th.nodeService = node
   159  	th.addCloser(func() error {
   160  		return node.Close()
   161  	})
   162  
   163  	return th
   164  }
   165  
   166  func (th *testHarness) addCloser(fn closeFn) {
   167  	th.closers = append(th.closers, fn)
   168  }
   169  
   170  func (th *testHarness) Start() {
   171  	serveGRPCServer := func(server *grpc.Server, listener net.Listener, state *int32) {
   172  		err := server.Serve(listener)
   173  		if closed := atomic.LoadInt32(state); closed == 0 {
   174  			require.NoError(th.t, err)
   175  		}
   176  	}
   177  
   178  	if th.nodeOptions.HeartbeatOptions().Enabled() {
   179  		go serveGRPCServer(th.heartbeatServer, th.heartbeatListener, &th.heartbeatStopped)
   180  	}
   181  
   182  	go serveGRPCServer(th.agentServer, th.agentListener, &th.agentStopped)
   183  }
   184  
   185  func (th *testHarness) StopHeartbeatServer() {
   186  	atomic.StoreInt32(&th.heartbeatStopped, 1)
   187  	th.heartbeatServer.Stop()
   188  }
   189  
   190  func (th *testHarness) Close() error {
   191  	atomic.StoreInt32(&th.agentStopped, 1)
   192  	atomic.StoreInt32(&th.heartbeatStopped, 1)
   193  
   194  	var multiErr xerrors.MultiError
   195  	for i := len(th.closers) - 1; i >= 0; i-- {
   196  		closer := th.closers[i]
   197  		multiErr = multiErr.Add(closer())
   198  	}
   199  	return multiErr.FinalError()
   200  }
   201  
   202  func (th *testHarness) newTempFile(contents []byte) *os.File {
   203  	file := newTempFile(th.t, th.harnessDir, contents)
   204  	th.addCloser(func() error {
   205  		return os.Remove(file.Name())
   206  	})
   207  	return file
   208  }
   209  
   210  func (th *testHarness) newTestScript(program testProgram) string {
   211  	sn := th.scriptNum
   212  	th.scriptNum++
   213  	file := newTestScript(th.t, th.harnessDir, sn, program)
   214  	th.addCloser(func() error {
   215  		return os.Remove(file)
   216  	})
   217  	return file
   218  }
   219  
   220  func testHeartbeatOptions() node.HeartbeatOptions {
   221  	return node.NewHeartbeatOptions().
   222  		SetEnabled(true).
   223  		SetTimeout(2 * time.Second).
   224  		SetCheckInterval(100 * time.Millisecond).
   225  		SetInterval(time.Second)
   226  }
   227  
   228  func (th *testHarness) newNodeOptions(hbOpts node.HeartbeatOptions, useTLS bool) node.Options {
   229  	return node.NewOptions(th.iopts).
   230  		SetHeartbeatOptions(hbOpts).
   231  		SetOperatorClientFn(th.testOperatorClientFn(useTLS))
   232  }
   233  
   234  func (th *testHarness) testOperatorClientFn(useTLS bool) node.OperatorClientFn {
   235  	endpoint := th.agentListener.Addr().String()
   236  	dialOpt := grpc.WithInsecure()
   237  	if useTLS {
   238  		tc, err := resources.ClientTransportCredentials()
   239  		require.NoError(th.t, err)
   240  		dialOpt = grpc.WithTransportCredentials(tc)
   241  	}
   242  	return func() (*grpc.ClientConn, m3em.OperatorClient, error) {
   243  		conn, err := grpc.Dial(endpoint, dialOpt)
   244  		if err != nil {
   245  			return nil, nil, err
   246  		}
   247  		return conn, m3em.NewOperatorClient(conn), err
   248  	}
   249  }