github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/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 }