1 // This file is part of OpenTSDB.
2 // Copyright (C) 2013  The OpenTSDB Authors.
3 //
4 // This program is free software: you can redistribute it and/or modify it
5 // under the terms of the GNU Lesser General Public License as published by
6 // the Free Software Foundation, either version 2.1 of the License, or (at your
7 // option) any later version.  This program is distributed in the hope that it
8 // will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
9 // of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser
10 // General Public License for more details.  You should have received a copy
11 // of the GNU Lesser General Public License along with this program.  If not,
12 // see <http://www.gnu.org/licenses/>.
13 package net.opentsdb.meta;
14 
15 import java.io.ByteArrayOutputStream;
16 import java.io.IOException;
17 import java.nio.charset.Charset;
18 import java.util.ArrayList;
19 import java.util.HashMap;
20 import java.util.List;
21 import java.util.Map;
22 import java.util.TreeMap;
23 
24 import org.hbase.async.Bytes;
25 import org.hbase.async.DeleteRequest;
26 import org.hbase.async.GetRequest;
27 import org.hbase.async.HBaseException;
28 import org.hbase.async.KeyValue;
29 import org.hbase.async.PutRequest;
30 import org.hbase.async.Scanner;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33 
34 import net.opentsdb.core.Const;
35 import net.opentsdb.core.Internal;
36 import net.opentsdb.core.RowKey;
37 import net.opentsdb.core.TSDB;
38 import net.opentsdb.uid.UniqueId;
39 import net.opentsdb.utils.JSON;
40 import net.opentsdb.utils.JSONException;
41 
42 import com.fasterxml.jackson.annotation.JsonAutoDetect;
43 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
44 import com.fasterxml.jackson.annotation.JsonInclude;
45 import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
46 import com.fasterxml.jackson.annotation.JsonInclude.Include;
47 import com.fasterxml.jackson.core.JsonGenerator;
48 import com.google.common.annotations.VisibleForTesting;
49 import com.stumbleupon.async.Callback;
50 import com.stumbleupon.async.Deferred;
51 
52 /**
53  * Annotations are used to record time-based notes about timeseries events.
54  * Every note must have an associated start_time as that determines
55  * where the note is stored.
56  * <p>
57  * Annotations may be associated with a specific timeseries, in which case
58  * the tsuid must be configured with a valid TSUID. If no TSUID
59  * is provided, the annotation is considered a "global" note that applies
60  * to everything stored in OpenTSDB. Global annotations are stored in the rows
61  * [ 0, 0, 0, &lt;timestamp&gt;] in the same manner as local annotations and
62  * timeseries data.
63  * <p>
64  * The description field should store a very brief line of information
65  * about the event. GUIs can display the description in their "main" view
66  * where multiple annotations may appear. Users of the GUI could then click
67  * or hover over the description for more detail including the {@link #notes}
68  * field.
69  * <p>
70  * Custom data can be stored in the custom hash map for user
71  * specific information. For example, you could add a "reporter" key
72  * with the name of the person who recorded the note.
73  * @since 2.0
74  */
75 @JsonAutoDetect(fieldVisibility = Visibility.PUBLIC_ONLY)
76 @JsonInclude(Include.NON_NULL)
77 @JsonIgnoreProperties(ignoreUnknown = true)
78 public final class Annotation implements Comparable<Annotation> {
79   private static final Logger LOG = LoggerFactory.getLogger(Annotation.class);
80 
81   /** Charset used to convert Strings to byte arrays and back. */
82   private static final Charset CHARSET = Charset.forName("ISO-8859-1");
83 
84   /** Byte used for the qualifier prefix to indicate this is an annotation */
85   private static final byte PREFIX = 0x01;
86 
87   /** The single column family used by this class. */
88   private static final byte[] FAMILY = "t".getBytes(CHARSET);
89 
90   /** If the note is associated with a timeseries, represents the ID */
91   private String tsuid = "";
92 
93   /** The start timestamp associated wit this note in seconds or ms */
94   private long start_time = 0;
95 
96   /** Optional end time if the note represents an event that was resolved */
97   private long end_time = 0;
98 
99   /** A short description of the event, displayed in GUIs */
100   private String description = "";
101 
102   /** A detailed accounting of the event or note */
103   private String notes = "";
104 
105   /** Optional user supplied key/values */
106   private HashMap<String, String> custom = null;
107 
108   /** Tracks fields that have changed by the user to avoid overwrites */
109   private final HashMap<String, Boolean> changed =
110     new HashMap<String, Boolean>();
111 
112   /**
113    * Default constructor, initializes the change map
114    */
Annotation()115   public Annotation() {
116     initializeChangedMap();
117   }
118 
119   /** @return A string with information about the annotation object */
120   @Override
toString()121   public String toString() {
122     return "TSUID: " + tsuid + " Start: " + start_time + "  Description: " +
123       description;
124   }
125 
126   /**
127    * Compares the {@code #start_time} of this annotation to the given note
128    * @return 1 if the local start time is greater, -1 if it's less or 0 if
129    * equal
130    */
131   @Override
compareTo(Annotation note)132   public int compareTo(Annotation note) {
133     return start_time > note.start_time ? 1 :
134       start_time < note.start_time ? -1 : 0;
135   }
136 
137   /**
138    * Attempts a CompareAndSet storage call, loading the object from storage,
139    * synchronizing changes, and attempting a put.
140    * <b>Note:</b> If the local object didn't have any fields set by the caller
141    * or there weren't any changes, then the data will not be written and an
142    * exception will be thrown.
143    * @param tsdb The TSDB to use for storage access
144    * @param overwrite When the RPC method is PUT, will overwrite all user
145    * accessible fields
146    * True if the storage call was successful, false if the object was
147    * modified in storage during the CAS call. If false, retry the call. Other
148    * failures will result in an exception being thrown.
149    * @throws HBaseException if there was an issue
150    * @throws IllegalArgumentException if required data was missing such as the
151    * {@code #start_time}
152    * @throws IllegalStateException if the data hasn't changed. This is OK!
153    * @throws JSONException if the object could not be serialized
154    */
syncToStorage(final TSDB tsdb, final Boolean overwrite)155   public Deferred<Boolean> syncToStorage(final TSDB tsdb,
156       final Boolean overwrite) {
157     if (start_time < 1) {
158       throw new IllegalArgumentException("The start timestamp has not been set");
159     }
160 
161     boolean has_changes = false;
162     for (Map.Entry<String, Boolean> entry : changed.entrySet()) {
163       if (entry.getValue()) {
164         has_changes = true;
165         break;
166       }
167     }
168     if (!has_changes) {
169       LOG.debug(this + " does not have changes, skipping sync to storage");
170       throw new IllegalStateException("No changes detected in Annotation data");
171     }
172 
173     final class StoreCB implements Callback<Deferred<Boolean>, Annotation> {
174 
175       @Override
176       public Deferred<Boolean> call(final Annotation stored_note)
177         throws Exception {
178         final byte[] original_note = stored_note == null ? new byte[0] :
179           stored_note.getStorageJSON();
180 
181         if (stored_note != null) {
182           Annotation.this.syncNote(stored_note, overwrite);
183         }
184 
185         final byte[] tsuid_byte = tsuid != null && !tsuid.isEmpty() ?
186             UniqueId.stringToUid(tsuid) : null;
187         final PutRequest put = new PutRequest(tsdb.dataTable(),
188             getRowKey(start_time, tsuid_byte), FAMILY,
189             getQualifier(start_time),
190             Annotation.this.getStorageJSON());
191         return tsdb.getClient().compareAndSet(put, original_note);
192       }
193 
194     }
195 
196     if (tsuid != null && !tsuid.isEmpty()) {
197       return getAnnotation(tsdb, UniqueId.stringToUid(tsuid), start_time)
198         .addCallbackDeferring(new StoreCB());
199     }
200     return getAnnotation(tsdb, start_time).addCallbackDeferring(new StoreCB());
201   }
202 
203   /**
204    * Attempts to mark an Annotation object for deletion. Note that if the
205    * annoation does not exist in storage, this delete call will not throw an
206    * error.
207    * @param tsdb The TSDB to use for storage access
208    * @return A meaningless Deferred for the caller to wait on until the call is
209    * complete. The value may be null.
210    */
delete(final TSDB tsdb)211   public Deferred<Object> delete(final TSDB tsdb) {
212     if (start_time < 1) {
213       throw new IllegalArgumentException("The start timestamp has not been set");
214     }
215 
216     final byte[] tsuid_byte = tsuid != null && !tsuid.isEmpty() ?
217         UniqueId.stringToUid(tsuid) : null;
218     final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(),
219         getRowKey(start_time, tsuid_byte), FAMILY,
220         getQualifier(start_time));
221     return tsdb.getClient().delete(delete);
222   }
223 
224   /**
225    * Attempts to fetch a global annotation from storage
226    * @param tsdb The TSDB to use for storage access
227    * @param start_time The start time as a Unix epoch timestamp
228    * @return A valid annotation object if found, null if not
229    */
getAnnotation(final TSDB tsdb, final long start_time)230   public static Deferred<Annotation> getAnnotation(final TSDB tsdb,
231       final long start_time) {
232     return getAnnotation(tsdb, (byte[])null, start_time);
233   }
234 
235   /**
236    * Attempts to fetch a global or local annotation from storage
237    * @param tsdb The TSDB to use for storage access
238    * @param tsuid The TSUID as a string. May be empty if retrieving a global
239    * annotation
240    * @param start_time The start time as a Unix epoch timestamp
241    * @return A valid annotation object if found, null if not
242    */
getAnnotation(final TSDB tsdb, final String tsuid, final long start_time)243   public static Deferred<Annotation> getAnnotation(final TSDB tsdb,
244       final String tsuid, final long start_time) {
245     if (tsuid != null && !tsuid.isEmpty()) {
246       return getAnnotation(tsdb, UniqueId.stringToUid(tsuid), start_time);
247     }
248     return getAnnotation(tsdb, (byte[])null, start_time);
249   }
250 
251   /**
252    * Attempts to fetch a global or local annotation from storage
253    * @param tsdb The TSDB to use for storage access
254    * @param tsuid The TSUID as a byte array. May be null if retrieving a global
255    * annotation
256    * @param start_time The start time as a Unix epoch timestamp
257    * @return A valid annotation object if found, null if not
258    */
getAnnotation(final TSDB tsdb, final byte[] tsuid, final long start_time)259   public static Deferred<Annotation> getAnnotation(final TSDB tsdb,
260       final byte[] tsuid, final long start_time) {
261 
262     /**
263      * Called after executing the GetRequest to parse the meta data.
264      */
265     final class GetCB implements Callback<Deferred<Annotation>,
266       ArrayList<KeyValue>> {
267 
268       /**
269        * @return Null if the meta did not exist or a valid Annotation object if
270        * it did.
271        */
272       @Override
273       public Deferred<Annotation> call(final ArrayList<KeyValue> row)
274         throws Exception {
275         if (row == null || row.isEmpty()) {
276           return Deferred.fromResult(null);
277         }
278 
279         Annotation note = JSON.parseToObject(row.get(0).value(),
280             Annotation.class);
281         return Deferred.fromResult(note);
282       }
283 
284     }
285 
286     final GetRequest get = new GetRequest(tsdb.dataTable(),
287         getRowKey(start_time, tsuid));
288     get.family(FAMILY);
289     get.qualifier(getQualifier(start_time));
290     return tsdb.getClient().get(get).addCallbackDeferring(new GetCB());
291   }
292 
293   /**
294    * Scans through the global annotation storage rows and returns a list of
295    * parsed annotation objects. If no annotations were found for the given
296    * timespan, the resulting list will be empty.
297    * @param tsdb The TSDB to use for storage access
298    * @param start_time Start time to scan from. May be 0
299    * @param end_time End time to scan to. Must be greater than 0
300    * @return A list with detected annotations. May be empty.
301    * @throws IllegalArgumentException if the end timestamp has not been set or
302    * the end time is less than the start time
303    */
getGlobalAnnotations(final TSDB tsdb, final long start_time, final long end_time)304   public static Deferred<List<Annotation>> getGlobalAnnotations(final TSDB tsdb,
305       final long start_time, final long end_time) {
306     if (end_time < 1) {
307       throw new IllegalArgumentException("The end timestamp has not been set");
308     }
309     if (end_time < start_time) {
310       throw new IllegalArgumentException(
311           "The end timestamp cannot be less than the start timestamp");
312     }
313 
314     /**
315      * Scanner that loops through the [0, 0, 0, timestamp] rows looking for
316      * global annotations. Returns a list of parsed annotation objects.
317      * The list may be empty.
318      */
319     final class ScannerCB implements Callback<Deferred<List<Annotation>>,
320       ArrayList<ArrayList<KeyValue>>> {
321       final Scanner scanner;
322       final ArrayList<Annotation> annotations = new ArrayList<Annotation>();
323 
324       /**
325        * Initializes the scanner
326        */
327       public ScannerCB() {
328         final byte[] start = new byte[Const.SALT_WIDTH() +
329                                       TSDB.metrics_width() +
330                                       Const.TIMESTAMP_BYTES];
331         final byte[] end = new byte[Const.SALT_WIDTH() +
332                                     TSDB.metrics_width() +
333                                     Const.TIMESTAMP_BYTES];
334 
335         final long normalized_start = (start_time -
336             (start_time % Const.MAX_TIMESPAN));
337         final long normalized_end = (end_time -
338             (end_time % Const.MAX_TIMESPAN) + Const.MAX_TIMESPAN);
339 
340         Bytes.setInt(start, (int) normalized_start,
341             Const.SALT_WIDTH() + TSDB.metrics_width());
342         Bytes.setInt(end, (int) normalized_end,
343             Const.SALT_WIDTH() + TSDB.metrics_width());
344 
345         scanner = tsdb.getClient().newScanner(tsdb.dataTable());
346         scanner.setStartKey(start);
347         scanner.setStopKey(end);
348         scanner.setFamily(FAMILY);
349       }
350 
351       public Deferred<List<Annotation>> scan() {
352         return scanner.nextRows().addCallbackDeferring(this);
353       }
354 
355       @Override
356       public Deferred<List<Annotation>> call (
357           final ArrayList<ArrayList<KeyValue>> rows) throws Exception {
358         if (rows == null || rows.isEmpty()) {
359           return Deferred.fromResult((List<Annotation>)annotations);
360         }
361 
362         for (final ArrayList<KeyValue> row : rows) {
363           for (KeyValue column : row) {
364             if ((column.qualifier().length == 3 || column.qualifier().length == 5)
365                 && column.qualifier()[0] == PREFIX()) {
366               Annotation note = JSON.parseToObject(column.value(),
367                   Annotation.class);
368               if (note.start_time < start_time || note.end_time > end_time) {
369                 continue;
370               }
371               annotations.add(note);
372             }
373           }
374         }
375 
376         return scan();
377       }
378 
379     }
380 
381     return new ScannerCB().scan();
382   }
383 
384   /**
385    * Deletes global or TSUID associated annotiations for the given time range.
386    * @param tsdb The TSDB object to use for storage access
387    * @param tsuid An optional TSUID. If set to null, then global annotations for
388    * the given range will be deleted
389    * @param start_time A start timestamp in milliseconds
390    * @param end_time An end timestamp in millseconds
391    * @return The number of annotations deleted
392    * @throws IllegalArgumentException if the timestamps are invalid
393    * @since 2.1
394    */
deleteRange(final TSDB tsdb, final byte[] tsuid, final long start_time, final long end_time)395   public static Deferred<Integer> deleteRange(final TSDB tsdb,
396       final byte[] tsuid, final long start_time, final long end_time) {
397     if (end_time < 1) {
398       throw new IllegalArgumentException("The end timestamp has not been set");
399     }
400     if (end_time < start_time) {
401       throw new IllegalArgumentException(
402           "The end timestamp cannot be less than the start timestamp");
403     }
404 
405     final List<Deferred<Object>> delete_requests = new ArrayList<Deferred<Object>>();
406     int width = tsuid != null ?
407         Const.SALT_WIDTH() + tsuid.length + Const.TIMESTAMP_BYTES :
408       Const.SALT_WIDTH() + TSDB.metrics_width() + Const.TIMESTAMP_BYTES;
409     final byte[] start_row = new byte[width];
410     final byte[] end_row = new byte[width];
411 
412     // downsample to seconds for the row keys
413     final long start = start_time / 1000;
414     final long end = end_time / 1000;
415     final long normalized_start = (start - (start % Const.MAX_TIMESPAN));
416     final long normalized_end = (end - (end % Const.MAX_TIMESPAN) + Const.MAX_TIMESPAN);
417     Bytes.setInt(start_row, (int) normalized_start,
418         Const.SALT_WIDTH() + TSDB.metrics_width());
419     Bytes.setInt(end_row, (int) normalized_end,
420         Const.SALT_WIDTH() + TSDB.metrics_width());
421 
422     if (tsuid != null) {
423       // first copy the metric UID then the tags
424       System.arraycopy(tsuid, 0, start_row, Const.SALT_WIDTH(), TSDB.metrics_width());
425       System.arraycopy(tsuid, 0, end_row, Const.SALT_WIDTH(), TSDB.metrics_width());
426       width = Const.SALT_WIDTH() + TSDB.metrics_width() + Const.TIMESTAMP_BYTES;
427       final int remainder = tsuid.length - TSDB.metrics_width();
428       System.arraycopy(tsuid, TSDB.metrics_width(), start_row, width, remainder);
429       System.arraycopy(tsuid, TSDB.metrics_width(), end_row, width, remainder);
430     }
431 
432     /**
433      * Iterates through the scanner results in an asynchronous manner, returning
434      * once the scanner returns a null result set.
435      */
436     final class ScannerCB implements Callback<Deferred<List<Deferred<Object>>>,
437         ArrayList<ArrayList<KeyValue>>> {
438       final Scanner scanner;
439 
440       public ScannerCB() {
441         scanner = tsdb.getClient().newScanner(tsdb.dataTable());
442         scanner.setStartKey(start_row);
443         scanner.setStopKey(end_row);
444         scanner.setFamily(FAMILY);
445         if (tsuid != null) {
446           final List<String> tsuids = new ArrayList<String>(1);
447           tsuids.add(UniqueId.uidToString(tsuid));
448           Internal.createAndSetTSUIDFilter(scanner, tsuids);
449         }
450       }
451 
452       public Deferred<List<Deferred<Object>>> scan() {
453         return scanner.nextRows().addCallbackDeferring(this);
454       }
455 
456       @Override
457       public Deferred<List<Deferred<Object>>> call (
458           final ArrayList<ArrayList<KeyValue>> rows) throws Exception {
459         if (rows == null || rows.isEmpty()) {
460           return Deferred.fromResult(delete_requests);
461         }
462 
463         for (final ArrayList<KeyValue> row : rows) {
464           final long base_time = Internal.baseTime(tsdb, row.get(0).key());
465           for (KeyValue column : row) {
466             if ((column.qualifier().length == 3 || column.qualifier().length == 5)
467                 && column.qualifier()[0] == PREFIX()) {
468               final long timestamp = timeFromQualifier(column.qualifier(),
469                   base_time);
470               if (timestamp < start_time || timestamp > end_time) {
471                 continue;
472               }
473               final DeleteRequest delete = new DeleteRequest(tsdb.dataTable(),
474                   column.key(), FAMILY, column.qualifier());
475               delete_requests.add(tsdb.getClient().delete(delete));
476             }
477           }
478         }
479         return scan();
480       }
481     }
482 
483     /** Called when the scanner is done. Delete requests may still be pending */
484     final class ScannerDoneCB implements Callback<Deferred<ArrayList<Object>>,
485       List<Deferred<Object>>> {
486       @Override
487       public Deferred<ArrayList<Object>> call(final List<Deferred<Object>> deletes)
488           throws Exception {
489         return Deferred.group(delete_requests);
490       }
491     }
492 
493     /** Waits on the group of deferreds to complete before returning the count */
494     final class GroupCB implements Callback<Deferred<Integer>, ArrayList<Object>> {
495       @Override
496       public Deferred<Integer> call(final ArrayList<Object> deletes)
497           throws Exception {
498         return Deferred.fromResult(deletes.size());
499       }
500     }
501 
502     Deferred<ArrayList<Object>> scanner_done = new ScannerCB().scan()
503         .addCallbackDeferring(new ScannerDoneCB());
504     return scanner_done.addCallbackDeferring(new GroupCB());
505   }
506 
507   /** @return The prefix byte for annotation objects */
PREFIX()508   public static byte PREFIX() {
509     return PREFIX;
510   }
511 
512   /**
513    * Serializes the object in a uniform matter for storage. Needed for
514    * successful CAS calls
515    * @return The serialized object as a byte array
516    */
517   @VisibleForTesting
getStorageJSON()518   byte[] getStorageJSON() {
519     // TODO - precalculate size
520     final ByteArrayOutputStream output = new ByteArrayOutputStream();
521     try {
522       final JsonGenerator json = JSON.getFactory().createGenerator(output);
523       json.writeStartObject();
524       if (tsuid != null && !tsuid.isEmpty()) {
525         json.writeStringField("tsuid", tsuid);
526       }
527       json.writeNumberField("startTime", start_time);
528       json.writeNumberField("endTime", end_time);
529       json.writeStringField("description", description);
530       json.writeStringField("notes", notes);
531       if (custom == null) {
532         json.writeNullField("custom");
533       } else {
534         final TreeMap<String, String> sorted_custom =
535             new TreeMap<String, String>(custom);
536         json.writeObjectField("custom", sorted_custom);
537       }
538 
539       json.writeEndObject();
540       json.close();
541       return output.toByteArray();
542     } catch (IOException e) {
543       throw new RuntimeException("Unable to serialize Annotation", e);
544     }
545   }
546 
547   /**
548    * Syncs the local object with the stored object for atomic writes,
549    * overwriting the stored data if the user issued a PUT request
550    * <b>Note:</b> This method also resets the {@code changed} map to false
551    * for every field
552    * @param meta The stored object to sync from
553    * @param overwrite Whether or not all user mutable data in storage should be
554    * replaced by the local object
555    */
syncNote(final Annotation note, final boolean overwrite)556   private void syncNote(final Annotation note, final boolean overwrite) {
557     if (note.start_time > 0 && (note.start_time < start_time || start_time == 0)) {
558       start_time = note.start_time;
559     }
560 
561     // handle user-accessible stuff
562     if (!overwrite && !changed.get("end_time")) {
563       end_time = note.end_time;
564     }
565     if (!overwrite && !changed.get("description")) {
566       description = note.description;
567     }
568     if (!overwrite && !changed.get("notes")) {
569       notes = note.notes;
570     }
571     if (!overwrite && !changed.get("custom")) {
572       custom = note.custom;
573     }
574 
575     // reset changed flags
576     initializeChangedMap();
577   }
578 
579   /**
580    * Sets or resets the changed map flags
581    */
initializeChangedMap()582   private void initializeChangedMap() {
583     // set changed flags
584     changed.put("end_time", false);
585     changed.put("description", false);
586     changed.put("notes", false);
587     changed.put("custom", false);
588   }
589 
590   /**
591    * Calculates and returns the column qualifier. The qualifier is the offset
592    * of the {@code #start_time} from the row key's base time stamp in seconds
593    * with a prefix of {@code #PREFIX}. Thus if the offset is 0 and the prefix is
594    * 1 and the timestamp is in seconds, the qualifier would be [1, 0, 0].
595    * Millisecond timestamps will have a 5 byte qualifier
596    * @return The column qualifier as a byte array
597    * @throws IllegalArgumentException if the start_time has not been set
598    */
getQualifier(final long start_time)599   private static byte[] getQualifier(final long start_time) {
600     if (start_time < 1) {
601       throw new IllegalArgumentException("The start timestamp has not been set");
602     }
603 
604     final long base_time;
605     final byte[] qualifier;
606     long timestamp = start_time;
607     // downsample to seconds to save space AND prevent duplicates if the time
608     // is on a second boundary (e.g. if someone posts at 1328140800 with value A
609     // and 1328140800000L with value B)
610     if (timestamp % 1000 == 0) {
611       timestamp = timestamp / 1000;
612     }
613 
614     if ((timestamp & Const.SECOND_MASK) != 0) {
615       // drop the ms timestamp to seconds to calculate the base timestamp
616       base_time = ((timestamp / 1000) -
617           ((timestamp / 1000) % Const.MAX_TIMESPAN));
618       qualifier = new byte[5];
619       final int offset = (int) (timestamp - (base_time * 1000));
620       System.arraycopy(Bytes.fromInt(offset), 0, qualifier, 1, 4);
621     } else {
622       base_time = (timestamp - (timestamp % Const.MAX_TIMESPAN));
623       qualifier = new byte[3];
624       final short offset = (short) (timestamp - base_time);
625       System.arraycopy(Bytes.fromShort(offset), 0, qualifier, 1, 2);
626     }
627     qualifier[0] = PREFIX;
628     return qualifier;
629   }
630 
631   /**
632    * Returns a timestamp after parsing an annotation qualifier.
633    * @param qualifier The full qualifier (including prefix) on either 3 or 5 bytes
634    * @param base_time The base time from the row in seconds
635    * @return A timestamp in milliseconds
636    * @since 2.1
637    */
timeFromQualifier(final byte[] qualifier, final long base_time)638   private static long timeFromQualifier(final byte[] qualifier,
639       final long base_time) {
640     final long offset;
641     if (qualifier.length == 3) {
642       offset = Bytes.getUnsignedShort(qualifier, 1);
643       return (base_time + offset) * 1000;
644     } else {
645       offset = Bytes.getUnsignedInt(qualifier, 1);
646       return (base_time * 1000) + offset;
647     }
648   }
649 
650   /**
651    * Calculates the row key based on the TSUID and the start time. If the TSUID
652    * is empty, the row key is a 0 filled byte array {@code TSDB.metrics_width()}
653    * wide plus the normalized start timestamp without any tag bytes.
654    * @param start_time The start time as a Unix epoch timestamp
655    * @param tsuid An optional TSUID if storing a local annotation
656    * @return The row key as a byte array
657    */
getRowKey(final long start_time, final byte[] tsuid)658   private static byte[] getRowKey(final long start_time, final byte[] tsuid) {
659     if (start_time < 1) {
660       throw new IllegalArgumentException("The start timestamp has not been set");
661     }
662 
663     final long base_time;
664     if ((start_time & Const.SECOND_MASK) != 0) {
665       // drop the ms timestamp to seconds to calculate the base timestamp
666       base_time = ((start_time / 1000) -
667           ((start_time / 1000) % Const.MAX_TIMESPAN));
668     } else {
669       base_time = (start_time - (start_time % Const.MAX_TIMESPAN));
670     }
671 
672     // if the TSUID is empty, then we're a global annotation. The row key will
673     // just be an empty byte array of metric width plus the timestamp. We also
674     // don't salt the global row key (though it has space for salts)
675     if (tsuid == null || tsuid.length < 1) {
676       final byte[] row = new byte[Const.SALT_WIDTH() +
677                                   TSDB.metrics_width() + Const.TIMESTAMP_BYTES];
678       Bytes.setInt(row, (int) base_time, Const.SALT_WIDTH() + TSDB.metrics_width());
679       return row;
680     }
681 
682     // otherwise we need to build the row key from the TSUID and start time
683     final byte[] row = new byte[Const.SALT_WIDTH() + Const.TIMESTAMP_BYTES +
684                                 tsuid.length];
685     System.arraycopy(tsuid, 0, row, Const.SALT_WIDTH(), TSDB.metrics_width());
686     Bytes.setInt(row, (int) base_time, Const.SALT_WIDTH() + TSDB.metrics_width());
687     System.arraycopy(tsuid, TSDB.metrics_width(), row,
688         Const.SALT_WIDTH() + TSDB.metrics_width() + Const.TIMESTAMP_BYTES,
689         (tsuid.length - TSDB.metrics_width()));
690     RowKey.prefixKeyWithSalt(row);
691     return row;
692   }
693 
694 // Getters and Setters --------------
695 
696   /** @return the tsuid, may be empty if this is a global annotation */
getTSUID()697   public final String getTSUID() {
698     return tsuid;
699   }
700 
701   /** @return the start_time */
getStartTime()702   public final long getStartTime() {
703     return start_time;
704   }
705 
706   /**  @return the end_time, may be 0 */
getEndTime()707   public final long getEndTime() {
708     return end_time;
709   }
710 
711   /** @return the description */
getDescription()712   public final String getDescription() {
713     return description;
714   }
715 
716   /** @return the notes, may be empty */
getNotes()717   public final String getNotes() {
718     return notes;
719   }
720 
721   /** @return the custom key/value map, may be null */
getCustom()722   public final Map<String, String> getCustom() {
723     return custom;
724   }
725 
726   /** @param tsuid the tsuid to store*/
setTSUID(final String tsuid)727   public void setTSUID(final String tsuid) {
728     this.tsuid = tsuid;
729   }
730 
731   /** @param start_time the start_time, required for every annotation */
setStartTime(final long start_time)732   public void setStartTime(final long start_time) {
733     this.start_time = start_time;
734   }
735 
736   /** @param end_time the end_time, optional*/
setEndTime(final long end_time)737   public void setEndTime(final long end_time) {
738     if (this.end_time != end_time) {
739       this.end_time = end_time;
740       changed.put("end_time", true);
741     }
742   }
743 
744   /** @param description the description, required for every annotation */
setDescription(final String description)745   public void setDescription(final String description) {
746     if (!this.description.equals(description)) {
747       this.description = description;
748       changed.put("description", true);
749     }
750   }
751 
752   /** @param notes the notes to set */
setNotes(final String notes)753   public void setNotes(final String notes) {
754     if (!this.notes.equals(notes)) {
755       this.notes = notes;
756       changed.put("notes", true);
757     }
758   }
759 
760   /** @param custom the custom key/value map */
setCustom(final Map<String, String> custom)761   public void setCustom(final Map<String, String> custom) {
762     // equivalency of maps is a pain, users have to submit the whole map
763     // anyway so we'll just mark it as changed every time we have a non-null
764     // value
765     if (this.custom != null || custom != null) {
766       changed.put("custom", true);
767       this.custom = new HashMap<String, String>(custom);
768     }
769   }
770 }
771