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, <timestamp>] 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