Package co.cask.cdap.metrics.data

Source Code of co.cask.cdap.metrics.data.TimeSeriesTable$ScannerFields

/*
* Copyright © 2014 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.metrics.data;

import co.cask.cdap.api.common.Bytes;
import co.cask.cdap.api.dataset.table.Row;
import co.cask.cdap.api.dataset.table.Scanner;
import co.cask.cdap.common.utils.ImmutablePair;
import co.cask.cdap.data2.OperationException;
import co.cask.cdap.data2.StatusCode;
import co.cask.cdap.data2.dataset2.lib.table.FuzzyRowFilter;
import co.cask.cdap.data2.dataset2.lib.table.MetricsTable;
import co.cask.cdap.metrics.MetricsConstants;
import co.cask.cdap.metrics.transport.MetricsRecord;
import co.cask.cdap.metrics.transport.TagMetric;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;

import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.NavigableMap;

/**
* Table for storing time series metrics.
* <p>
* Row key:
* {@code context|metricName|tags|timebase|runId}
* </p>
* <p>
* Columns: offset to timebase of the row.
* </p>
* <p>
* Cell: Value for the metrics specified by the row with at the timestamp of (timbase + offset) * resolution.
* </p>
* <p>
* TODO: More doc.
* </p>
*/
public final class TimeSeriesTable {

  private static final int MAX_ROLL_TIME = 0xfffe;
  private static final byte[] FOUR_ZERO_BYTES = {0, 0, 0, 0};
  private static final byte[] FOUR_ONE_BYTES = {1, 1, 1, 1};

  private final MetricsTable timeSeriesTable;
  private final MetricsEntityCodec entityCodec;
  private final int resolution;
  private final int rollTimebaseInterval;
  private final ImmutablePair<byte[], byte[]> defaultTagFuzzyPair;

  // Cache for delta values.
  private final byte[][] deltaCache;

  /**
   * Creates a MetricTable.
   *
   * @param timeSeriesTable A OVC table for storing metric information.
   * @param entityCodec The {@link MetricsEntityCodec} for encoding entity.
   * @param resolution Resolution in second of the table
   * @param rollTime Number of resolution for writing to a new row with a new timebase.
   *                 Meaning the differences between timebase of two consecutive rows divided by
   *                 resolution seconds. It essentially defines how many columns per row in the table.
   *                 This value should be < 65535.
   */
  TimeSeriesTable(MetricsTable timeSeriesTable,
                  MetricsEntityCodec entityCodec, int resolution, int rollTime) {

    this.timeSeriesTable = timeSeriesTable;
    this.entityCodec = entityCodec;
    this.resolution = resolution;

    // Two bytes for column name, which is a delta timestamp
    Preconditions.checkArgument(rollTime <= MAX_ROLL_TIME, "Rolltime should be <= " + MAX_ROLL_TIME);
    this.rollTimebaseInterval = rollTime * resolution;
    this.deltaCache = createDeltaCache(rollTime);

    this.defaultTagFuzzyPair = createDefaultTagFuzzyPair();
  }

  /**
   * Saves a collection of {@link co.cask.cdap.metrics.transport.MetricsRecord}.
   */
  public void save(Iterable<MetricsRecord> records) throws OperationException {
    save(records.iterator());
  }

  public void save(Iterator<MetricsRecord> records) throws OperationException {
    if (!records.hasNext()) {
      return;
    }

    // Simply collecting all rows/cols/values that need to be put to the underlying table.
    NavigableMap<byte[], NavigableMap<byte[], byte[]>> table = Maps.newTreeMap(Bytes.BYTES_COMPARATOR);

    while (records.hasNext()) {
      getUpdates(records.next(), table);
    }

    try {
      timeSeriesTable.put(table);
    } catch (Exception e) {
      throw new OperationException(StatusCode.INTERNAL_ERROR, e.getMessage(), e);
    }
  }

  public MetricsScanner scan(MetricsScanQuery query) throws OperationException {
    return scanFor(query, false);
  }

  public MetricsScanner scanAllTags(MetricsScanQuery query) throws OperationException {
    return scanFor(query, true);
  }

  /**
   * Deletes all the row keys which match the context prefix.
   *
   * @param contextPrefix Prefix of the context to match.  Must not be null, as full table deletes should be done
   *                      through the clear method.
   * @throws OperationException if there is an error in deleting entries.
   */
  public void delete(String contextPrefix) throws OperationException {
    Preconditions.checkArgument(contextPrefix != null, "null context not allowed for delete");
    try {
      timeSeriesTable.deleteAll(entityCodec.encodeWithoutPadding(MetricsEntityType.CONTEXT, contextPrefix));
    } catch (Exception e) {
      throw new OperationException(StatusCode.INTERNAL_ERROR, e.getMessage(), e);
    }
  }

  /**
   * Deletes all the row keys which match the context prefix and metric prefix.  Context and Metric cannot both be
   * null, as full table deletes should be done through the clear method.
   *
   * @param contextPrefix Prefix of the context to match, null means any context.
   * @param metricPrefix Prefix of the metric to match, null means any metric.
   * @throws OperationException if there is an error in deleting entries.
   */
  public void delete(String contextPrefix, String metricPrefix) throws OperationException {
    Preconditions.checkArgument(contextPrefix != null || metricPrefix != null,
                                "context and metric cannot both be null");
    if (metricPrefix == null) {
      delete(contextPrefix);
    } else {
      byte[] startRow = getPaddedKey(contextPrefix, "0", metricPrefix, null, 0, 0);
      byte[] endRow = getPaddedKey(contextPrefix, "0", metricPrefix, null, Integer.MAX_VALUE, 0xff);
      try {
        // Create fuzzy row filter
        ImmutablePair<byte[], byte[]> contextPair = entityCodec.paddedFuzzyEncode(MetricsEntityType.CONTEXT,
                                                                                  contextPrefix, 0);
        ImmutablePair<byte[], byte[]> metricPair = entityCodec.paddedFuzzyEncode(MetricsEntityType.METRIC,
                                                                                 metricPrefix, 0);
        ImmutablePair<byte[], byte[]> tagPair = entityCodec.paddedFuzzyEncode(MetricsEntityType.TAG, null, 0);
        ImmutablePair<byte[], byte[]> runIdPair = entityCodec.paddedFuzzyEncode(MetricsEntityType.RUN, null, 0);
        FuzzyRowFilter filter = new FuzzyRowFilter(ImmutableList.of(ImmutablePair.of(
          Bytes.concat(contextPair.getFirst(), metricPair.getFirst(), tagPair.getFirst(),
                       Bytes.toBytes(0), runIdPair.getFirst()),
          Bytes.concat(contextPair.getSecond(), metricPair.getSecond(), tagPair.getSecond(),
                       FOUR_ONE_BYTES, runIdPair.getSecond()))));

        timeSeriesTable.deleteRange(startRow, endRow, null, filter);
      } catch (Exception e) {
        throw new OperationException(StatusCode.INTERNAL_ERROR, e.getMessage(), e);
      }
    }
  }

  /**
   * Delete all entries that would match the given scan query.
   *
   * @param query Query specifying context, metric, runid, tag, and time range of entries to delete.  A null value for
   *              context, metric, and runId will match any value for those fields.  A null value for tag will
   *              match untagged entries, which is the same as using MetricsConstants.EMPTY_TAG.
   * @throws OperationException
   */
  public void delete(MetricsScanQuery query) throws OperationException {
    try {
      ScannerFields fields = getScannerFields(query);
      timeSeriesTable.deleteRange(fields.startRow, fields.endRow, fields.columns, fields.filter);
    } catch (Exception e) {
      throw new OperationException(StatusCode.INTERNAL_ERROR, e.getMessage(), e);
    }
  }

  /**
   * Deletes all row keys that has timestamp before the given time.
   * @param beforeTime All data before this timestamp will be removed (exclusive).
   */
  public void deleteBefore(long beforeTime) throws OperationException {
    // End time base is the last time base that is smaller than endTime.
    int endTimeBase = getTimeBase(beforeTime);

    Scanner scanner = null;
    try {
      scanner = timeSeriesTable.scan(null, null, null, null);

      // Loop through the scanner entries and collect rows to be deleted
      List<byte[]> rows = Lists.newArrayList();
      Row nextEntry;
      while ((nextEntry = scanner.next()) != null) {
        byte[] rowKey = nextEntry.getRow();

        // Decode timestamp
        int offset = entityCodec.getEncodedSize(MetricsEntityType.CONTEXT) +
          entityCodec.getEncodedSize(MetricsEntityType.METRIC) +
          entityCodec.getEncodedSize(MetricsEntityType.TAG);
        int timeBase = Bytes.toInt(rowKey, offset, 4);
        if (timeBase < endTimeBase) {
          rows.add(rowKey);
        }
      }
      // If there is any row collected, delete them
      if (!rows.isEmpty()) {
        timeSeriesTable.delete(rows);
      }
    } catch (Exception e) {
      throw new OperationException(StatusCode.INTERNAL_ERROR, e.getMessage(), e);
    } finally {
      if (scanner != null) {
        scanner.close();
      }
    }
  }


  /**
   * Clears the storage table.
   * @throws OperationException If error in clearing data.
   */
  public void clear() throws OperationException {
    try {
      timeSeriesTable.deleteAll(new byte[]{});
    } catch (Exception e) {
      throw new OperationException(StatusCode.INTERNAL_ERROR, e.getMessage(), e);
    }
  }

  private MetricsScanner scanFor(MetricsScanQuery query, boolean shouldMatchAllTags) throws OperationException {
    try {
      ScannerFields fields = getScannerFields(query, shouldMatchAllTags);
      Scanner scanner = timeSeriesTable.scan(fields.startRow, fields.endRow, fields.columns, fields.filter);
      return new MetricsScanner(query, scanner, entityCodec, resolution);
    } catch (Exception e) {
      throw new OperationException(StatusCode.INTERNAL_ERROR, e.getMessage(), e);
    }
  }

  /**
   * Setups all rows, columns and values for updating the metric table.
   */
  private void getUpdates(MetricsRecord record, NavigableMap<byte[], NavigableMap<byte[], byte[]>> table) {
    long timestamp = record.getTimestamp() / resolution * resolution;
    int timeBase = getTimeBase(timestamp);

    // Key for the no tag one
    byte[] rowKey = getKey(record.getContext(), record.getRunId(), record.getName(), null, timeBase);

    // delta is guaranteed to be 2 bytes.
    byte[] column = deltaCache[(int) (timestamp - timeBase)];

    addValue(rowKey, column, table, record.getValue());

    // Save tags metrics
    for (TagMetric tag : record.getTags()) {
      rowKey = getKey(record.getContext(), record.getRunId(), record.getName(), tag.getTag(), timeBase);
      addValue(rowKey, column, table, tag.getValue());
    }
  }

  private void addValue(byte[] rowKey, byte[] column,
                        NavigableMap<byte[], NavigableMap<byte[], byte[]>> table, int value) {
    byte[] oldValue = get(table, rowKey, column);
    int newValue = value;
    if (oldValue != null) {
      newValue = Bytes.toInt(oldValue) + value;
    }
    put(table, rowKey, column, Bytes.toBytes(newValue));
  }

  private static byte[] get(NavigableMap<byte[], NavigableMap<byte[], byte[]>> table, byte[] row, byte[] column) {
    NavigableMap<byte[], byte[]> rowMap = table.get(row);
    return rowMap == null ? null : rowMap.get(column);
  }

  private static void put(NavigableMap<byte[], NavigableMap<byte[], byte[]>> table,
                          byte[] row, byte[] column, byte[] value) {
    NavigableMap<byte[], byte[]> rowMap = table.get(row);
    if (rowMap == null) {
      rowMap = Maps.newTreeMap(Bytes.BYTES_COMPARATOR);
      table.put(row, rowMap);
    }

    rowMap.put(column, value);
  }

  /**
   * Creates the row key for the given context, metric, tag, and timebase.
   */
  private byte[] getKey(String context, String runId, String metric, String tag, int timeBase) {
    Preconditions.checkArgument(context != null, "Context cannot be null.");
    Preconditions.checkArgument(runId != null, "RunId cannot be null.");
    Preconditions.checkArgument(metric != null, "Metric cannot be null.");

    return Bytes.concat(entityCodec.encode(MetricsEntityType.CONTEXT, context),
                        entityCodec.encode(MetricsEntityType.METRIC, metric),
                        entityCodec.encode(MetricsEntityType.TAG, tag == null ? MetricsConstants.EMPTY_TAG : tag),
                        Bytes.toBytes(timeBase),
                        entityCodec.encode(MetricsEntityType.RUN, runId));
  }

  private byte[] getPaddedKey(String contextPrefix, String runId, String metricPrefix, String tagPrefix,
                              int timeBase, int padding) {

    // If there is no contextPrefix, metricPrefix or runId, just applies the padding
    return Bytes.concat(
      entityCodec.paddedEncode(MetricsEntityType.CONTEXT, contextPrefix, padding),
      entityCodec.paddedEncode(MetricsEntityType.METRIC, metricPrefix, padding),
      entityCodec.paddedEncode(MetricsEntityType.TAG, tagPrefix, padding),
      Bytes.toBytes(timeBase),
      entityCodec.paddedEncode(MetricsEntityType.RUN, runId, padding));
  }

  private FuzzyRowFilter getFilter(MetricsScanQuery query, long startTimeBase,
                                   long endTimeBase, boolean shouldMatchAllTags) {
    String tag = query.getTagPrefix();

    // Create fuzzy row filter
    ImmutablePair<byte[], byte[]> contextPair = entityCodec.paddedFuzzyEncode(MetricsEntityType.CONTEXT,
                                                                              query.getContextPrefix(), 0);
    ImmutablePair<byte[], byte[]> metricPair = entityCodec.paddedFuzzyEncode(MetricsEntityType.METRIC,
                                                                             query.getMetricPrefix(), 0);
    ImmutablePair<byte[], byte[]> tagPair = (!shouldMatchAllTags && tag == null)
                                                ? defaultTagFuzzyPair
                                                : entityCodec.paddedFuzzyEncode(MetricsEntityType.TAG, tag, 0);
    ImmutablePair<byte[], byte[]> runIdPair = entityCodec.paddedFuzzyEncode(MetricsEntityType.RUN, query.getRunId(), 0);

    // For each timbase, construct a fuzzy filter pair
    List<ImmutablePair<byte[], byte[]>> fuzzyPairs = Lists.newLinkedList();
    for (long timeBase = startTimeBase; timeBase <= endTimeBase; timeBase += this.rollTimebaseInterval) {
      fuzzyPairs.add(ImmutablePair.of(Bytes.concat(contextPair.getFirst(), metricPair.getFirst(), tagPair.getFirst(),
                                                   Bytes.toBytes((int) timeBase), runIdPair.getFirst()),
                                      Bytes.concat(contextPair.getSecond(), metricPair.getSecond(), tagPair.getSecond(),
                                                   FOUR_ZERO_BYTES, runIdPair.getSecond())));
    }

    return new FuzzyRowFilter(fuzzyPairs);
  }

  /**
   * Returns timebase computed with the table setting for the given timestamp.
   */
  private int getTimeBase(long time) {
    // We are using 4 bytes timebase for row
    long timeBase = time / rollTimebaseInterval * rollTimebaseInterval;
    Preconditions.checkArgument(timeBase < 0x100000000L, "Timestamp is too large.");
    return (int) timeBase;
  }


  private byte[][] createDeltaCache(int rollTime) {
    byte[][] deltas = new byte[rollTime + 1][];

    for (int i = 0; i <= rollTime; i++) {
      deltas[i] = Bytes.toBytes((short) i);
    }
    return deltas;
  }

  private ImmutablePair<byte[], byte[]> createDefaultTagFuzzyPair() {
    byte[] key = entityCodec.encode(MetricsEntityType.TAG, MetricsConstants.EMPTY_TAG);
    byte[] mask = new byte[key.length];
    Arrays.fill(mask, (byte) 0);
    return new ImmutablePair<byte[], byte[]>(key, mask);
  }

  private  ScannerFields getScannerFields(MetricsScanQuery query) {
    return getScannerFields(query, false);
  }

  private ScannerFields getScannerFields(MetricsScanQuery query, boolean shouldMatchAllTags) {
    int startTimeBase = getTimeBase(query.getStartTime());
    int endTimeBase = getTimeBase(query.getEndTime());

    byte[][] columns = null;
    if (startTimeBase == endTimeBase) {
      // If on the same timebase, we only need subset of columns
      int startCol = (int) (query.getStartTime() - startTimeBase) / resolution;
      int endCol = (int) (query.getEndTime() - endTimeBase) / resolution;
      columns = new byte[endCol - startCol + 1][];

      for (int i = 0; i < columns.length; i++) {
        columns[i] = Bytes.toBytes((short) (startCol + i));
      }
    }

    String tagPrefix = query.getTagPrefix();
    if (!shouldMatchAllTags && tagPrefix == null) {
      tagPrefix = MetricsConstants.EMPTY_TAG;
    }
    byte[] startRow = getPaddedKey(query.getContextPrefix(), query.getRunId(),
                                   query.getMetricPrefix(), tagPrefix, startTimeBase, 0);
    byte[] endRow = getPaddedKey(query.getContextPrefix(), query.getRunId(),
                                 query.getMetricPrefix(), tagPrefix, endTimeBase + 1, 0xff);
    FuzzyRowFilter filter = getFilter(query, startTimeBase, endTimeBase, shouldMatchAllTags);

    return new ScannerFields(startRow, endRow, columns, filter);
  }

  private class ScannerFields {
    private final byte[] startRow;
    private final byte[] endRow;
    private final byte[][] columns;
    private final FuzzyRowFilter filter;

    ScannerFields(byte[] startRow, byte[] endRow, byte[][] columns, FuzzyRowFilter filter) {
      this.startRow = startRow;
      this.endRow = endRow;
      this.columns = columns;
      this.filter = filter;
    }
  }
}
TOP

Related Classes of co.cask.cdap.metrics.data.TimeSeriesTable$ScannerFields

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.