// 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.tsd; import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpResponseStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.stumbleupon.async.Callback; import com.stumbleupon.async.Deferred; import net.opentsdb.core.TSDB; import net.opentsdb.meta.Annotation; import net.opentsdb.uid.UniqueId; import net.opentsdb.utils.DateTime; import net.opentsdb.utils.JSONException; /** * Handles create, update, replace and delete calls for individual annotation * objects. Annotations are stored in the data table alongside data points. * Queries will return annotations along with the data if requested. This RPC * is only used for modifying the individual entries. * @since 2.0 */ final class AnnotationRpc implements HttpRpc { private static final Logger LOG = LoggerFactory.getLogger(AnnotationRpc.class); /** * Performs CRUD methods on individual annotation objects. * @param tsdb The TSD to which we belong * @param query The query to parse and respond to */ public void execute(final TSDB tsdb, HttpQuery query) throws IOException { final HttpMethod method = query.getAPIMethod(); final String[] uri = query.explodeAPIPath(); final String endpoint = uri.length > 1 ? uri[1] : ""; if (endpoint != null && endpoint.toLowerCase().endsWith("bulk")) { executeBulk(tsdb, method, query); return; } final Annotation note; if (query.hasContent()) { note = query.serializer().parseAnnotationV1(); } else { note = parseQS(query); } // GET if (method == HttpMethod.GET) { try { if ("annotations".toLowerCase().equals(uri[0])) { fetchMultipleAnnotations(tsdb, note, query); } else { fetchSingleAnnotation(tsdb, note, query); } } catch (BadRequestException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } // POST } else if (method == HttpMethod.POST || method == HttpMethod.PUT) { /** * Storage callback used to determine if the storage call was successful * or not. Also returns the updated object from storage. */ class SyncCB implements Callback<Deferred<Annotation>, Boolean> { @Override public Deferred<Annotation> call(Boolean success) throws Exception { if (!success) { throw new BadRequestException( HttpResponseStatus.INTERNAL_SERVER_ERROR, "Failed to save the Annotation to storage", "This may be caused by another process modifying storage data"); } return Annotation.getAnnotation(tsdb, note.getTSUID(), note.getStartTime()); } } try { final Deferred<Annotation> process_meta = note.syncToStorage(tsdb, method == HttpMethod.PUT).addCallbackDeferring(new SyncCB()); final Annotation updated_meta = process_meta.joinUninterruptibly(); tsdb.indexAnnotation(note); query.sendReply(query.serializer().formatAnnotationV1(updated_meta)); } catch (IllegalStateException e) { query.sendStatusOnly(HttpResponseStatus.NOT_MODIFIED); } catch (IllegalArgumentException e) { throw new BadRequestException(e); } catch (Exception e) { throw new RuntimeException(e); } // DELETE } else if (method == HttpMethod.DELETE) { try { note.delete(tsdb).joinUninterruptibly(); tsdb.deleteAnnotation(note); } catch (IllegalArgumentException e) { throw new BadRequestException( "Unable to delete Annotation information", e); } catch (Exception e) { throw new RuntimeException(e); } query.sendStatusOnly(HttpResponseStatus.NO_CONTENT); } else { throw new BadRequestException(HttpResponseStatus.METHOD_NOT_ALLOWED, "Method not allowed", "The HTTP method [" + method.getName() + "] is not permitted for this endpoint"); } } /** * Performs CRUD methods on a list of annotation objects to reduce calls to * the API. * @param tsdb The TSD to which we belong * @param method The request method * @param query The query to parse and respond to */ void executeBulk(final TSDB tsdb, final HttpMethod method, HttpQuery query) { if (method == HttpMethod.POST || method == HttpMethod.PUT) { executeBulkUpdate(tsdb, method, query); } else if (method == HttpMethod.DELETE) { executeBulkDelete(tsdb, query); } else { throw new BadRequestException(HttpResponseStatus.METHOD_NOT_ALLOWED, "Method not allowed", "The HTTP method [" + query.method().getName() + "] is not permitted for this endpoint"); } } /** * Performs CRU methods on a list of annotation objects to reduce calls to * the API. Only supports body content and adding or updating annotation * objects. Deletions are separate. * @param tsdb The TSD to which we belong * @param method The request method * @param query The query to parse and respond to */ void executeBulkUpdate(final TSDB tsdb, final HttpMethod method, HttpQuery query) { final List<Annotation> notes; try { notes = query.serializer().parseAnnotationsV1(); } catch (IllegalArgumentException e){ throw new BadRequestException(e); } catch (JSONException e){ throw new BadRequestException(e); } final List<Deferred<Annotation>> callbacks = new ArrayList<Deferred<Annotation>>(notes.size()); /** * Storage callback used to determine if the storage call was successful * or not. Also returns the updated object from storage. */ class SyncCB implements Callback<Deferred<Annotation>, Boolean> { final private Annotation note; public SyncCB(final Annotation note) { this.note = note; } @Override public Deferred<Annotation> call(Boolean success) throws Exception { if (!success) { throw new BadRequestException( HttpResponseStatus.INTERNAL_SERVER_ERROR, "Failed to save an Annotation to storage", "This may be caused by another process modifying storage data: " + note); } return Annotation.getAnnotation(tsdb, note.getTSUID(), note.getStartTime()); } } /** * Simple callback that will index the updated annotation */ class IndexCB implements Callback<Deferred<Annotation>, Annotation> { @Override public Deferred<Annotation> call(final Annotation note) throws Exception { tsdb.indexAnnotation(note); return Deferred.fromResult(note); } } for (Annotation note : notes) { try { Deferred<Annotation> deferred = note.syncToStorage(tsdb, method == HttpMethod.PUT) .addCallbackDeferring(new SyncCB(note)); Deferred<Annotation> indexer = deferred.addCallbackDeferring(new IndexCB()); callbacks.add(indexer); } catch (IllegalStateException e) { LOG.info("No changes for annotation: " + note); } catch (IllegalArgumentException e) { throw new BadRequestException(HttpResponseStatus.BAD_REQUEST, e.getMessage(), "Annotation error: " + note, e); } } try { // wait untill all of the syncs have completed, then rebuild the list // of annotations using the data synced from storage. Deferred.group(callbacks).joinUninterruptibly(); notes.clear(); for (Deferred<Annotation> note : callbacks) { notes.add(note.joinUninterruptibly()); } query.sendReply(query.serializer().formatAnnotationsV1(notes)); } catch (IllegalArgumentException e) { throw new BadRequestException(e); } catch (Exception e) { throw new RuntimeException(e); } } /** * Handles bulk deletions of a range of annotations (local or global) using * query string or body data * @param tsdb The TSD to which we belong * @param query The query to parse and respond to */ void executeBulkDelete(final TSDB tsdb, HttpQuery query) { try { final AnnotationBulkDelete delete_request; if (query.hasContent()) { delete_request = query.serializer().parseAnnotationBulkDeleteV1(); } else { delete_request = parseBulkDeleteQS(query); } // validate the start time on the string. Users could request a timestamp of // 0 to delete all annotations, BUT we don't want them doing that accidentally if (delete_request.start_time == null || delete_request.start_time.isEmpty()) { throw new BadRequestException(HttpResponseStatus.BAD_REQUEST, "Missing the start time value"); } if (!delete_request.global && (delete_request.tsuids == null || delete_request.tsuids.isEmpty())) { throw new BadRequestException(HttpResponseStatus.BAD_REQUEST, "Missing the TSUIDs or global annotations flag"); } final int pre_allocate = delete_request.tsuids != null ? delete_request.tsuids.size() + 1 : 1; List<Deferred<Integer>> deletes = new ArrayList<Deferred<Integer>>(pre_allocate); if (delete_request.global) { deletes.add(Annotation.deleteRange(tsdb, null, delete_request.getStartTime(), delete_request.getEndTime())); } if (delete_request.tsuids != null) { for (String tsuid : delete_request.tsuids) { deletes.add(Annotation.deleteRange(tsdb, UniqueId.stringToUid(tsuid), delete_request.getStartTime(), delete_request.getEndTime())); } } Deferred.group(deletes).joinUninterruptibly(); delete_request.total_deleted = 0; // just in case the caller set it for (Deferred<Integer> count : deletes) { delete_request.total_deleted += count.joinUninterruptibly(); } query.sendReply(query.serializer() .formatAnnotationBulkDeleteV1(delete_request)); } catch (BadRequestException e) { throw e; } catch (IllegalArgumentException e) { throw new BadRequestException(e); } catch (RuntimeException e) { throw new BadRequestException(e); } catch (Exception e) { throw new RuntimeException("Shouldn't be here", e); } } /** * Parses a query string for annotation information. Note that {@code custom} * key/values are not supported via query string. Users must issue a POST or * PUT with content data. * @param query The query to parse * @return An annotation object if parsing was successful * @throws IllegalArgumentException - if the request was malformed */ private Annotation parseQS(final HttpQuery query) { final Annotation note = new Annotation(); final String tsuid = query.getQueryStringParam("tsuid"); if (tsuid != null) { note.setTSUID(tsuid); } final String start = query.getQueryStringParam("start_time"); final Long start_time = DateTime.parseDateTimeString(start, ""); if (start_time < 1) { throw new BadRequestException("Missing start time"); } // TODO - fix for ms support in the future note.setStartTime(start_time / 1000); final String end = query.getQueryStringParam("end_time"); final Long end_time = DateTime.parseDateTimeString(end, ""); // TODO - fix for ms support in the future note.setEndTime(end_time / 1000); final String description = query.getQueryStringParam("description"); if (description != null) { note.setDescription(description); } final String notes = query.getQueryStringParam("notes"); if (notes != null) { note.setNotes(notes); } return note; } private void fetchSingleAnnotation(final TSDB tsdb, final Annotation note, final HttpQuery query) throws Exception { final Annotation stored_annotation = Annotation.getAnnotation(tsdb, note.getTSUID(), note.getStartTime()) .joinUninterruptibly(); if (stored_annotation == null) { throw new BadRequestException(HttpResponseStatus.NOT_FOUND, "Unable to locate annotation in storage"); } query.sendReply(query.serializer().formatAnnotationV1(stored_annotation)); } private void fetchMultipleAnnotations(final TSDB tsdb, final Annotation note, final HttpQuery query) throws Exception { if (note.getEndTime() == 0) { note.setEndTime(System.currentTimeMillis()); } final List<Annotation> annotations = Annotation.getGlobalAnnotations(tsdb, note.getStartTime(), note.getEndTime()) .joinUninterruptibly(); if (annotations == null) { throw new BadRequestException(HttpResponseStatus.NOT_FOUND, "Unable to locate annotations in storage"); } query.sendReply(query.serializer().formatAnnotationsV1(annotations)); } /** * Parses a query string for a bulk delet request * @param query The query to parse * @return A bulk delete query */ private AnnotationBulkDelete parseBulkDeleteQS(final HttpQuery query) { final AnnotationBulkDelete settings = new AnnotationBulkDelete(); settings.start_time = query.getRequiredQueryStringParam("start_time"); settings.end_time = query.getQueryStringParam("end_time"); if (query.hasQueryStringParam("tsuids")) { String[] tsuids = query.getQueryStringParam("tsuids").split(","); settings.tsuids = new ArrayList<String>(tsuids.length); for (String tsuid : tsuids) { settings.tsuids.add(tsuid.trim()); } } if (query.hasQueryStringParam("global")) { settings.global = true; } return settings; } /** * Represents a bulk annotation delete query. Either one or more TSUIDs must * be supplied or the global flag can be set to determine what annotations * are purged. Both may be set in one request. Annotations for the time * between and including the start and end times will be removed based on * the annotation's recorded start time. */ public static class AnnotationBulkDelete { /** The start time, may be relative, absolute or unixy */ private String start_time; /** An option end time. If not set, current time is used */ private String end_time; /** Optional list of TSUIDs */ private List<String> tsuids; /** Optional flag to determine whether global notes for the range should be * purged */ private boolean global; /** Total number of items deleted (for later response to the user) */ private long total_deleted; /** * Default ctor for Jackson */ public AnnotationBulkDelete() { } /** @return The start timestamp in milliseconds */ public long getStartTime() { return DateTime.parseDateTimeString(start_time, null); } /** @return The ending timestamp in milliseconds. If it wasn't set, the * current time is returned */ public long getEndTime() { if (end_time == null || end_time.isEmpty()) { return System.currentTimeMillis(); } return DateTime.parseDateTimeString(end_time, null); } /** @return List of TSUIDs to delete annotations for (may be NULL) */ public List<String> getTsuids() { return tsuids; } /** @return Whether or not global annotations for the span should be purged */ public boolean getGlobal() { return global; } /** @return The total number of annotations matched and deleted */ public long getTotalDeleted() { return total_deleted; } /** @param start_time Start time for the range. May be relative, absolute * or unixy in seconds or milliseconds */ public void setStartTime(String start_time) { this.start_time = start_time; } /** @param end_time Optional end time to set for the range. Similar to start */ public void setEndTime(String end_time) { this.end_time = end_time; } /** @param tsuids A list of TSUIDs to scan for annotations */ public void setTsuids(List<String> tsuids) { this.tsuids = tsuids; } /** @param global Whether or not to delete global annotations for the range */ public void setGlobal(boolean global) { this.global = global; } /** @param total_deleted Total number of annotations deleted */ public void setTotalDeleted(long total_deleted) { this.total_deleted = total_deleted; } } }