package com.linkedin.thirdeye.integration; import com.google.common.cache.LoadingCache; import com.linkedin.pinot.client.ResultSet; import com.linkedin.pinot.client.ResultSetGroup; import com.linkedin.thirdeye.anomaly.ThirdEyeAnomalyConfiguration; import com.linkedin.thirdeye.anomaly.alert.AlertJobScheduler; import com.linkedin.thirdeye.anomaly.detection.DetectionJobScheduler; import com.linkedin.thirdeye.anomaly.grouping.GroupingJobScheduler; import com.linkedin.thirdeye.anomaly.job.JobConstants.JobStatus; import com.linkedin.thirdeye.anomaly.monitor.MonitorConfiguration; import com.linkedin.thirdeye.anomaly.monitor.MonitorJobScheduler; import com.linkedin.thirdeye.anomaly.task.TaskConstants.TaskStatus; import com.linkedin.thirdeye.anomaly.task.TaskConstants.TaskType; import com.linkedin.thirdeye.anomaly.task.TaskDriver; import com.linkedin.thirdeye.anomaly.task.TaskDriverConfiguration; import com.linkedin.thirdeye.anomaly.task.TaskInfoFactory; import com.linkedin.thirdeye.api.TimeGranularity; import com.linkedin.thirdeye.api.TimeSpec; import com.linkedin.thirdeye.client.ThirdEyeCacheRegistry; import com.linkedin.thirdeye.client.ThirdEyeClient; import com.linkedin.thirdeye.client.ThirdEyeRequest; import com.linkedin.thirdeye.client.ThirdEyeResponse; import com.linkedin.thirdeye.client.cache.MetricDataset; import com.linkedin.thirdeye.client.cache.QueryCache; import com.linkedin.thirdeye.client.pinot.PinotQuery; import com.linkedin.thirdeye.client.pinot.PinotThirdEyeResponse; import com.linkedin.thirdeye.completeness.checker.DataCompletenessConstants.DataCompletenessType; import com.linkedin.thirdeye.completeness.checker.DataCompletenessScheduler; import com.linkedin.thirdeye.completeness.checker.DataCompletenessTaskInfo; import com.linkedin.thirdeye.datalayer.bao.AbstractManagerTestBase; import com.linkedin.thirdeye.datalayer.dto.DatasetConfigDTO; import com.linkedin.thirdeye.datalayer.dto.JobDTO; import com.linkedin.thirdeye.datalayer.dto.MergedAnomalyResultDTO; import com.linkedin.thirdeye.datalayer.dto.MetricConfigDTO; import com.linkedin.thirdeye.datalayer.dto.RawAnomalyResultDTO; import com.linkedin.thirdeye.datalayer.dto.TaskDTO; import com.linkedin.thirdeye.detector.email.filter.AlertFilterFactory; import com.linkedin.thirdeye.detector.function.AnomalyFunctionFactory; import com.linkedin.thirdeye.util.ThirdEyeUtils; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.Random; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.joda.time.DateTime; import org.mockito.Matchers; import org.mockito.Mockito; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.quartz.SchedulerException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.testng.Assert; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; public class AnomalyApplicationEndToEndTest extends AbstractManagerTestBase { private static final Logger LOG = LoggerFactory.getLogger(AnomalyApplicationEndToEndTest.class); private DetectionJobScheduler detectionJobScheduler = null; private TaskDriver taskDriver = null; private MonitorJobScheduler monitorJobScheduler = null; private AlertJobScheduler alertJobScheduler = null; private DataCompletenessScheduler dataCompletenessScheduler = null; private GroupingJobScheduler groupingJobScheduler = null; private AnomalyFunctionFactory anomalyFunctionFactory = null; private AlertFilterFactory alertFilterFactory = null; private ThirdEyeCacheRegistry cacheRegistry = ThirdEyeCacheRegistry.getInstance(); private ThirdEyeAnomalyConfiguration thirdeyeAnomalyConfig; private List<TaskDTO> tasks; private List<JobDTO> jobs; private long functionId; private int id = 0; private String dashboardHost = "http://localhost:8080/dashboard"; private String functionPropertiesFile = "/sample-functions.properties"; private String alertFilterPropertiesFile = "/sample-alertfilter.properties"; private String metric = "cost"; private String collection = "test-collection"; @BeforeClass void beforeClass() { super.init(); Assert.assertNotNull(daoRegistry.getJobDAO()); } @AfterClass(alwaysRun = true) void afterClass() throws Exception { cleanup_schedulers(); super.cleanup(); } private void cleanup_schedulers() throws SchedulerException { if (detectionJobScheduler != null) { detectionJobScheduler.shutdown(); } if (alertJobScheduler != null) { alertJobScheduler.shutdown(); } if (monitorJobScheduler != null) { monitorJobScheduler.shutdown(); } if (taskDriver != null) { taskDriver.shutdown(); } if (dataCompletenessScheduler != null) { dataCompletenessScheduler.shutdown(); } if (groupingJobScheduler != null) { groupingJobScheduler.shutdown(); } } private void setup() throws Exception { // Mock query cache ThirdEyeClient mockThirdeyeClient = Mockito.mock(ThirdEyeClient.class); Mockito.when(mockThirdeyeClient.execute(Matchers.any(ThirdEyeRequest.class))) .thenAnswer(new Answer<ThirdEyeResponse>() { @Override public ThirdEyeResponse answer(InvocationOnMock invocation) throws Throwable { Object[] args = invocation.getArguments(); ThirdEyeRequest request = (ThirdEyeRequest) args[0]; ThirdEyeResponse response = getMockResponse(request); return response; } }); QueryCache mockQueryCache = new QueryCache(mockThirdeyeClient, Executors.newFixedThreadPool(10)); cacheRegistry.registerQueryCache(mockQueryCache); MetricConfigDTO metricConfig = getTestMetricConfig(collection, metric, 1L); // create metric config in cache LoadingCache<MetricDataset, MetricConfigDTO> mockMetricConfigCache = Mockito.mock(LoadingCache.class); Mockito.when(mockMetricConfigCache.get(new MetricDataset(metric, collection))).thenReturn(metricConfig); cacheRegistry.registerMetricConfigCache(mockMetricConfigCache); // create dataset config in cache LoadingCache<String, DatasetConfigDTO> mockDatasetConfigCache = Mockito.mock(LoadingCache.class); Mockito.when(mockDatasetConfigCache.get(collection)).thenReturn(getTestDatasetConfig(collection)); cacheRegistry.registerDatasetConfigCache(mockDatasetConfigCache); ResultSet mockResultSet = Mockito.mock(ResultSet.class); Mockito.when(mockResultSet.getRowCount()).thenReturn(0); final ResultSetGroup mockResultSetGroup = Mockito.mock(ResultSetGroup.class); Mockito.when(mockResultSetGroup.getResultSet(0)).thenReturn(mockResultSet); LoadingCache<PinotQuery, ResultSetGroup> mockResultSetGroupCache = Mockito.mock(LoadingCache.class); Mockito.when(mockResultSetGroupCache.get(Matchers.any(PinotQuery.class))) .thenAnswer(new Answer<ResultSetGroup>() { @Override public ResultSetGroup answer(InvocationOnMock invocation) throws Throwable { return mockResultSetGroup; } }); cacheRegistry.registerResultSetGroupCache(mockResultSetGroupCache); // Application config thirdeyeAnomalyConfig = new ThirdEyeAnomalyConfiguration(); thirdeyeAnomalyConfig.setId(id); thirdeyeAnomalyConfig.setDashboardHost(dashboardHost); MonitorConfiguration monitorConfiguration = new MonitorConfiguration(); monitorConfiguration.setMonitorFrequency(new TimeGranularity(3, TimeUnit.SECONDS)); thirdeyeAnomalyConfig.setMonitorConfiguration(monitorConfiguration); TaskDriverConfiguration taskDriverConfiguration = new TaskDriverConfiguration(); taskDriverConfiguration.setNoTaskDelayInMillis(1000); taskDriverConfiguration.setRandomDelayCapInMillis(200); taskDriverConfiguration.setTaskFailureDelayInMillis(500); taskDriverConfiguration.setMaxParallelTasks(2); thirdeyeAnomalyConfig.setTaskDriverConfiguration(taskDriverConfiguration); thirdeyeAnomalyConfig.setRootDir(System.getProperty("dw.rootDir", "NOT_SET(dw.rootDir)")); // create test anomaly function functionId = anomalyFunctionDAO.save(getTestFunctionSpec(metric, collection)); // create test email configuration emailConfigurationDAO.save(getTestEmailConfiguration(metric, collection)); // create test alert configuration alertConfigDAO.save(getTestAlertConfiguration("test alert v2")); // create test dataset config datasetConfigDAO.save(getTestDatasetConfig(collection)); // create test grouping config classificationConfigDAO.save(getTestGroupingConfiguration(functionId)); // setup function factory for worker and merger InputStream factoryStream = AnomalyApplicationEndToEndTest.class.getResourceAsStream(functionPropertiesFile); anomalyFunctionFactory = new AnomalyFunctionFactory(factoryStream); // setup alertfilter factory for worker InputStream alertFilterStream = AnomalyApplicationEndToEndTest.class.getResourceAsStream(alertFilterPropertiesFile); alertFilterFactory = new AlertFilterFactory(alertFilterStream); } private ThirdEyeResponse getMockResponse(ThirdEyeRequest request) { ThirdEyeResponse response = null; Random rand = new Random(); DatasetConfigDTO datasetConfig = datasetConfigDAO.findByDataset(collection); TimeSpec dataTimeSpec = ThirdEyeUtils.getTimeSpecFromDatasetConfig(datasetConfig); List<String[]> rows = new ArrayList<>(); DateTime start = request.getStartTimeInclusive(); DateTime end = request.getEndTimeExclusive(); List<String> metrics = request.getMetricNames(); int bucket = 0; while (start.isBefore(end)) { String[] row = new String[metrics.size() + 1]; row[0] = String.valueOf(bucket); bucket++; for (int i = 0; i < metrics.size(); i++) { row[i+1] = String.valueOf(rand.nextInt(1000)); } rows.add(row); start = start.plusHours(1); } response = new PinotThirdEyeResponse(request, rows, dataTimeSpec); return response; } @Test(enabled=true) public void testThirdeyeAnomalyApplication() throws Exception { Assert.assertNotNull(daoRegistry.getJobDAO()); // setup caches and config setup(); Assert.assertNotNull(daoRegistry.getJobDAO()); // startDataCompletenessChecker startDataCompletenessScheduler(); Thread.sleep(10000); int jobSizeDataCompleteness = jobDAO.findAll().size(); int taskSizeDataCompleteness = taskDAO.findAll().size(); Assert.assertTrue(jobSizeDataCompleteness == 1); Assert.assertTrue(taskSizeDataCompleteness == 2); JobDTO jobDTO = jobDAO.findAll().get(0); Assert.assertTrue(jobDTO.getJobName().startsWith(TaskType.DATA_COMPLETENESS.toString())); List<TaskDTO> taskDTOs = taskDAO.findAll(); for (TaskDTO taskDTO : taskDTOs) { Assert.assertEquals(taskDTO.getTaskType(), TaskType.DATA_COMPLETENESS); Assert.assertEquals(taskDTO.getStatus(), TaskStatus.WAITING); DataCompletenessTaskInfo taskInfo = (DataCompletenessTaskInfo) TaskInfoFactory. getTaskInfoFromTaskType(taskDTO.getTaskType(), taskDTO.getTaskInfo()); Assert.assertTrue((taskInfo.getDataCompletenessType() == DataCompletenessType.CHECKER) || (taskInfo.getDataCompletenessType() == DataCompletenessType.CLEANUP)); } // start detection scheduler startDetectionScheduler(); // start alert scheduler startAlertScheduler(); // check for number of entries in tasks and jobs Thread.sleep(10000); int jobSize1 = jobDAO.findAll().size(); int taskSize1 = taskDAO.findAll().size(); Assert.assertTrue(jobSize1 > 0); Assert.assertTrue(taskSize1 > 0); Thread.sleep(10000); int jobSize2 = jobDAO.findAll().size(); int taskSize2 = taskDAO.findAll().size(); Assert.assertTrue(jobSize2 > jobSize1); Assert.assertTrue(taskSize2 > taskSize1); tasks = taskDAO.findAll(); // check for task type int detectionCount = 0; int alertCount = 0; for (TaskDTO task : tasks) { if (task.getTaskType().equals(TaskType.ANOMALY_DETECTION)) { detectionCount ++; } else if (task.getTaskType().equals(TaskType.ALERT)) { alertCount ++; } } Assert.assertTrue(detectionCount > 0); Assert.assertTrue(alertCount > 0); // check for task status tasks = taskDAO.findAll(); for (TaskDTO task : tasks) { Assert.assertEquals(task.getStatus(), TaskStatus.WAITING); } // start monitor startMonitor(); // check for monitor tasks Thread.sleep(5000); tasks = taskDAO.findAll(); int monitorCount = 0; for (TaskDTO task : tasks) { if (task.getTaskType().equals(TaskType.MONITOR)) { monitorCount++; } } Assert.assertTrue(monitorCount > 0); // check for job status jobs = jobDAO.findAll(); for (JobDTO job : jobs) { Assert.assertEquals(job.getStatus(), JobStatus.SCHEDULED); } // start task drivers startWorker(); // check for change in task status to COMPLETED Thread.sleep(30000); tasks = taskDAO.findAll(); int completedCount = 0; for (TaskDTO task : tasks) { if (task.getStatus().equals(TaskStatus.COMPLETED)) { completedCount++; } } Assert.assertTrue(completedCount > 0); // Raw anomalies of the same function and dimensions should have been merged by the worker, so we // check if any raw anomalies present, whose existence means the worker fails the synchronous merge. List<RawAnomalyResultDTO> rawAnomalies = rawAnomalyResultDAO.findUnmergedByFunctionId(functionId); Assert.assertTrue(rawAnomalies.size() == 0); // check merged anomalies List<MergedAnomalyResultDTO> mergedAnomalies = mergedAnomalyResultDAO.findByFunctionId(functionId); Assert.assertTrue(mergedAnomalies.size() > 0); // THE FOLLOWING TEST MAY FAIL OCCASIONALLY DUE TO MACHINE COMPUTATION POWER // check for job status COMPLETED jobs = jobDAO.findAll(); int completedJobCount = 0; for (JobDTO job : jobs) { int attempt = 0; while (attempt < 3 && !job.getStatus().equals(JobStatus.COMPLETED)) { LOG.info("Checking job status with attempt : {}", attempt + 1); Thread.sleep(5_000); attempt++; } if (job.getStatus().equals(JobStatus.COMPLETED)) { completedJobCount++; break; } } Assert.assertTrue(completedJobCount > 0); // start grouper startGrouper(); JobDTO latestCompletedDetectionJobDTO = jobDAO.findLatestCompletedDetectionJobByFunctionId(functionId); Assert.assertNotNull(latestCompletedDetectionJobDTO); Thread.sleep(5000); jobs = jobDAO.findAll(); JobDTO latestCompletedGroupingJobDTO = jobDAO.findLatestCompletedGroupingJobById(functionId); Assert.assertNotNull(latestCompletedGroupingJobDTO); } private void startDataCompletenessScheduler() throws Exception { dataCompletenessScheduler = new DataCompletenessScheduler(); dataCompletenessScheduler.start(); } private void startGrouper() { groupingJobScheduler = new GroupingJobScheduler(); groupingJobScheduler.start(); } private void startMonitor() { monitorJobScheduler = new MonitorJobScheduler(thirdeyeAnomalyConfig.getMonitorConfiguration()); monitorJobScheduler.start(); } private void startWorker() throws Exception { taskDriver = new TaskDriver(thirdeyeAnomalyConfig, anomalyFunctionFactory, alertFilterFactory); taskDriver.start(); } private void startAlertScheduler() throws SchedulerException { alertJobScheduler = new AlertJobScheduler(); alertJobScheduler.start(); } private void startDetectionScheduler() throws SchedulerException { detectionJobScheduler = new DetectionJobScheduler(); detectionJobScheduler.start(); } }