github.com/m3db/m3@v1.5.0/src/m3em/agent/agent_test.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  package agent
    22  
    23  import (
    24  	"fmt"
    25  	"io/ioutil"
    26  	"net"
    27  	"os"
    28  	"sync"
    29  	"testing"
    30  	"time"
    31  
    32  	hb "github.com/m3db/m3/src/m3em/generated/proto/heartbeat"
    33  	"github.com/m3db/m3/src/m3em/generated/proto/m3em"
    34  	"github.com/m3db/m3/src/m3em/os/exec"
    35  	mockexec "github.com/m3db/m3/src/m3em/os/exec/mocks"
    36  	xgrpc "github.com/m3db/m3/src/m3em/x/grpc"
    37  	m3xclock "github.com/m3db/m3/src/x/clock"
    38  	"github.com/m3db/m3/src/x/instrument"
    39  
    40  	"github.com/golang/mock/gomock"
    41  	"github.com/stretchr/testify/require"
    42  	context "golang.org/x/net/context"
    43  )
    44  
    45  func newTempDir(t *testing.T) string {
    46  	path, err := ioutil.TempDir("", "agent-test")
    47  	require.NoError(t, err)
    48  	return path
    49  }
    50  
    51  func newTestOptions(workingDir string) Options {
    52  	iopts := instrument.NewOptions()
    53  	return NewOptions(iopts).
    54  		SetHeartbeatTimeout(2 * time.Second).
    55  		SetWorkingDirectory(workingDir).
    56  		SetExecGenFn(func(p string, c string) (string, []string) {
    57  			return p, nil
    58  		})
    59  }
    60  
    61  type mockHeartbeatServer struct {
    62  	sync.Mutex
    63  	beats []hb.HeartbeatRequest
    64  }
    65  
    66  func (mh *mockHeartbeatServer) Heartbeat(c context.Context, h *hb.HeartbeatRequest) (*hb.HeartbeatResponse, error) {
    67  	mh.Lock()
    68  	defer mh.Unlock()
    69  	mh.beats = append(mh.beats, *h)
    70  	return &hb.HeartbeatResponse{}, nil
    71  }
    72  
    73  func (mh *mockHeartbeatServer) heartbeats() []hb.HeartbeatRequest {
    74  	mh.Lock()
    75  	defer mh.Unlock()
    76  	beats := make([]hb.HeartbeatRequest, 0, len(mh.beats))
    77  	beats = append(beats, mh.beats...)
    78  	return beats
    79  }
    80  
    81  func TestAgentClose(t *testing.T) {
    82  	ctrl := gomock.NewController(t)
    83  	defer ctrl.Finish()
    84  
    85  	tempDir := newTempDir(t)
    86  	defer os.RemoveAll(tempDir)
    87  
    88  	opts := newTestOptions(tempDir)
    89  	testAgent, err := New(opts)
    90  	require.NoError(t, err)
    91  	rawAgent, ok := testAgent.(*opAgent)
    92  	require.True(t, ok)
    93  
    94  	require.NoError(t, rawAgent.Close())
    95  }
    96  
    97  func TestProgramCrashNotify(t *testing.T) {
    98  	ctrl := gomock.NewController(t)
    99  	defer ctrl.Finish()
   100  
   101  	tempDir := newTempDir(t)
   102  	defer os.RemoveAll(tempDir)
   103  
   104  	hbService := &mockHeartbeatServer{}
   105  	hbListener, err := net.Listen("tcp", "127.0.0.1:0")
   106  	hbServer := xgrpc.NewServer(nil)
   107  	hb.RegisterHeartbeaterServer(hbServer, hbService)
   108  	require.NoError(t, err)
   109  	go hbServer.Serve(hbListener)
   110  	defer hbServer.Stop()
   111  
   112  	opts := newTestOptions(tempDir)
   113  	testAgent, err := New(opts)
   114  	require.NoError(t, err)
   115  	rawAgent, ok := testAgent.(*opAgent)
   116  	require.True(t, ok)
   117  
   118  	var testListener exec.ProcessListener
   119  	pm := mockexec.NewMockProcessMonitor(ctrl)
   120  	rawAgent.newProcessMonitorFn = func(c exec.Cmd, l exec.ProcessListener) (exec.ProcessMonitor, error) {
   121  		testListener = l
   122  		return pm, nil
   123  	}
   124  
   125  	setupResp, err := rawAgent.Setup(context.Background(), &m3em.SetupRequest{
   126  		SessionToken:           "abc",
   127  		HeartbeatEnabled:       true,
   128  		HeartbeatFrequencySecs: 1,
   129  		HeartbeatEndpoint:      hbListener.Addr().String(),
   130  	})
   131  	require.NoError(t, err)
   132  	require.NotNil(t, setupResp)
   133  
   134  	rawAgent.executablePath = "someString"
   135  	rawAgent.configPath = "otherString"
   136  
   137  	pm.EXPECT().Start().Return(nil)
   138  	startResp, err := rawAgent.Start(context.Background(), &m3em.StartRequest{})
   139  	require.NoError(t, err)
   140  	require.NotNil(t, startResp)
   141  	time.Sleep(time.Second)
   142  
   143  	pm.EXPECT().Stop().Do(func() {
   144  		testListener.OnComplete()
   145  	}).Return(nil)
   146  	stopResp, err := rawAgent.Stop(context.Background(), &m3em.StopRequest{})
   147  	require.NoError(t, err)
   148  	require.NotNil(t, stopResp)
   149  
   150  	// ensure no termination message received in hb server
   151  	time.Sleep(time.Second)
   152  	beats := hbService.heartbeats()
   153  	require.NotEmpty(t, beats)
   154  	for _, beat := range beats {
   155  		if beat.Code == hb.HeartbeatCode_PROCESS_TERMINATION {
   156  			require.Fail(t, "received unexpected heartbeat message")
   157  		}
   158  	}
   159  }
   160  
   161  func TestTooManyFailedHeartbeatsUnsetup(t *testing.T) {
   162  	tempDir := newTempDir(t)
   163  	defer os.RemoveAll(tempDir)
   164  
   165  	opts := newTestOptions(tempDir)
   166  	testAgent, err := New(opts)
   167  	require.NoError(t, err)
   168  	rawAgent, ok := testAgent.(*opAgent)
   169  	require.True(t, ok)
   170  
   171  	setupResp, err := rawAgent.Setup(context.Background(), &m3em.SetupRequest{
   172  		SessionToken:           "some-token",
   173  		HeartbeatEnabled:       true,
   174  		HeartbeatFrequencySecs: 1,
   175  		HeartbeatEndpoint:      "badaddress.com:80",
   176  	})
   177  	require.NoError(t, err)
   178  	require.NotNil(t, setupResp)
   179  
   180  	// ensure agent has reset itself after timeout
   181  	time.Sleep(opts.HeartbeatTimeout() * 10)
   182  	require.False(t, rawAgent.isSetup())
   183  }
   184  
   185  func TestTooManyFailedHeartbeatsStop(t *testing.T) {
   186  	ctrl := gomock.NewController(t)
   187  	defer ctrl.Finish()
   188  
   189  	tempDir := newTempDir(t)
   190  	defer os.RemoveAll(tempDir)
   191  
   192  	opts := newTestOptions(tempDir)
   193  	testAgent, err := New(opts)
   194  	require.NoError(t, err)
   195  	rawAgent, ok := testAgent.(*opAgent)
   196  	require.True(t, ok)
   197  
   198  	pm := mockexec.NewMockProcessMonitor(ctrl)
   199  	rawAgent.newProcessMonitorFn = func(c exec.Cmd, l exec.ProcessListener) (exec.ProcessMonitor, error) {
   200  		return pm, nil
   201  	}
   202  
   203  	setupResp, err := rawAgent.Setup(context.Background(), &m3em.SetupRequest{
   204  		SessionToken:           "abc",
   205  		HeartbeatEnabled:       true,
   206  		HeartbeatFrequencySecs: 1,
   207  		HeartbeatEndpoint:      "baddaddress.com:80",
   208  	})
   209  	require.NoError(t, err)
   210  	require.NotNil(t, setupResp)
   211  
   212  	rawAgent.executablePath = "someString"
   213  	rawAgent.configPath = "otherString"
   214  
   215  	gomock.InOrder(
   216  		pm.EXPECT().Start().Return(nil),
   217  		pm.EXPECT().Stop().Return(nil),
   218  	)
   219  
   220  	startResp, err := rawAgent.Start(context.Background(), &m3em.StartRequest{})
   221  	require.NoError(t, err)
   222  	require.NotNil(t, startResp)
   223  	time.Sleep(time.Second)
   224  
   225  	// ensure agent has reset itself after timeout
   226  	time.Sleep(2 * opts.HeartbeatTimeout())
   227  	require.False(t, rawAgent.isSetup())
   228  }
   229  
   230  func TestSetupOverrite(t *testing.T) {
   231  	ctrl := gomock.NewController(t)
   232  	defer ctrl.Finish()
   233  
   234  	tempDir := newTempDir(t)
   235  	defer os.RemoveAll(tempDir)
   236  
   237  	hbService := &mockHeartbeatServer{}
   238  	hbListener, err := net.Listen("tcp", "127.0.0.1:0")
   239  	require.NoError(t, err)
   240  	hbServer := xgrpc.NewServer(nil)
   241  	hb.RegisterHeartbeaterServer(hbServer, hbService)
   242  	go hbServer.Serve(hbListener)
   243  	defer hbServer.Stop()
   244  
   245  	opts := newTestOptions(tempDir)
   246  	testAgent, err := New(opts)
   247  	require.NoError(t, err)
   248  	rawAgent, ok := testAgent.(*opAgent)
   249  	require.True(t, ok)
   250  
   251  	setupResp, err := rawAgent.Setup(context.Background(), &m3em.SetupRequest{
   252  		SessionToken:           "abc",
   253  		HeartbeatEnabled:       true,
   254  		HeartbeatFrequencySecs: 1,
   255  		HeartbeatEndpoint:      hbListener.Addr().String(),
   256  	})
   257  	require.NoError(t, err)
   258  	require.NotNil(t, setupResp)
   259  
   260  	// ensure heartbeating has started
   261  	time.Sleep(time.Second)
   262  	beats := hbService.heartbeats()
   263  	require.NotEmpty(t, beats)
   264  
   265  	// make new heartbeatServer
   266  	newHbService := &mockHeartbeatServer{}
   267  	newHbListener, err := net.Listen("tcp", "127.0.0.1:0")
   268  	require.NoError(t, err)
   269  	newHbServer := xgrpc.NewServer(nil)
   270  	hb.RegisterHeartbeaterServer(newHbServer, newHbService)
   271  	go newHbServer.Serve(newHbListener)
   272  	defer newHbServer.Stop()
   273  
   274  	// ask agent to send messages to new hb server
   275  	setupResp, err = rawAgent.Setup(context.Background(), &m3em.SetupRequest{
   276  		SessionToken:           "other",
   277  		Force:                  true,
   278  		HeartbeatEnabled:       true,
   279  		HeartbeatFrequencySecs: 1,
   280  		HeartbeatEndpoint:      newHbListener.Addr().String(),
   281  	})
   282  	require.NoError(t, err)
   283  	require.NotNil(t, setupResp)
   284  
   285  	// old hb service should receive no more messages
   286  	oldHBBeatsT0 := hbService.heartbeats()
   287  
   288  	// ensure new heartbeating has started
   289  	time.Sleep(time.Second)
   290  	newHBBeats := newHbService.heartbeats()
   291  	require.NotEmpty(t, newHBBeats)
   292  
   293  	// old hb service should not have received any more messages
   294  	oldHBBeatsT1 := hbService.heartbeats()
   295  	require.Equal(t, oldHBBeatsT0, oldHBBeatsT1)
   296  }
   297  
   298  func TestClientReconnect(t *testing.T) {
   299  	t.Skipf("TODO(prateek): investigte flaky test: TestClientReconnect")
   300  
   301  	ctrl := gomock.NewController(t)
   302  	defer ctrl.Finish()
   303  
   304  	tempDir := newTempDir(t)
   305  	defer os.RemoveAll(tempDir)
   306  
   307  	hbService := &mockHeartbeatServer{}
   308  	hbListener, err := net.Listen("tcp", "127.0.0.1:0")
   309  	require.NoError(t, err)
   310  	// holding onto address as we'll need to open the listener again
   311  	listenAddress := hbListener.Addr().String()
   312  
   313  	hbServer := xgrpc.NewServer(nil)
   314  	hb.RegisterHeartbeaterServer(hbServer, hbService)
   315  	go hbServer.Serve(hbListener)
   316  
   317  	opts := newTestOptions(tempDir)
   318  	testAgent, err := New(opts)
   319  	require.NoError(t, err)
   320  	rawAgent, ok := testAgent.(*opAgent)
   321  	require.True(t, ok)
   322  
   323  	setupResp, err := rawAgent.Setup(context.Background(), &m3em.SetupRequest{
   324  		SessionToken:           "abc",
   325  		HeartbeatEnabled:       true,
   326  		HeartbeatFrequencySecs: 1,
   327  		HeartbeatEndpoint:      listenAddress,
   328  	})
   329  	require.NoError(t, err)
   330  	require.NotNil(t, setupResp)
   331  
   332  	// ensure heartbeating has started
   333  	foundHeartbeats := m3xclock.WaitUntil(func() bool {
   334  		return len(hbService.heartbeats()) > 0
   335  	}, 5*time.Second)
   336  	require.True(t, foundHeartbeats)
   337  
   338  	// crash heartbeat server
   339  	hbServer.Stop()
   340  
   341  	// re-start heartbeat server after a second
   342  	time.Sleep(time.Second)
   343  	hbService = &mockHeartbeatServer{}
   344  	hbListener, err = net.Listen("tcp", listenAddress)
   345  	require.NoError(t, err)
   346  	hbServer = xgrpc.NewServer(nil)
   347  	hb.RegisterHeartbeaterServer(hbServer, hbService)
   348  	go hbServer.Serve(hbListener)
   349  	defer hbServer.Stop()
   350  
   351  	// ensure heartbeating has restarted
   352  	foundHeartbeats = m3xclock.WaitUntil(func() bool {
   353  		return len(hbService.heartbeats()) > 0
   354  	}, 5*time.Second)
   355  	require.True(t, foundHeartbeats)
   356  }
   357  
   358  func TestPullFile(t *testing.T) {
   359  	ctrl := gomock.NewController(t)
   360  	defer ctrl.Finish()
   361  
   362  	tempDir := newTempDir(t)
   363  	defer os.RemoveAll(tempDir)
   364  
   365  	opts := newTestOptions(tempDir)
   366  	testAgent, err := New(opts)
   367  	require.NoError(t, err)
   368  	opAgent, ok := testAgent.(*opAgent)
   369  	require.True(t, ok)
   370  
   371  	var (
   372  		testStdoutPath = fmt.Sprintf("%s/some-path", tempDir)
   373  		testChunkSize  = 5
   374  		testMaxSize    = 10
   375  		testBytes      []byte
   376  		expectedBytes  []byte
   377  	)
   378  
   379  	// create testBytes 2x allowable amount
   380  	for i := 0; i < testMaxSize; i++ {
   381  		testBytes = append(testBytes, byte('a'))
   382  	}
   383  	for i := 0; i < testMaxSize; i++ {
   384  		testBytes = append(testBytes, byte('b'))
   385  		expectedBytes = append(expectedBytes, byte('b'))
   386  	}
   387  
   388  	// create file with testBytes contents
   389  	require.NoError(t, ioutil.WriteFile(testStdoutPath, testBytes, os.FileMode(0666)))
   390  	pm := mockexec.NewMockProcessMonitor(ctrl)
   391  	pullServer := m3em.NewMockOperator_PullFileServer(ctrl)
   392  
   393  	pm.EXPECT().StdoutPath().Return(testStdoutPath)
   394  	gomock.InOrder(
   395  		pullServer.EXPECT().Send(&m3em.PullFileResponse{
   396  			Data: &m3em.DataChunk{
   397  				Bytes: expectedBytes[testChunkSize:],
   398  				Idx:   1,
   399  			},
   400  			Truncated: true,
   401  		}).Return(nil),
   402  		pullServer.EXPECT().Send(&m3em.PullFileResponse{
   403  			Data: &m3em.DataChunk{
   404  				Bytes: expectedBytes[testChunkSize:],
   405  				Idx:   2,
   406  			},
   407  			Truncated: true,
   408  		}).Return(nil),
   409  	)
   410  
   411  	opAgent.processMonitor = pm
   412  	opAgent.token = "setup-token"
   413  	require.Nil(t, opAgent.PullFile(&m3em.PullFileRequest{
   414  		ChunkSize: int64(testChunkSize),
   415  		MaxSize:   int64(testMaxSize),
   416  		FileType:  m3em.PullFileType_PULL_FILE_TYPE_SERVICE_STDOUT,
   417  	}, pullServer))
   418  }