/* * 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.addthis.hydra.job.alert; import java.net.ConnectException; import java.net.SocketTimeoutException; import java.util.List; import com.addthis.hydra.job.Job; import com.addthis.hydra.job.JobState; import com.addthis.hydra.job.JobTask; import com.addthis.hydra.job.JobTaskState; import com.addthis.hydra.job.spawn.Spawn; import com.addthis.maljson.JSONArray; import com.addthis.maljson.JSONObject; import com.google.common.collect.ImmutableMap; import org.junit.Test; import static com.addthis.codec.config.Configs.decodeObject; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; public class JobAlertTest { @Test public void basicTriggerTest() throws Exception { long now = System.currentTimeMillis(); Job idleJob = createJobWithState(JobState.IDLE); Job errorJob = createJobWithState(JobState.ERROR); Job runningJob = createJobWithState(JobState.RUNNING); AbstractJobAlert errorAlert = decodeObject(AbstractJobAlert.class, "alertId = errorAlert, type = 0, jobIds = []"); assertNotNull("Error alert should trigger with at least one error job", errorAlert.alertActiveForJob(null, errorJob, null)); assertNull("Error alert should not trigger with only idle job", errorAlert.alertActiveForJob(null, idleJob, null)); AbstractJobAlert completeAlert = decodeObject(AbstractJobAlert.class, "alertId = completeAlert, type = 1, jobIds = []"); assertNull("Complete alert should not trigger with running job", completeAlert.alertActiveForJob(null, runningJob, null)); runningJob.setState(JobState.IDLE); assertNotNull("Complete alert should trigger on job completion", completeAlert.alertActiveForJob(null, runningJob, null)); runningJob.setState(JobState.RUNNING); AbstractJobAlert runtimeAlert = decodeObject(AbstractJobAlert.class, "alertId = runtimeAlert, type = 2, timeout = 1 hour, jobIds = []"); assertNull("Runtime alert should not trigger with idle job", runtimeAlert.alertActiveForJob(null, idleJob, null)); runningJob.setStartTime(now - 1000); assertNull("Runtime alert should not trigger with recently-submitted job", runtimeAlert.alertActiveForJob(null, runningJob, null)); runningJob.setStartTime(now - 180 * 60 * 1000); assertNotNull("Runtime alert should trigger with long-running job", runtimeAlert.alertActiveForJob(null, runningJob, null)); AbstractJobAlert rekickAlert = decodeObject(AbstractJobAlert.class, "alertId = rekickAlert, type = 3, timeout = 1 hour, jobIds = []"); idleJob.setEndTime(now - 10 * 60 * 1000); assertNull("Rekick alert should not fire after short time period", rekickAlert.alertActiveForJob(null, idleJob, null)); idleJob.setEndTime(now - 300 * 60 * 1000); assertNotNull("Rekick alert should fire after long time period", rekickAlert.alertActiveForJob(null, idleJob, null)); } @Test public void jsonTest() throws Exception { AbstractJobAlert initialAlert = decodeObject(AbstractJobAlert.class, "alertId = sampleid, type = 0, email = \"someone@domain.com\", " + "description = this is a new alert, jobIds = [j1, j2], " + "webhookURL = \"http://example.com\""); JSONObject json = initialAlert.toJSON(); assertEquals(initialAlert.alertId, json.getString("alertId")); assertEquals(0, json.getInt("type")); assertEquals(initialAlert.email, json.getString("email")); assertEquals(initialAlert.webhookURL, json.getString("webhookURL")); assertEquals(initialAlert.description, json.getString("description")); assertEquals(new JSONArray(initialAlert.jobIds), json.getJSONArray("jobIds")); } private Job createJobWithState(JobState jobState) throws Exception { Job job = new Job(); job.setState(jobState); return job; } @Test public void constructingWebhookObject() throws Exception { Spawn mockSpawn = mock(Spawn.class); Job j1 = new Job("test_id"); j1.setState(JobState.ERROR); j1.setDescription("job desc"); j1.setStartTime(0L); j1.setEndTime(1L); JobTask task1 = new JobTask(); task1.setByteCount(5); task1.setFileCount(4); task1.setState(JobTaskState.ERROR, true); JobTask task2 = new JobTask(); task2.setByteCount(4); task2.setFileCount(6); task2.setState(JobTaskState.IDLE, true); JobTask task3 = new JobTask(); task2.setByteCount(4); task2.setFileCount(6); task2.setState(JobTaskState.BUSY, true); j1.addTask(task1); j1.addTask(task2); j1.addTask(task3); when(mockSpawn.getJob("test_id")).thenReturn(j1); AbstractJobAlert runtimeAlert = decodeObject(AbstractJobAlert.class, "alertId = a, type = 2, email = \"someone@domain.com\", " + "description = runtime alert, jobIds = [j1], timeout = 1000"); JobAlertRunner.AlertWebhookRequest obj = JobAlertRunner.getWebhookObject(mockSpawn, runtimeAlert, "http://localhost", "bad reason", ImmutableMap.of("test_id", "something horrible happened")); assertEquals("http://localhost", obj.getAlertLink()); assertEquals("Task runtime exceeded", obj.getAlertType()); assertEquals("bad reason", obj.getAlertReason()); assertEquals("runtime alert", obj.getAlertDescription()); List<JobAlertRunner.JobError> jobsInError = obj.getJobsInError(); assertEquals(1, jobsInError.size()); JobAlertRunner.JobError jobInfo = jobsInError.get(0); assertEquals("ERROR", jobInfo.getJobState().name()); assertEquals("job desc", jobInfo.getDescription()); assertEquals(0L, jobInfo.getStartTime()); assertEquals(1L, jobInfo.getEndTime()); assertEquals("test_id", jobInfo.getId()); assertEquals("something horrible happened", jobInfo.getError()); assertEquals("localhost", jobInfo.getClusterHead()); assertEquals(3, jobInfo.getNodeCount()); assertEquals(1, jobInfo.getErrorCount()); } /** Error message should stay unchanged on repeated scan of triggered runtime or rekick exceeded alert */ @Test public void noErrorChangeOnTriggeredTimeoutAlertRescan() throws Exception { AbstractJobAlert runtimeAlert = decodeObject(AbstractJobAlert.class, "alertId = a, type = 2, email = \"someone@domain.com\", " + "description = runtime alert, jobIds = [j1], timeout = 1000"); AbstractJobAlert rekickAlert = decodeObject(AbstractJobAlert.class, "alertId = b, type = 3, email = \"someone@domain.com\", " + "description = rekick alert, jobIds = [j1], timeout = 1000"); Job j1 = createJobWithState(JobState.RUNNING); j1.setStartTime(0L); j1.setEndTime(1L); String runtimeAlertMsg1 = runtimeAlert.alertActiveForJob(null, j1, null); String rekickAlertMsg1 = runtimeAlert.alertActiveForJob(null, j1, null); Thread.sleep(10); String runtimeAlertMsg2 = runtimeAlert.alertActiveForJob(null, j1, null); String rekickAlertMsg2 = runtimeAlert.alertActiveForJob(null, j1, null); assertEquals("runtime alert message unchanged", runtimeAlertMsg1, runtimeAlertMsg2); assertEquals("rekick alert message unchanged", rekickAlertMsg1, rekickAlertMsg2); } @Test public void conditionallyTriggerAlertOnCanaryException() throws Exception { AbstractJobAlert alert = decodeObject(AbstractJobAlert.class, "alertId = a, type = 5, description = canary alert, jobIds = []"); Exception definitelyBadException = new RuntimeException("error"); Exception normallyOkException = new RuntimeException(new SocketTimeoutException("socket timeout")); assertEquals("bad exception", definitelyBadException.toString(), alert.handleCanaryException(definitelyBadException, null)); for (int i = 1; i < AbstractJobAlert.MAX_CONSECUTIVE_CANARY_EXCEPTION; i++) { assertNull("benign exception #" + i, alert.handleCanaryException(normallyOkException, null)); } assertNotNull("benign exception (exceeds MAX_CONSECUTIVE_CANARY_EXCEPTION)", alert.handleCanaryException(normallyOkException, null)); assertNull("benign exception (consective ex counter reset)", alert.handleCanaryException(normallyOkException, null)); } @Test public void preserveExistingAlertOnCanaryException() throws Exception { AbstractJobAlert alert = decodeObject(AbstractJobAlert.class, "alertId = a, type = 5, description = canary alert, jobIds = []"); Exception definitelyBadException = new RuntimeException("error"); Exception normallyOkException = new RuntimeException(new SocketTimeoutException("socket timeout")); assertEquals("bad exception", definitelyBadException.toString(), alert.handleCanaryException(definitelyBadException, null)); assertEquals("benign exception #1", "some previous error", alert.handleCanaryException(normallyOkException, "some previous error")); assertNull("benign exception #2", alert.handleCanaryException(normallyOkException, null)); } @Test public void queryDownAlertOnCanaryException() throws Exception { AbstractJobAlert alert = decodeObject(AbstractJobAlert.class, "alertId = a, type = 5, description = canary alert, jobIds = []"); Exception e = new RuntimeException(new ConnectException()); assertEquals("alert error message should be unchanged on ConnectException", "previous error", alert.handleCanaryException(e, "previous error")); } }