/* * * Copyright 2016 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.netflix.genie.web.tasks.job; import com.netflix.genie.common.dto.JobExecution; import com.netflix.genie.core.events.JobFinishedEvent; import com.netflix.genie.core.events.KillJobEvent; import com.netflix.genie.core.jobs.JobConstants; import com.netflix.genie.core.properties.JobsProperties; import com.netflix.genie.test.categories.UnitTest; import com.netflix.genie.web.tasks.GenieTaskScheduleType; import com.netflix.spectator.api.Counter; import com.netflix.spectator.api.Registry; import org.apache.commons.exec.CommandLine; import org.apache.commons.exec.ExecuteException; import org.apache.commons.exec.Executor; import org.apache.commons.lang3.SystemUtils; import org.hamcrest.Matchers; import org.junit.Assert; import org.junit.Assume; import org.junit.Before; import org.junit.Test; import org.junit.experimental.categories.Category; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.ApplicationEventMulticaster; import java.io.File; import java.io.IOException; import java.util.Calendar; import java.util.List; import java.util.UUID; /** * Unit tests for JobMonitor. * * @author tgianos * @since 3.0.0 */ @Category(UnitTest.class) public class JobMonitorUnitTests { private static final long DELAY = 180235L; private static final long MAX_STD_OUT_LENGTH = 108234203L; private static final long MAX_STD_ERR_LENGTH = 18023482L; private JobMonitor monitor; private JobExecution jobExecution; private Executor executor; private ApplicationEventPublisher publisher; private ApplicationEventMulticaster eventMulticaster; private Registry registry; private File stdOut; private File stdErr; private Counter successfulCheckRate; private Counter timeoutRate; private Counter finishedRate; private Counter unsuccessfulCheckRate; private Counter stdOutTooLarge; private Counter stdErrTooLarge; /** * Setup for the tests. */ @Before public void setup() { final Calendar tomorrow = Calendar.getInstance(JobConstants.UTC); tomorrow.add(Calendar.DAY_OF_YEAR, 1); this.jobExecution = new JobExecution.Builder(UUID.randomUUID().toString()) .withProcessId(3808) .withCheckDelay(DELAY) .withTimeout(tomorrow.getTime()) .withId(UUID.randomUUID().toString()) .build(); this.executor = Mockito.mock(Executor.class); this.publisher = Mockito.mock(ApplicationEventPublisher.class); this.eventMulticaster = Mockito.mock(ApplicationEventMulticaster.class); this.successfulCheckRate = Mockito.mock(Counter.class); this.timeoutRate = Mockito.mock(Counter.class); this.finishedRate = Mockito.mock(Counter.class); this.unsuccessfulCheckRate = Mockito.mock(Counter.class); this.stdOutTooLarge = Mockito.mock(Counter.class); this.stdErrTooLarge = Mockito.mock(Counter.class); this.registry = Mockito.mock(Registry.class); this.stdOut = Mockito.mock(File.class); this.stdErr = Mockito.mock(File.class); Mockito .when(this.registry.counter("genie.jobs.successfulStatusCheck.rate")) .thenReturn(this.successfulCheckRate); Mockito .when(this.registry.counter("genie.jobs.timeout.rate")) .thenReturn(this.timeoutRate); Mockito .when(this.registry.counter("genie.jobs.finished.rate")) .thenReturn(this.finishedRate); Mockito .when(this.registry.counter("genie.jobs.unsuccessfulStatusCheck.rate")) .thenReturn(this.unsuccessfulCheckRate); Mockito .when(this.registry.counter("genie.jobs.stdOutTooLarge.rate")) .thenReturn(this.stdOutTooLarge); Mockito .when(this.registry.counter("genie.jobs.stdErrTooLarge.rate")) .thenReturn(this.stdErrTooLarge); final JobsProperties outputMaxProperties = new JobsProperties(); outputMaxProperties.getMax().setStdOutSize(MAX_STD_OUT_LENGTH); outputMaxProperties.getMax().setStdErrSize(MAX_STD_ERR_LENGTH); this.monitor = new JobMonitor( this.jobExecution, this.stdOut, this.stdErr, this.executor, this.publisher, this.eventMulticaster, this.registry, outputMaxProperties ); } /** * This test should only run on windows machines and asserts that the system fails on Windows. */ @Test(expected = UnsupportedOperationException.class) public void cantRunOnWindows() { Assume.assumeTrue(SystemUtils.IS_OS_WINDOWS); this.monitor.run(); } /** * Make sure that a process whose std out file has grown too large will attempt to be killed. * * @throws IOException on error */ @Test public void canKillProcessOnTooLargeStdOut() throws IOException { Assume.assumeTrue(SystemUtils.IS_OS_UNIX); Mockito .when(this.executor.execute(Mockito.any(CommandLine.class))) .thenReturn(0) .thenReturn(0) .thenReturn(0); Mockito.when(this.stdOut.exists()).thenReturn(true); Mockito.when(this.stdOut.length()) .thenReturn(MAX_STD_OUT_LENGTH - 1) .thenReturn(MAX_STD_OUT_LENGTH) .thenReturn(MAX_STD_OUT_LENGTH + 1); Mockito.when(this.stdErr.exists()).thenReturn(false); for (int i = 0; i < 3; i++) { this.monitor.run(); } Mockito.verify(this.successfulCheckRate, Mockito.times(2)).increment(); Mockito.verify(this.stdOutTooLarge, Mockito.times(1)).increment(); Mockito.verify(this.publisher, Mockito.times(1)).publishEvent(Mockito.any(KillJobEvent.class)); } /** * Make sure that a process whose std err file has grown too large will attempt to be killed. * * @throws IOException on error */ @Test public void canKillProcessOnTooLargeStdErr() throws IOException { Assume.assumeTrue(SystemUtils.IS_OS_UNIX); Mockito .when(this.executor.execute(Mockito.any(CommandLine.class))) .thenReturn(0) .thenReturn(0) .thenReturn(0); Mockito.when(this.stdOut.exists()).thenReturn(false); Mockito.when(this.stdErr.exists()).thenReturn(true); Mockito.when(this.stdErr.length()) .thenReturn(MAX_STD_ERR_LENGTH - 1) .thenReturn(MAX_STD_ERR_LENGTH) .thenReturn(MAX_STD_ERR_LENGTH + 1); for (int i = 0; i < 3; i++) { this.monitor.run(); } Mockito.verify(this.successfulCheckRate, Mockito.times(2)).increment(); Mockito.verify(this.stdErrTooLarge, Mockito.times(1)).increment(); Mockito.verify(this.publisher, Mockito.times(1)).publishEvent(Mockito.any(KillJobEvent.class)); } /** * Make sure that a running process doesn't publish anything. * * @throws IOException on error */ @Test public void canCheckRunningProcessOnUnixLikeSystem() throws IOException { Assume.assumeTrue(SystemUtils.IS_OS_UNIX); Mockito .when(this.executor.execute(Mockito.any(CommandLine.class))) .thenReturn(0) .thenThrow(new IOException()) .thenReturn(0); Mockito.when(this.stdOut.exists()).thenReturn(false); Mockito.when(this.stdErr.exists()).thenReturn(false); for (int i = 0; i < 3; i++) { this.monitor.run(); } Mockito.verify(this.successfulCheckRate, Mockito.times(2)).increment(); Mockito.verify(this.publisher, Mockito.never()).publishEvent(Mockito.any(ApplicationEvent.class)); Mockito.verify(this.eventMulticaster, Mockito.never()).multicastEvent(Mockito.any(ApplicationEvent.class)); Mockito.verify(this.unsuccessfulCheckRate, Mockito.times(1)).increment(); } /** * Make sure that a finished process sends event. * * @throws IOException on error */ @Test public void canCheckFinishedProcessOnUnixLikeSystem() throws IOException { Assume.assumeTrue(SystemUtils.IS_OS_UNIX); Mockito.when(this.executor.execute(Mockito.any(CommandLine.class))).thenThrow(new ExecuteException("done", 1)); this.monitor.run(); final ArgumentCaptor<JobFinishedEvent> captor = ArgumentCaptor.forClass(JobFinishedEvent.class); Mockito .verify(this.eventMulticaster, Mockito.times(1)) .multicastEvent(captor.capture()); Assert.assertNotNull(captor.getValue()); final String jobId = this.jobExecution.getId().orElseThrow(IllegalArgumentException::new); Assert.assertThat( captor.getValue().getId(), Matchers.is(jobId) ); Assert.assertThat(captor.getValue().getSource(), Matchers.is(this.monitor)); Mockito.verify(this.finishedRate, Mockito.times(1)).increment(); } /** * Make sure that a timed out process sends event. * * @throws IOException on error */ @Test public void canTryToKillTimedOutProcess() throws IOException { Assume.assumeTrue(SystemUtils.IS_OS_UNIX); // Set timeout to yesterday to force timeout when check happens final Calendar yesterday = Calendar.getInstance(JobConstants.UTC); yesterday.add(Calendar.DAY_OF_YEAR, -1); this.jobExecution = new JobExecution.Builder(UUID.randomUUID().toString()) .withProcessId(3808) .withCheckDelay(DELAY) .withTimeout(yesterday.getTime()) .withId(UUID.randomUUID().toString()) .build(); this.monitor = new JobMonitor( this.jobExecution, this.stdOut, this.stdErr, this.executor, this.publisher, this.eventMulticaster, this.registry, new JobsProperties() ); this.monitor.run(); final ArgumentCaptor<KillJobEvent> captor = ArgumentCaptor.forClass(KillJobEvent.class); Mockito .verify(this.publisher, Mockito.times(1)) .publishEvent(captor.capture()); Assert.assertNotNull(captor.getValue()); final String jobId = this.jobExecution.getId().orElseThrow(IllegalArgumentException::new); Assert.assertThat( captor.getValue().getId(), Matchers.is(jobId) ); Assert.assertThat(captor.getValue().getReason(), Matchers.is("Job exceeded timeout")); Assert.assertThat(captor.getValue().getSource(), Matchers.is(this.monitor)); Mockito.verify(this.timeoutRate, Mockito.times(1)).increment(); } /** * Make sure that an error doesn't publish anything until it runs too many times then it tries to kill the job. * * @throws IOException on error */ @Test public void cantGetStatusIfErrorOnUnixLikeSystem() throws IOException { Assume.assumeTrue(SystemUtils.IS_OS_UNIX); Mockito.when(this.executor.execute(Mockito.any(CommandLine.class))).thenThrow(new IOException()); // Run six times to force error for (int i = 0; i < 6; i++) { this.monitor.run(); } final ArgumentCaptor<KillJobEvent> eventCaptor = ArgumentCaptor.forClass(KillJobEvent.class); Mockito.verify(this.publisher, Mockito.times(1)).publishEvent(eventCaptor.capture()); final List<KillJobEvent> events = eventCaptor.getAllValues(); Assert.assertThat(events.size(), Matchers.is(1)); final String jobId = this.jobExecution.getId().orElseThrow(IllegalArgumentException::new); Assert.assertThat( events.get(0).getId(), Matchers.is(jobId) ); Assert.assertThat(events.get(0).getSource(), Matchers.is(this.monitor)); final ArgumentCaptor<JobFinishedEvent> finishedCaptor = ArgumentCaptor.forClass(JobFinishedEvent.class); Mockito.verify(this.eventMulticaster, Mockito.times(1)).multicastEvent(finishedCaptor.capture()); Assert.assertThat( finishedCaptor.getValue().getId(), Matchers.is(jobId) ); Assert.assertThat(finishedCaptor.getValue().getSource(), Matchers.is(this.monitor)); Mockito.verify(this.unsuccessfulCheckRate, Mockito.times(6)).increment(); } /** * Make sure the right schedule type is returned. */ @Test public void canGetScheduleType() { Assert.assertThat(this.monitor.getScheduleType(), Matchers.is(GenieTaskScheduleType.FIXED_DELAY)); } /** * Make sure asking for a trigger isn't allowed. */ @Test(expected = UnsupportedOperationException.class) public void cantGetTrigger() { this.monitor.getTrigger(); } /** * Make sure asking for a trigger isn't allowed. */ @Test(expected = UnsupportedOperationException.class) public void cantGetFixedRate() { this.monitor.getFixedRate(); } /** * Make sure the fixed delay value is what we expect. */ @Test public void canGetFixedDelay() { Assert.assertThat(DELAY, Matchers.is(this.monitor.getFixedDelay())); } }