/* * Copyright © 2015 Cask Data, 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 co.cask.cdap.internal.app.store; import co.cask.cdap.api.common.Bytes; import co.cask.cdap.api.dataset.lib.AbstractDataset; import co.cask.cdap.api.dataset.table.Row; import co.cask.cdap.api.dataset.table.Scan; import co.cask.cdap.api.dataset.table.Scanner; import co.cask.cdap.api.dataset.table.Table; import co.cask.cdap.common.app.RunIds; import co.cask.cdap.data2.dataset2.lib.table.MDSKey; import co.cask.cdap.proto.Id; import co.cask.cdap.proto.PercentileInformation; import co.cask.cdap.proto.ProgramType; import co.cask.cdap.proto.WorkflowStatistics; import com.google.common.base.Preconditions; import com.google.common.primitives.Longs; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; import org.apache.twill.api.RunId; import java.lang.reflect.Type; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import javax.annotation.Nullable; /** * Dataset for Completed Workflows and their associated programs */ public class WorkflowDataset extends AbstractDataset { private static final Gson GSON = new Gson(); private static final byte[] RUNID = Bytes.toBytes("r"); private static final byte[] TIME_TAKEN = Bytes.toBytes("t"); private static final byte[] NODES = Bytes.toBytes("n"); private static final Type PROGRAM_RUNS_TYPE = new TypeToken<List<ProgramRun>>() { }.getType(); private final Table table; WorkflowDataset(Table table) { super("workflow.statistics", table); this.table = table; } void write(Id.Workflow id, RunRecordMeta runRecordMeta, List<ProgramRun> programRunList) { long startTs = runRecordMeta.getStartTs(); MDSKey mdsKey = new MDSKey.Builder().add(id.getApplication().getNamespaceId()) .add(id.getApplicationId()).add(id.getId()).add(startTs).build(); byte[] rowKey = mdsKey.getKey(); Long stopTs = runRecordMeta.getStopTs(); Preconditions.checkState(stopTs != null, "Workflow Stats are written when the workflow has completed. Hence, " + "expected workflow stop time to be non-null. Workflow = %s, Run = %s, Stop time = %s", id, runRecordMeta, stopTs); long timeTaken = stopTs - startTs; String value = GSON.toJson(programRunList, PROGRAM_RUNS_TYPE); table.put(rowKey, RUNID, Bytes.toBytes(runRecordMeta.getPid())); table.put(rowKey, TIME_TAKEN, Bytes.toBytes(timeTaken)); table.put(rowKey, NODES, Bytes.toBytes(value)); } /** * This function scans the workflow.stats dataset for a list of workflow runs in a time range. * * @param id The workflow id * @param timeRangeStart Start of the time range that the scan should begin from * @param timeRangeEnd End of the time range that the scan should end at * @return List of WorkflowRunRecords */ private List<WorkflowRunRecord> scan(Id.Workflow id, long timeRangeStart, long timeRangeEnd) { byte[] startRowKey = new MDSKey.Builder().add(id.getApplication().getNamespaceId()).add(id.getApplicationId()). add(id.getId()).add(timeRangeStart).build().getKey(); byte[] endRowKey = new MDSKey.Builder().add(id.getApplication().getNamespaceId()).add(id.getApplicationId()). add(id.getId()).add(timeRangeEnd).build().getKey(); Scan scan = new Scan(startRowKey, endRowKey); Scanner scanner = table.scan(scan); Row indexRow; List<WorkflowRunRecord> workflowRunRecordList = new ArrayList<>(); while ((indexRow = scanner.next()) != null) { Map<byte[], byte[]> columns = indexRow.getColumns(); String workflowRunId = Bytes.toString(columns.get(RUNID)); long timeTaken = Bytes.toLong(columns.get(TIME_TAKEN)); List<ProgramRun> programRunList = GSON.fromJson(Bytes.toString(columns.get(NODES)), PROGRAM_RUNS_TYPE); WorkflowRunRecord workflowRunRecord = new WorkflowRunRecord(workflowRunId, timeTaken, programRunList); workflowRunRecordList.add(workflowRunRecord); } return workflowRunRecordList; } /** * This method returns the statistics for a corresponding workflow. The user has to * provide a time interval and a list of percentiles that are required. * * @param id The workflow id * @param startTime The start of the time range from where the user wants the statistics * @param endTime The end of the time range till where the user wants the statistics * @param percentiles The list of percentiles that the user wants information on * @return A statistics object that provides information about the workflow or null if there are no runs associated * with the workflow * @throws Exception */ @Nullable public WorkflowStatistics getStatistics(Id.Workflow id, long startTime, long endTime, List<Double> percentiles) throws Exception { List<WorkflowRunRecord> workflowRunRecords = scan(id, startTime, endTime); int runs = workflowRunRecords.size(); if (runs == 0) { return null; } double avgRunTime = 0.0; for (WorkflowDataset.WorkflowRunRecord workflowRunRecord : workflowRunRecords) { avgRunTime += workflowRunRecord.getTimeTaken(); } avgRunTime /= runs; workflowRunRecords = sort(workflowRunRecords); List<PercentileInformation> percentileInformationList = getPercentiles(workflowRunRecords, percentiles); Collection<ProgramRunDetails> programToRunRecord = getProgramRuns(workflowRunRecords); Map<String, Map<String, String>> programToStatistic = new HashMap<>(); for (ProgramRunDetails entry : programToRunRecord) { double avgForProgram = 0; for (long value : entry.getProgramRunList()) { avgForProgram += value; } avgForProgram /= entry.getProgramRunList().size(); Map<String, String> programMap = new HashMap<>(); programMap.put("type", entry.getProgramType().toString()); programMap.put("runs", Long.toString(entry.getProgramRunList().size())); programMap.put("avgRunTime", Double.toString(avgForProgram)); programToStatistic.put(entry.getName(), programMap); List<Long> runList = entry.getProgramRunList(); Collections.sort(runList); for (double percentile : percentiles) { long percentileValue = runList.get((int) ((percentile * runList.size()) / 100)); programToStatistic.get(entry.getName()).put(Double.toString(percentile), Long.toString(percentileValue)); } } return new WorkflowStatistics(startTime, endTime, runs, avgRunTime, percentileInformationList, programToStatistic); } private List<PercentileInformation> getPercentiles(List<WorkflowRunRecord> workflowRunRecords, List<Double> percentiles) { int runs = workflowRunRecords.size(); List<PercentileInformation> percentileInformationList = new ArrayList<>(); for (double i : percentiles) { List<String> percentileRun = new ArrayList<>(); int percentileStart = (int) ((i * runs) / 100); for (int j = percentileStart; j < runs; j++) { percentileRun.add(workflowRunRecords.get(j).getWorkflowRunId()); } percentileInformationList.add( new PercentileInformation(i, workflowRunRecords.get(percentileStart).getTimeTaken(), percentileRun)); } return percentileInformationList; } private List<WorkflowDataset.WorkflowRunRecord> sort(List<WorkflowDataset.WorkflowRunRecord> workflowRunRecords) { Collections.sort(workflowRunRecords, new Comparator<WorkflowRunRecord>() { @Override public int compare(WorkflowDataset.WorkflowRunRecord o1, WorkflowDataset.WorkflowRunRecord o2) { return Longs.compare(o1.getTimeTaken(), o2.getTimeTaken()); } }); return workflowRunRecords; } private Collection<ProgramRunDetails> getProgramRuns(List<WorkflowDataset.WorkflowRunRecord> workflowRunRecords) { Map<String, ProgramRunDetails> programToRunRecord = new HashMap<>(); for (WorkflowDataset.WorkflowRunRecord workflowRunRecord : workflowRunRecords) { for (WorkflowDataset.ProgramRun run : workflowRunRecord.getProgramRuns()) { ProgramRunDetails programRunDetails = programToRunRecord.get(run.getName()); if (programRunDetails == null) { programRunDetails = new ProgramRunDetails(run.getName(), run.getProgramType(), new ArrayList<Long>()); programToRunRecord.put(run.getName(), programRunDetails); } programRunDetails.addToProgramRunList(run.getTimeTaken()); } } return programToRunRecord.values(); } @Nullable WorkflowRunRecord getRecord(Id.Workflow id, String pid) { RunId runId = RunIds.fromString(pid); long startTime = RunIds.getTime(runId, TimeUnit.SECONDS); MDSKey mdsKey = new MDSKey.Builder().add(id.getNamespaceId()) .add(id.getApplicationId()).add(id.getId()).add(startTime).build(); byte[] startRowKey = mdsKey.getKey(); Row indexRow = table.get(startRowKey); if (indexRow.isEmpty()) { return null; } Map<byte[], byte[]> columns = indexRow.getColumns(); String workflowRunId = Bytes.toString(columns.get(RUNID)); long timeTaken = Bytes.toLong(columns.get(TIME_TAKEN)); List<ProgramRun> actionRunsList = GSON.fromJson(Bytes.toString(columns.get(NODES)), PROGRAM_RUNS_TYPE); return new WorkflowRunRecord(workflowRunId, timeTaken, actionRunsList); } Collection<WorkflowRunRecord> getDetailsOfRange(Id.Workflow workflow, String runId, int limit, long timeInterval) { Map<String, WorkflowRunRecord> mainRunRecords = getNeighbors(workflow, RunIds.fromString(runId), limit, timeInterval); WorkflowRunRecord workflowRunRecord = getRecord(workflow, runId); if (workflowRunRecord != null) { mainRunRecords.put(workflowRunRecord.getWorkflowRunId(), workflowRunRecord); } return mainRunRecords.values(); } /** * Returns a map of WorkflowRunId to WorkflowRunRecord that are close to the WorkflowRunId provided by the user. * * @param id The workflow * @param runId The runid of the workflow * @param limit The limit on each side of the run that we want to see into * @param timeInterval The time interval that we want the results to be spaced apart * @return A Map of WorkflowRunId to the corresponding Workflow Run Record. A map is used so that duplicates of * the WorkflowRunRecord are not obtained */ private Map<String, WorkflowRunRecord> getNeighbors(Id.Workflow id, RunId runId, int limit, long timeInterval) { long startTime = RunIds.getTime(runId, TimeUnit.SECONDS); Map<String, WorkflowRunRecord> workflowRunRecords = new HashMap<>(); int i = -limit; long prevStartTime = startTime - (limit * timeInterval); // The loop iterates across the range that is startTime - (limit * timeInterval) to // startTime + (limit * timeInterval) since we want to capture all runs that started in this range. // Since we want to stop getting the same key, we have the prevStartTime become 1 more than the time at which // the last record was found if the (interval * the count of the loop) is less than the time. while (prevStartTime <= startTime + (limit * timeInterval)) { MDSKey mdsKey = new MDSKey.Builder().add(id.getNamespaceId()) .add(id.getApplicationId()).add(id.getId()).add(prevStartTime).build(); byte[] startRowKey = mdsKey.getKey(); Scan scan = new Scan(startRowKey, null); Scanner scanner = table.scan(scan); Row indexRow = scanner.next(); if (indexRow == null) { return workflowRunRecords; } byte[] rowKey = indexRow.getRow(); long time = ByteBuffer.wrap(rowKey, rowKey.length - Bytes.SIZEOF_LONG, Bytes.SIZEOF_LONG).getLong(); if (!((time >= (startTime - (limit * timeInterval))) && time <= (startTime + (limit * timeInterval)))) { break; } Map<byte[], byte[]> columns = indexRow.getColumns(); String workflowRunId = Bytes.toString(columns.get(RUNID)); long timeTaken = Bytes.toLong(columns.get(TIME_TAKEN)); List<ProgramRun> programRunList = GSON.fromJson(Bytes.toString(columns.get(NODES)), PROGRAM_RUNS_TYPE); workflowRunRecords.put(workflowRunId, new WorkflowRunRecord(workflowRunId, timeTaken, programRunList)); prevStartTime = startTime + (i * timeInterval) < time ? time + 1 : startTime + (i * timeInterval); i++; } return workflowRunRecords; } /** * Class to store the name, type and list of runs of the programs across all workflow runs */ private static final class ProgramRunDetails { private final String name; private final ProgramType programType; private final List<Long> programRunList; public ProgramRunDetails(String name, ProgramType programType, List<Long> programRunList) { this.name = name; this.programType = programType; this.programRunList = programRunList; } public void addToProgramRunList(long time) { this.programRunList.add(time); } public String getName() { return name; } public ProgramType getProgramType() { return programType; } public List<Long> getProgramRunList() { return programRunList; } } /** * Class to keep track of Workflow Run Records */ public static final class WorkflowRunRecord { private final String workflowRunId; private final long timeTaken; private final List<ProgramRun> programRuns; public WorkflowRunRecord(String workflowRunId, long timeTaken, List<ProgramRun> programRuns) { this.programRuns = programRuns; this.timeTaken = timeTaken; this.workflowRunId = workflowRunId; } public long getTimeTaken() { return timeTaken; } public List<ProgramRun> getProgramRuns() { return programRuns; } public String getWorkflowRunId() { return workflowRunId; } } /** * Class for keeping track of programs in a workflow */ public static final class ProgramRun { private final String runId; private final long timeTaken; private final ProgramType programType; private final String name; public ProgramRun(String name, String runId, ProgramType programType, long timeTaken) { this.name = name; this.runId = runId; this.programType = programType; this.timeTaken = timeTaken; } public ProgramType getProgramType() { return programType; } public long getTimeTaken() { return timeTaken; } public String getName() { return name; } public String getRunId() { return runId; } } }