Package net.opentsdb.meta

Source Code of net.opentsdb.meta.FetchNewCB

// This file is part of OpenTSDB.
// Copyright (C) 2013  The OpenTSDB Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 2.1 of the License, or (at your
// option) any later version.  This program is distributed in the hope that it
// will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
// of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser
// General Public License for more details.  You should have received a copy
// of the GNU Lesser General Public License along with this program.  If not,
// see <http://www.gnu.org/licenses/>.
package net.opentsdb.meta;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.opentsdb.core.TSDB;
import net.opentsdb.uid.UniqueId;
import net.opentsdb.uid.UniqueId.UniqueIdType;
import net.opentsdb.utils.JSON;
import net.opentsdb.utils.JSONException;

import org.hbase.async.AtomicIncrementRequest;
import org.hbase.async.Bytes;
import org.hbase.async.DeleteRequest;
import org.hbase.async.GetRequest;
import org.hbase.async.HBaseException;
import org.hbase.async.KeyValue;
import org.hbase.async.PutRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonGenerator;
import com.stumbleupon.async.Callback;
import com.stumbleupon.async.Deferred;

/**
* Timeseries Metadata is associated with a particular series of data points
* and includes user configurable values and some stats calculated by OpenTSDB.
* Whenever a new timeseries is recorded, an associated TSMeta object will
* be stored with only the tsuid field configured. These meta objects may then
* be used to determine what combinations of metrics and tags exist in the
* system.
* <p>
* When you call {@link #syncToStorage} on this object, it will verify that the
* associated UID objects this meta data is linked with still exist. Then it
* will fetch the existing data and copy changes, overwriting the user fields if
* specific (e.g. via a PUT command). If overwriting is not called for (e.g. a
* POST was issued), then only the fields provided by the user will be saved,
* preserving all of the other fields in storage. Hence the need for the
* {@code changed} hash map and the {@link #syncMeta} method.
* <p>
* The metric and tag UIDMeta objects may be loaded from their respective
* locations in the data storage system if requested. Note that this will cause
* at least 3 extra storage calls when loading.
* @since 2.0
*/
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(Include.NON_NULL)
@JsonAutoDetect(fieldVisibility = Visibility.PUBLIC_ONLY)
public final class TSMeta {
  private static final Logger LOG = LoggerFactory.getLogger(TSMeta.class);

  /** Charset used to convert Strings to byte arrays and back. */
  private static final Charset CHARSET = Charset.forName("ISO-8859-1");
 
  /** The single column family used by this class. */
  private static final byte[] FAMILY = "name".getBytes(CHARSET);
 
  /** The cell qualifier to use for timeseries meta */
  private static final byte[] META_QUALIFIER = "ts_meta".getBytes(CHARSET);
 
  /** The cell qualifier to use for timeseries meta */
  private static final byte[] COUNTER_QUALIFIER = "ts_ctr".getBytes(CHARSET);
 
  /** Hexadecimal representation of the TSUID this metadata is associated with */
  private String tsuid = "";
 
  /** The metric associated with this timeseries */
  private UIDMeta metric = null;
 
  /** A list of tagk/tagv pairs of UIDMetadata associated with this timeseries */
  private ArrayList<UIDMeta> tags = null;
 
  /** An optional, user supplied descriptive name */
  private String display_name = "";
 
  /** An optional short description of the timeseries */
  private String description = "";
 
  /** Optional detailed notes about the timeseries */
  private String notes = "";
 
  /** A timestamp of when this timeseries was first recorded in seconds */
  private long created = 0;
 
  /** Optional user supplied key/values */
  private HashMap<String, String> custom = null;
 
  /** An optional field recording the units of data in this timeseries */
  private String units = "";
 
  /** An optional field used to record the type of data, e.g. counter, gauge */
  private String data_type = "";
 
  /** How long to keep raw data in this timeseries */
  private int retention = 0;
 
  /**
   * A user defined maximum value for this timeseries, can be used to
   * calculate percentages
   */
  private double max = Double.NaN;
 
  /**
   * A user defined minimum value for this timeseries, can be used to
   * calculate percentages
   */
  private double min = Double.NaN;
 
  /** The last time this data was recorded in seconds */
  private long last_received = 0;
 
  /** The total number of data points recorded since meta has been enabled */
  private long total_dps;

  /** Tracks fields that have changed by the user to avoid overwrites */
  private final HashMap<String, Boolean> changed =
    new HashMap<String, Boolean>();

  /**
   * Default constructor necessary for POJO de/serialization
   */
  public TSMeta() {
    initializeChangedMap();
  }
 
  /**
   * Constructor for RPC timeseries parsing that will not set the timestamps
   * @param tsuid The UID of the timeseries
   */
  public TSMeta(final String tsuid) {
    this.tsuid = tsuid;
    initializeChangedMap();
  }
 
  /**
   * Constructor for new timeseries that initializes the created and
   * last_received times to the current system time
   * @param tsuid The UID of the timeseries
   */
  public TSMeta(final byte[] tsuid, final long created) {
    this.tsuid = UniqueId.uidToString(tsuid);
    // downgrade to seconds
    this.created = created > 9999999999L ? created / 1000 : created;
    initializeChangedMap();
    changed.put("created", true);
  }
 
  /** @return a string with details about this object */
  @Override
  public String toString() {
    return tsuid;
  }
 
  /**
   * Attempts to delete the meta object from storage
   * @param tsdb The TSDB to use for access to storage
   * @return A deferred without meaning. The response may be null and should
   * only be used to track completion.
   * @throws HBaseException if there was an issue
   * @throws IllegalArgumentException if data was missing (uid and type)
   */
  public Deferred<Object> delete(final TSDB tsdb) {
    if (tsuid == null || tsuid.isEmpty()) {
      throw new IllegalArgumentException("Missing UID");
    }

    final DeleteRequest delete = new DeleteRequest(tsdb.metaTable(),
        UniqueId.stringToUid(tsuid), FAMILY, META_QUALIFIER);
    return tsdb.getClient().delete(delete);
  }
 
  /**
   * Attempts a CompareAndSet storage call, loading the object from storage,
   * synchronizing changes, and attempting a put. Also verifies that associated
   * UID name mappings exist before merging.
   * <b>Note:</b> If the local object didn't have any fields set by the caller
   * or there weren't any changes, then the data will not be written and an
   * exception will be thrown.
   * <b>Note:</b> We do not store the UIDMeta information with TSMeta's since
   * users may change a single UIDMeta object and we don't want to update every
   * TSUID that includes that object with the new data. Instead, UIDMetas are
   * merged into the TSMeta on retrieval so we always have canonical data. This
   * also saves space in storage.
   * @param tsdb The TSDB to use for storage access
   * @param overwrite When the RPC method is PUT, will overwrite all user
   * accessible fields
   * @return True if the storage call was successful, false if the object was
   * modified in storage during the CAS call. If false, retry the call. Other
   * failures will result in an exception being thrown.
   * @throws HBaseException if there was an issue
   * @throws IllegalArgumentException if parsing failed
   * @throws NoSuchUniqueId If any of the UID name mappings do not exist
   * @throws IllegalStateException if the data hasn't changed. This is OK!
   * @throws JSONException if the object could not be serialized
   */
  public Deferred<Boolean> syncToStorage(final TSDB tsdb,
      final boolean overwrite) {
    if (tsuid == null || tsuid.isEmpty()) {
      throw new IllegalArgumentException("Missing TSUID");
    }
   
    boolean has_changes = false;
    for (Map.Entry<String, Boolean> entry : changed.entrySet()) {
      if (entry.getValue()) {
        has_changes = true;
        break;
      }
    }
    if (!has_changes) {
      LOG.debug(this + " does not have changes, skipping sync to storage");
      throw new IllegalStateException("No changes detected in TSUID meta data");
    }

    /**
     * Callback used to verify that the UID name mappings exist. We don't need
     * to process the actual name, we just want it to throw an error if any
     * of the UIDs don't exist.
     */
    class UidCB implements Callback<Object, String> {

      @Override
      public Object call(String name) throws Exception {
        // nothing to do as missing mappings will throw a NoSuchUniqueId
        return null;
      }
     
    }
   
    // parse out the tags from the tsuid
    final List<byte[]> parsed_tags = UniqueId.getTagPairsFromTSUID(tsuid,
        TSDB.metrics_width(), TSDB.tagk_width(), TSDB.tagv_width());
   
    // Deferred group used to accumulate UidCB callbacks so the next call
    // can wait until all of the UIDs have been verified
    ArrayList<Deferred<Object>> uid_group =
      new ArrayList<Deferred<Object>>(parsed_tags.size() + 1);
   
    // calculate the metric UID and fetch it's name mapping
    final byte[] metric_uid = UniqueId.stringToUid(
        tsuid.substring(0, TSDB.metrics_width() * 2));
    uid_group.add(tsdb.getUidName(UniqueIdType.METRIC, metric_uid)
        .addCallback(new UidCB()));
   
    int idx = 0;
    for (byte[] tag : parsed_tags) {
      if (idx % 2 == 0) {
        uid_group.add(tsdb.getUidName(UniqueIdType.TAGK, tag)
            .addCallback(new UidCB()));
      } else {
        uid_group.add(tsdb.getUidName(UniqueIdType.TAGV, tag)
            .addCallback(new UidCB()));
      }
      idx++;
    }
   
    /**
     * Callback executed after all of the UID mappings have been verified. This
     * will then proceed with the CAS call.
     */
    final class ValidateCB implements Callback<Deferred<Boolean>,
      ArrayList<Object>> {
      private final TSMeta local_meta;
     
      public ValidateCB(final TSMeta local_meta) {
        this.local_meta = local_meta;
      }
     
      /**
       * Nested class that executes the CAS after retrieving existing TSMeta
       * from storage.
       */
      final class StoreCB implements Callback<Deferred<Boolean>, TSMeta> {

        /**
         * Executes the CAS if the TSMeta was successfully retrieved
         * @return True if the CAS was successful, false if the stored data
         * was modified in flight
         * @throws IllegalArgumentException if the TSMeta did not exist in
         * storage. Only the TSD should be able to create TSMeta objects.
         */
        @Override
        public Deferred<Boolean> call(TSMeta stored_meta) throws Exception {
          if (stored_meta == null) {
            throw new IllegalArgumentException("Requested TSMeta did not exist");
          }
         
          final byte[] original_meta = stored_meta.getStorageJSON();
          local_meta.syncMeta(stored_meta, overwrite);

          final PutRequest put = new PutRequest(tsdb.metaTable(),
              UniqueId.stringToUid(local_meta.tsuid), FAMILY, META_QUALIFIER,
              local_meta.getStorageJSON());

          return tsdb.getClient().compareAndSet(put, original_meta);
        }
       
      }
     
      /**
       * Called on UID mapping verification and continues executing the CAS
       * procedure.
       * @return Results from the {@link #StoreCB} callback
       */
      @Override
      public Deferred<Boolean> call(ArrayList<Object> validated)
        throws Exception {
        return getFromStorage(tsdb, UniqueId.stringToUid(tsuid))
          .addCallbackDeferring(new StoreCB());
      }
     
    }
   
    // Begins the callback chain by validating that the UID mappings exist
    return Deferred.group(uid_group).addCallbackDeferring(new ValidateCB(this));
  }
 
  /**
   * Attempts to store a new, blank timeseries meta object via a CompareAndSet
   * <b>Note:</b> This should not be called by user accessible methods as it will
   * overwrite any data already in the column.
   * <b>Note:</b> This call does not guarantee that the UIDs exist before
   * storing as it should only be called *after* a data point has been recorded
   * or during a meta sync.
   * @param tsdb The TSDB to use for storage access
   * @return True if the CAS completed successfully (and no TSMeta existed
   * previously), false if something was already stored in the TSMeta column.
   * @throws HBaseException if there was an issue fetching
   * @throws IllegalArgumentException if parsing failed
   * @throws JSONException if the object could not be serialized
   */
  public Deferred<Boolean> storeNew(final TSDB tsdb) {
    if (tsuid == null || tsuid.isEmpty()) {
      throw new IllegalArgumentException("Missing TSUID");
    }

    final PutRequest put = new PutRequest(tsdb.metaTable(),
        UniqueId.stringToUid(tsuid), FAMILY, META_QUALIFIER, getStorageJSON());
   
    final class PutCB implements Callback<Deferred<Boolean>, Object> {
      @Override
      public Deferred<Boolean> call(Object arg0) throws Exception {
        return Deferred.fromResult(true);
      }     
    }
   
    return tsdb.getClient().put(put).addCallbackDeferring(new PutCB());
  }
 
  /**
   * Attempts to fetch the timeseries meta data and associated UIDMeta objects
   * from storage.
   * <b>Note:</b> Until we have a caching layer implemented, this will make at
   * least 4 reads to the storage system, 1 for the TSUID meta, 1 for the
   * metric UIDMeta and 1 each for every tagk/tagv UIDMeta object.
   * <p>
   * See {@link #getFromStorage(TSDB, byte[])} for details.
   * @param tsdb The TSDB to use for storage access
   * @param tsuid The UID of the meta to fetch
   * @return A TSMeta object if found, null if not
   * @throws HBaseException if there was an issue fetching
   * @throws IllegalArgumentException if parsing failed
   * @throws JSONException if the data was corrupted
   * @throws NoSuchUniqueName if one of the UIDMeta objects does not exist
   */
  public static Deferred<TSMeta> getTSMeta(final TSDB tsdb, final String tsuid) {
    return getFromStorage(tsdb, UniqueId.stringToUid(tsuid))
      .addCallbackDeferring(new LoadUIDs(tsdb, tsuid));
  }
 
  /**
   * Parses a TSMeta object from the given column, optionally loading the
   * UIDMeta objects
   * @param tsdb The TSDB to use for storage access
   * @param column The KeyValue column to parse
   * @param load_uidmetas Whether or not UIDmeta objects should be loaded
   * @return A TSMeta if parsed successfully
   * @throws NoSuchUniqueName if one of the UIDMeta objects does not exist
   * @throws JSONException if the data was corrupted
   */
  public static Deferred<TSMeta> parseFromColumn(final TSDB tsdb,
      final KeyValue column, final boolean load_uidmetas) {
    if (column.value() == null || column.value().length < 1) {
      throw new IllegalArgumentException("Empty column value");
    }
   
    final TSMeta meta = JSON.parseToObject(column.value(), TSMeta.class);
   
    // fix in case the tsuid is missing
    if (meta.tsuid == null || meta.tsuid.isEmpty()) {
      meta.tsuid = UniqueId.uidToString(column.key());
    }
   
    if (!load_uidmetas) {
      return Deferred.fromResult(meta);
    }
   
    final LoadUIDs deferred = new LoadUIDs(tsdb, meta.tsuid);
    try {
      return deferred.call(meta);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
 
  /**
   * Determines if an entry exists in storage or not.
   * This is used by the UID Manager tool to determine if we need to write a
   * new TSUID entry or not. It will not attempt to verify if the stored data is
   * valid, just checks to see if something is stored in the proper column.
   * @param tsdb  The TSDB to use for storage access
   * @param tsuid The UID of the meta to verify
   * @return True if data was found, false if not
   * @throws HBaseException if there was an issue fetching
   */
  public static Deferred<Boolean> metaExistsInStorage(final TSDB tsdb,
      final String tsuid) {
    final GetRequest get = new GetRequest(tsdb.metaTable(),
        UniqueId.stringToUid(tsuid));
    get.family(FAMILY);
    get.qualifier(META_QUALIFIER);
   
    /**
     * Callback from the GetRequest that simply determines if the row is empty
     * or not
     */
    final class ExistsCB implements Callback<Boolean, ArrayList<KeyValue>> {

      @Override
      public Boolean call(ArrayList<KeyValue> row) throws Exception {
        if (row == null || row.isEmpty() || row.get(0).value() == null) {
          return false;
        }
        return true;
      }
     
    }
   
    return tsdb.getClient().get(get).addCallback(new ExistsCB());
  }
 
  /**
   * Determines if the counter column exists for the TSUID.
   * This is used by the UID Manager tool to determine if we need to write a
   * new TSUID entry or not. It will not attempt to verify if the stored data is
   * valid, just checks to see if something is stored in the proper column.
   * @param tsdb The TSDB to use for storage access
   * @param tsuid The UID of the meta to verify
   * @return True if data was found, false if not
   * @throws HBaseException if there was an issue fetching
   */
  public static Deferred<Boolean> counterExistsInStorage(final TSDB tsdb,
      final byte[] tsuid) {
    final GetRequest get = new GetRequest(tsdb.metaTable(), tsuid);
    get.family(FAMILY);
    get.qualifier(COUNTER_QUALIFIER);
   
    /**
     * Callback from the GetRequest that simply determines if the row is empty
     * or not
     */
    final class ExistsCB implements Callback<Boolean, ArrayList<KeyValue>> {

      @Override
      public Boolean call(ArrayList<KeyValue> row) throws Exception {
        if (row == null || row.isEmpty() || row.get(0).value() == null) {
          return false;
        }
        return true;
      }
     
    }
   
    return tsdb.getClient().get(get).addCallback(new ExistsCB());
  }
 
  /**
   * Increments the tsuid datapoint counter or creates a new counter. Also
   * creates a new meta data entry if the counter did not exist.
   * <b>Note:</b> This method also:
   * <ul><li>Passes the new TSMeta object to the Search plugin after loading
   * UIDMeta objects</li>
   * <li>Passes the new TSMeta through all configured trees if enabled</li></ul>
   * @param tsdb The TSDB to use for storage access
   * @param tsuid The TSUID to increment or create
   * @return 0 if the put failed, a positive LONG if the put was successful
   * @throws HBaseException if there was a storage issue
   * @throws JSONException if the data was corrupted
   * @throws NoSuchUniqueName if one of the UIDMeta objects does not exist
   */
  public static Deferred<Long> incrementAndGetCounter(final TSDB tsdb,
      final byte[] tsuid) {
   
    /**
     * Callback that will create a new TSMeta if the increment result is 1 or
     * will simply return the new value.
     */
    final class TSMetaCB implements Callback<Deferred<Long>, Long> {

      /**
       * Called after incrementing the counter and will create a new TSMeta if
       * the returned value was 1 as well as pass the new meta through trees
       * and the search indexer if configured.
       * @return 0 if the put failed, a positive LONG if the put was successful
       */
      @Override
      public Deferred<Long> call(final Long incremented_value)
        throws Exception {
       
        if (incremented_value > 1) {
          // TODO - maybe update the search index every X number of increments?
          // Otherwise the search engine would only get last_updated/count
          // whenever the user runs the full sync CLI
          return Deferred.fromResult(incremented_value);
        }
       
        // create a new meta object with the current system timestamp. Ideally
        // we would want the data point's timestamp, but that's much more data
        // to keep track of and may not be accurate.
        final TSMeta meta = new TSMeta(tsuid,
            System.currentTimeMillis() / 1000);
       
        /**
         * Called after the meta has been passed through tree processing. The
         * result of the processing doesn't matter and the user may not even
         * have it enabled, so we'll just return the counter.
         */
        final class TreeCB implements Callback<Deferred<Long>, Boolean> {

          @Override
          public Deferred<Long> call(Boolean success) throws Exception {
            return Deferred.fromResult(incremented_value);
          }
         
        }
       
        /**
         * Called after retrieving the newly stored TSMeta and loading
         * associated UIDMeta objects. This class will also pass the meta to the
         * search plugin and run it through any configured trees
         */
        final class FetchNewCB implements Callback<Deferred<Long>, TSMeta> {

          @Override
          public Deferred<Long> call(TSMeta stored_meta) throws Exception {
           
            // pass to the search plugin
            tsdb.indexTSMeta(stored_meta);
           
            // pass through the trees
            return tsdb.processTSMetaThroughTrees(stored_meta)
              .addCallbackDeferring(new TreeCB());
          }
         
        }
       
        /**
         * Called after the CAS to store the new TSMeta object. If the CAS
         * failed then we return immediately with a 0 for the counter value.
         * Otherwise we keep processing to load the meta and pass it on.
         */
        final class StoreNewCB implements Callback<Deferred<Long>, Boolean> {

          @Override
          public Deferred<Long> call(Boolean success) throws Exception {
            if (!success) {
              LOG.warn("Unable to save metadata: " + meta);
              return Deferred.fromResult(0L);
            }
           
            LOG.info("Successfullly created new TSUID entry for: " + meta);
            final Deferred<TSMeta> meta = getFromStorage(tsdb, tsuid)
              .addCallbackDeferring(
                new LoadUIDs(tsdb, UniqueId.uidToString(tsuid)));
            return meta.addCallbackDeferring(new FetchNewCB());
          }
         
        }
       
        // store the new TSMeta object and setup the callback chain
        return meta.storeNew(tsdb).addCallbackDeferring(new StoreNewCB());         
      }
     
    }

    // setup the increment request and execute
    final AtomicIncrementRequest inc = new AtomicIncrementRequest(
        tsdb.metaTable(), tsuid, FAMILY, COUNTER_QUALIFIER);
    // if the user has disabled real time TSMeta tracking (due to OOM issues)
    // then we only want to increment the data point count.
    if (!tsdb.getConfig().enable_realtime_ts()) {
      return tsdb.getClient().bufferAtomicIncrement(inc);
    }
    return tsdb.getClient().bufferAtomicIncrement(inc).addCallbackDeferring(
        new TSMetaCB());
  }
 
  /**
   * Attempts to fetch the timeseries meta data from storage.
   * This method will fetch the {@code counter} and {@code meta} columns.
   * <b>Note:</b> This method will not load the UIDMeta objects.
   * @param tsdb The TSDB to use for storage access
   * @param tsuid The UID of the meta to fetch
   * @return A TSMeta object if found, null if not
   * @throws HBaseException if there was an issue fetching
   * @throws IllegalArgumentException if parsing failed
   * @throws JSONException if the data was corrupted
   */
  private static Deferred<TSMeta> getFromStorage(final TSDB tsdb,
      final byte[] tsuid) {
   
    /**
     * Called after executing the GetRequest to parse the meta data.
     */
    final class GetCB implements Callback<Deferred<TSMeta>, ArrayList<KeyValue>> {

      /**
       * @return Null if the meta did not exist or a valid TSMeta object if it
       * did.
       */
      @Override
      public Deferred<TSMeta> call(final ArrayList<KeyValue> row) throws Exception {
        if (row == null || row.isEmpty()) {
          return Deferred.fromResult(null);
        }
       
        long dps = 0;
        long last_received = 0;
        TSMeta meta = null;
       
        for (KeyValue column : row) {
          if (Arrays.equals(COUNTER_QUALIFIER, column.qualifier())) {
            dps = Bytes.getLong(column.value());
            last_received = column.timestamp() / 1000;
          } else if (Arrays.equals(META_QUALIFIER, column.qualifier())) {
            meta = JSON.parseToObject(column.value(), TSMeta.class);
          }
        }
       
        if (meta == null) {
          LOG.warn("Found a counter TSMeta column without a meta for TSUID: " +
              UniqueId.uidToString(row.get(0).key()));
          return Deferred.fromResult(null);
        }
       
        meta.total_dps = dps;
        meta.last_received = last_received;
        return Deferred.fromResult(meta);
      }
     
    }
   
    final GetRequest get = new GetRequest(tsdb.metaTable(), tsuid);
    get.family(FAMILY);
    get.qualifiers(new byte[][] { COUNTER_QUALIFIER, META_QUALIFIER });
    return tsdb.getClient().get(get).addCallbackDeferring(new GetCB());
  }
 
  /** @return The configured meta data column qualifier byte array*/
  public static byte[] META_QUALIFIER() {
    return META_QUALIFIER;
  }
 
  /** @return The configured counter column qualifier byte array*/
  public static byte[] COUNTER_QUALIFIER() {
    return COUNTER_QUALIFIER;
  }
 
  /** @return The configured meta data family byte array*/
  public static byte[] FAMILY() {
    return FAMILY;
  }
 
  /**
   * Syncs the local object with the stored object for atomic writes,
   * overwriting the stored data if the user issued a PUT request
   * <b>Note:</b> This method also resets the {@code changed} map to false
   * for every field
   * @param meta The stored object to sync from
   * @param overwrite Whether or not all user mutable data in storage should be
   * replaced by the local object
   */
  private void syncMeta(final TSMeta meta, final boolean overwrite) {
    // storage *could* have a missing TSUID if something went pear shaped so
    // only use the one that's configured. If the local is missing, we're foobar
    if (meta.tsuid != null && !meta.tsuid.isEmpty()) {
      tsuid = meta.tsuid;
    }
    if (tsuid == null || tsuid.isEmpty()) {
      throw new IllegalArgumentException("TSUID is empty");
    }
    if (meta.created > 0 && (meta.created < created || created == 0)) {
      created = meta.created;
    }
   
    // handle user-accessible stuff
    if (!overwrite && !changed.get("display_name")) {
      display_name = meta.display_name;
    }
    if (!overwrite && !changed.get("description")) {
      description = meta.description;
    }
    if (!overwrite && !changed.get("notes")) {
      notes = meta.notes;
    }
    if (!overwrite && !changed.get("custom")) {
      custom = meta.custom;
    }
    if (!overwrite && !changed.get("units")) {
      units = meta.units;
    }
    if (!overwrite && !changed.get("data_type")) {
      data_type = meta.data_type;
    }
    if (!overwrite && !changed.get("retention")) {
      retention = meta.retention;
    }
    if (!overwrite && !changed.get("max")) {
      max = meta.max;
    }
    if (!overwrite && !changed.get("min")) {
      min = meta.min;
    }
   
    last_received = meta.last_received;
    total_dps = meta.total_dps;
   
    // reset changed flags
    initializeChangedMap();
  }
 
  /**
   * Sets or resets the changed map flags
   */
  private void initializeChangedMap() {
    // set changed flags
    changed.put("display_name", false);
    changed.put("description", false);
    changed.put("notes", false);
    changed.put("created", false);
    changed.put("custom", false);
    changed.put("units", false);
    changed.put("data_type", false);
    changed.put("retention", false);
    changed.put("max", false);
    changed.put("min", false);
    changed.put("last_received", false);
    changed.put("created", false);
  }
 
  /**
   * Formats the JSON output for writing to storage. It drops objects we don't
   * need or want to store (such as the UIDMeta objects or the total dps) to
   * save space. It also serializes in order so that we can make a proper CAS
   * call. Otherwise the POJO serializer may place the fields in any order
   * and CAS calls would fail all the time.
   * @return A byte array to write to storage
   */
  private byte[] getStorageJSON() {
    // 256 bytes is a good starting value, assumes default info
    final ByteArrayOutputStream output = new ByteArrayOutputStream(256);
    try {
      final JsonGenerator json = JSON.getFactory().createGenerator(output);
      json.writeStartObject();
      json.writeStringField("tsuid", tsuid);
      json.writeStringField("displayName", display_name);
      json.writeStringField("description", description);
      json.writeStringField("notes", notes);
      json.writeNumberField("created", created);
      if (custom == null) {
        json.writeNullField("custom");
      } else {
        json.writeObjectFieldStart("custom");
        for (Map.Entry<String, String> entry : custom.entrySet()) {
          json.writeStringField(entry.getKey(), entry.getValue());
        }
        json.writeEndObject();
      }
      json.writeStringField("units", units);
      json.writeStringField("dataType", data_type);
      json.writeNumberField("retention", retention);
      json.writeNumberField("max", max);
      json.writeNumberField("min", min);
     
      json.writeEndObject();
      json.close();
      return output.toByteArray();
    } catch (IOException e) {
      throw new RuntimeException("Unable to serialize TSMeta", e);
    }
  }
 
  /**
   * Asynchronously loads the UIDMeta objects into the given TSMeta object. Used
   * by multiple methods so it's broken into it's own class here.
   */
  private static class LoadUIDs implements Callback<Deferred<TSMeta>, TSMeta> {

    final private TSDB tsdb;
    final private String tsuid;
   
    public LoadUIDs(final TSDB tsdb, final String tsuid) {
      this.tsdb = tsdb;
      this.tsuid = tsuid;
    }
   
    /**
     * @return A TSMeta object loaded with UIDMetas if successful
     * @throws HBaseException if there was a storage issue
     * @throws JSONException if the data was corrupted
     * @throws NoSuchUniqueName if one of the UIDMeta objects does not exist
     */
    @Override
    public Deferred<TSMeta> call(final TSMeta meta) throws Exception {
      if (meta == null) {
        return Deferred.fromResult(null);
      }
     
      // split up the tags
      final List<byte[]> tags = UniqueId.getTagPairsFromTSUID(tsuid,
          TSDB.metrics_width(), TSDB.tagk_width(), TSDB.tagv_width());
      meta.tags = new ArrayList<UIDMeta>(tags.size());
     
      // initialize with empty objects, otherwise the "set" operations in
      // the callback won't work. Each UIDMeta will be given an index so that
      // the callback can store it in the proper location
      for (int i = 0; i < tags.size(); i++) {
        meta.tags.add(new UIDMeta());
      }
     
      // list of fetch calls that we can wait on for completion
      ArrayList<Deferred<Object>> uid_group =
        new ArrayList<Deferred<Object>>(tags.size() + 1);
     
      /**
       * Callback for each getUIDMeta request that will place the resulting
       * meta data in the proper location. The meta should always be either an
       * actual stored value or a default. On creation, this callback will have
       * an index to associate the UIDMeta with the proper location.
       */
      final class UIDMetaCB implements Callback<Object, UIDMeta> {

        final int index;
       
        public UIDMetaCB(final int index) {
          this.index = index;
        }
       
        /**
         * @return null always since we don't care about the result, just that
         * the callback has completed.
         */
        @Override
        public Object call(final UIDMeta uid_meta) throws Exception {
          if (index < 0) {
            meta.metric = uid_meta;
          } else {
            meta.tags.set(index, uid_meta);
          }
          return null;
        }
       
      }
     
      // for the UIDMeta indexes: -1 means metric, >= 0 means tag. Each
      // getUIDMeta request must be added to the uid_group array so that we
      // can wait for them to complete before returning the TSMeta object,
      // otherwise the caller may get a TSMeta with missing UIDMetas
      uid_group.add(UIDMeta.getUIDMeta(tsdb, UniqueIdType.METRIC,
        tsuid.substring(0, TSDB.metrics_width() * 2)).addCallback(
            new UIDMetaCB(-1)));
     
      int idx = 0;
      for (byte[] tag : tags) {
        if (idx % 2 == 0) {
          uid_group.add(UIDMeta.getUIDMeta(tsdb, UniqueIdType.TAGK, tag)
                .addCallback(new UIDMetaCB(idx)));
        } else {
          uid_group.add(UIDMeta.getUIDMeta(tsdb, UniqueIdType.TAGV, tag)
              .addCallback(new UIDMetaCB(idx)));
        }         
        idx++;
      }
     
      /**
       * Super simple callback that is used to wait on the group of getUIDMeta
       * deferreds so that we return only when all of the UIDMetas have been
       * loaded.
       */
      final class CollateCB implements Callback<Deferred<TSMeta>,
        ArrayList<Object>> {

        @Override
        public Deferred<TSMeta> call(ArrayList<Object> uids) throws Exception {
          return Deferred.fromResult(meta);
        }
       
      }
     
      // start the callback chain by grouping and waiting on all of the UIDMeta
      // deferreds
      return Deferred.group(uid_group).addCallbackDeferring(new CollateCB());
    }
   
  }
 
  // Getters and Setters --------------
 
  /** @return the TSUID as a hex encoded string */
  public final String getTSUID() {
    return tsuid;
  }

  /** @return the metric UID meta object */
  public final UIDMeta getMetric() {
    return metric;
  }

  /** @return the tag UID meta objects in an array, tagk first, then tagv, etc */
  public final List<UIDMeta> getTags() {
    return tags;
  }

  /** @return optional display name */
  public final String getDisplayName() {
    return display_name;
  }

  /** @return optional description */
  public final String getDescription() {
    return description;
  }

  /** @return optional notes */
  public final String getNotes() {
    return notes;
  }

  /** @return when the TSUID was first recorded, Unix epoch */
  public final long getCreated() {
    return created;
  }

  /** @return optional custom key/value map, may be null */
  public final Map<String, String> getCustom() {
    return custom;
  }

  /** @return optional units */
  public final String getUnits() {
    return units;
  }

  /** @return optional data type */
  public final String getDataType() {
    return data_type;
  }

  /** @return optional retention, default of 0 means retain indefinitely */
  public final int getRetention() {
    return retention;
  }

  /** @return optional max value, set by the user */
  public final double getMax() {
    return max;
  }

  /** @return optional min value, set by the user */
  public final double getMin() {
    return min;
  }

  /** @return the last received timestamp, Unix epoch */
  public final long getLastReceived() {
    return last_received;
  }

  /** @return the total number of data points as tracked by the meta data */
  public final long getTotalDatapoints() {
    return this.total_dps;
  }
 
  /** @param display_name an optional name for the timeseries */
  public final void setDisplayName(final String display_name) {
    if (!this.display_name.equals(display_name)) {
      changed.put("display_name", true);
      this.display_name = display_name;
    }
  }

  /** @param description an optional description */
  public final void setDescription(final String description) {
    if (!this.description.equals(description)) {
      changed.put("description", true);
      this.description = description;
    }
  }

  /** @param notes optional notes */
  public final void setNotes(final String notes) {
    if (!this.notes.equals(notes)) {
      changed.put("notes", true);
      this.notes = notes;
    }
  }

  /** @param created the created timestamp Unix epoch in seconds */
  public final void setCreated(final long created) {
    if (this.created != created) {
      changed.put("created", true);
      this.created = created;
    }
  }
 
  /** @param custom optional key/value map */
  public final void setCustom(final Map<String, String> custom) {
    // equivalency of maps is a pain, users have to submit the whole map
    // anyway so we'll just mark it as changed every time we have a non-null
    // value
    if (this.custom != null || custom != null) {
      changed.put("custom", true);
      this.custom = new HashMap<String, String>(custom);
    }
  }

  /** @param units optional units designation */
  public final void setUnits(final String units) {
    if (!this.units.equals(units)) {
      changed.put("units", true);
      this.units = units;
    }
  }

  /** @param data_type optional type of data, e.g. "counter", "gauge" */
  public final void setDataType(final String data_type) {
    if (!this.data_type.equals(data_type)) {
      changed.put("data_type", true);
      this.data_type = data_type;
    }
  }

  /** @param retention optional rentention in days, 0 = indefinite */
  public final void setRetention(final int retention) {
    if (this.retention != retention) {
      changed.put("retention", true);
      this.retention = retention;
    }
  }

  /** @param max optional max value for the timeseries, NaN is the default */
  public final void setMax(final double max) {
    if (this.max != max) {
      changed.put("max", true);
      this.max = max;
    }
  }

  /** @param min optional min value for the timeseries, NaN is the default */
  public final void setMin(final double min) {
    if (this.min != min) {
      changed.put("min", true);
      this.min = min;
    }
  }
}
TOP

Related Classes of net.opentsdb.meta.FetchNewCB

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.