/************************************************************************* * Copyright 2009-2013 Eucalyptus Systems, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3 of the License. * * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see http://www.gnu.org/licenses/. * * Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta * CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need * additional information or have any questions. ************************************************************************/ package com.eucalyptus.cloudwatch.common.internal.domain.listmetrics; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import javax.annotation.Nullable; import javax.persistence.EntityTransaction; import com.eucalyptus.cloudwatch.common.internal.domain.InvalidTokenException; import com.eucalyptus.cloudwatch.common.internal.domain.metricdata.MetricEntity; import com.eucalyptus.configurable.ConfigurableClass; import com.eucalyptus.configurable.ConfigurableField; import com.eucalyptus.entities.TransactionResource; import com.google.common.collect.Iterables; import com.google.common.collect.LinkedListMultimap; import com.google.common.collect.Multimap; import org.apache.log4j.Logger; import org.hibernate.Criteria; import org.hibernate.criterion.Disjunction; import org.hibernate.criterion.Restrictions; import com.eucalyptus.cloudwatch.common.internal.domain.DimensionEntity; import com.eucalyptus.cloudwatch.common.internal.domain.NextTokenUtils; import com.eucalyptus.cloudwatch.common.internal.domain.metricdata.MetricEntity.MetricType; import com.eucalyptus.cloudwatch.common.internal.domain.metricdata.SimpleMetricEntity; import com.eucalyptus.entities.Entities; import com.eucalyptus.records.Logs; import com.google.common.base.Predicate; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; public class ListMetricManager { public static volatile Integer LIST_METRIC_NUM_DB_OPERATIONS_PER_TRANSACTION = 10000; public static volatile Integer LIST_METRIC_NUM_DB_OPERATIONS_UNTIL_SESSION_FLUSH = 50; private static final Logger LOG = Logger.getLogger(ListMetricManager.class); public static void addMetric(String accountId, String metricName, String namespace, Map<String, String> dimensionMap, MetricType metricType) { try (final TransactionResource db = Entities.transactionFor(ListMetric.class)) { addMetric(db, accountId, metricName, namespace, dimensionMap, metricType); db.commit(); } } private static ListMetric createMetric(String accountId, String metricName, String namespace, Map<String, String> dimensionMap, MetricType metricType) { if (dimensionMap == null) { dimensionMap = new HashMap<String, String>(); } else if (dimensionMap.size() > ListMetric.MAX_DIM_NUM) { throw new IllegalArgumentException("Too many dimensions for metric, " + dimensionMap.size()); } TreeSet<DimensionEntity> dimensions = new TreeSet<DimensionEntity>(); for (Map.Entry<String,String> entry: dimensionMap.entrySet()) { DimensionEntity d = new DimensionEntity(); d.setName(entry.getKey()); d.setValue(entry.getValue()); dimensions.add(d); } ListMetric metric = new ListMetric(); metric.setAccountId(accountId); metric.setMetricName(metricName); metric.setNamespace(namespace); metric.setDimensions(dimensions); metric.setMetricType(metricType); return metric; } private static void addMetric(EntityTransaction db, String accountId, String metricName, String namespace, Map<String, String> dimensionMap, MetricType metricType) { ListMetric metric = createMetric(accountId, metricName, namespace, dimensionMap, metricType); Criteria criteria = Entities.createCriteria(ListMetric.class) .add( Restrictions.eq( "accountId" , metric.getAccountId() ) ) .add( Restrictions.eq( "metricName" , metric.getMetricName() ) ) .add( Restrictions.eq( "namespace" , metric.getNamespace() ) ); // add dimension restrictions int dimIndex = 1; for (DimensionEntity d: metric.getDimensions()) { criteria.add( Restrictions.eq( "dim" + dimIndex + "Name", d.getName() ) ); criteria.add( Restrictions.eq( "dim" + dimIndex + "Value", d.getValue() ) ); dimIndex++; } while (dimIndex <= ListMetric.MAX_DIM_NUM) { criteria.add( Restrictions.isNull( "dim" + dimIndex + "Name") ); criteria.add( Restrictions.isNull( "dim" + dimIndex + "Value") ); dimIndex++; } ListMetric inDbMetric = (ListMetric) criteria.uniqueResult(); if (inDbMetric != null) { inDbMetric.setVersion(1 + inDbMetric.getVersion()); } else { Entities.persist(metric); } } public static void deleteAllMetrics() { try (final TransactionResource db = Entities.transactionFor(ListMetric.class)) { Entities.deleteAll(ListMetric.class); db.commit(); } } /** * Delete all metrics before a certain date * @param before the date to delete before (inclusive) */ public static void deleteMetrics(Date before) { try (final TransactionResource db = Entities.transactionFor(ListMetric.class)) { Map<String, Date> criteria = new HashMap<String, Date>(); criteria.put("before", before); Entities.deleteAllMatching(ListMetric.class, "WHERE lastUpdateTimestamp < :before", criteria); db.commit(); } } /** * Returns the metrics that are associated with the applied parameters * @param accountId the account Id. If null, this filter will not be used. * @param metricName the metric name. If null, this filter will not be used. * @param namespace the namespace. If null, this filter will not be used. * @param dimensionMap the dimensions (name/value) to filter against. Only metrics containing all these dimensions will be returned (it is only a subset match, not exact). If null, this filter will not be used. * @param after the time after which all metrics must have been updated (last seen). If null, this filter will not be used. * @param before the time before which all metrics must have been updated (last seen). If null, this filter will not be used. * @param maxRecords TODO * @param nextToken TODO * @return the collection of metrics, filtered by the input */ public static List<ListMetric> listMetrics(String accountId, String metricName, String namespace, Map<String, String> dimensionMap, Date after, Date before, Integer maxRecords, String nextToken) throws InvalidTokenException { if (dimensionMap != null && dimensionMap.size() > ListMetric.MAX_DIM_NUM) { throw new IllegalArgumentException("Too many dimensions " + dimensionMap.size()); } try (final TransactionResource db = Entities.transactionFor(ListMetric.class)) { Date nextTokenCreatedTime = NextTokenUtils.getNextTokenCreatedTime(nextToken, ListMetric.class); Map<String, String> sortedDimensionMap = new TreeMap<String, String>(); Criteria criteria = Entities.createCriteria(ListMetric.class); if (accountId != null) { criteria = criteria.add(Restrictions.eq("accountId", accountId)); } if (metricName != null) { criteria = criteria.add(Restrictions.eq("metricName", metricName)); } if (namespace != null) { criteria = criteria.add(Restrictions.eq("namespace", namespace)); } if (before != null) { criteria = criteria.add(Restrictions.le("lastUpdateTimestamp", before)); } if (after != null) { criteria = criteria.add(Restrictions.ge("lastUpdateTimestamp", after)); } if (dimensionMap != null && !dimensionMap.isEmpty()) { // sort the map sortedDimensionMap.putAll(dimensionMap); // now we are going to add a bunch of restrictions to the criteria... // note though there are certain dimensions we don't need to check. // For example if we have two dimensions, we don't need to check dimension 10 for // the first item or dimension 1 for the last item. int numDimensions = sortedDimensionMap.size(); int lowDimNum = 1; int highDimNum = ListMetric.MAX_DIM_NUM + 1 - numDimensions; for (Map.Entry<String, String> dimEntry : sortedDimensionMap.entrySet()) { Disjunction or = Restrictions.disjunction(); for (int i = lowDimNum; i <= highDimNum; i++) { or.add(Restrictions.conjunction() .add(Restrictions.eq("dim" + i + "Name", dimEntry.getKey())) .add(Restrictions.eq("dim" + i + "Value", dimEntry.getValue()))); } lowDimNum++; highDimNum++; criteria = criteria.add(or); } } criteria = NextTokenUtils.addNextTokenConstraints(maxRecords, nextToken, nextTokenCreatedTime, criteria); List<ListMetric> dbResult = (List<ListMetric>) criteria.list(); db.commit(); return dbResult; } } private static class NonPrefetchFields { private MetricType metricType; private Map<String, String> dimensionMap; public NonPrefetchFields(MetricType metricType, Map<String, String> dimensionMap) { this.metricType = metricType; this.dimensionMap = dimensionMap; } public MetricType getMetricType() { return metricType; } public void setMetricType(MetricType metricType) { this.metricType = metricType; } public Map<String, String> getDimensionMap() { return dimensionMap; } public void setDimensionMap(Map<String, String> dimensionMap) { this.dimensionMap = dimensionMap; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; NonPrefetchFields that = (NonPrefetchFields) o; if (dimensionMap != null ? !dimensionMap.equals(that.dimensionMap) : that.dimensionMap != null) return false; if (metricType != that.metricType) return false; return true; } @Override public int hashCode() { int result = metricType != null ? metricType.hashCode() : 0; result = 31 * result + (dimensionMap != null ? dimensionMap.hashCode() : 0); return result; } } private static class PrefetchFields { private String accountId; private String namespace; private String metricName; public String getAccountId() { return accountId; } public void setAccountId(String accountId) { this.accountId = accountId; } public PrefetchFields(String accountId, String namespace, String metricName) { this.accountId = accountId; this.namespace = namespace; this.metricName = metricName; } public String getNamespace() { return namespace; } public void setNamespace(String namespace) { this.namespace = namespace; } public String getMetricName() { return metricName; } public void setMetricName(String metricName) { this.metricName = metricName; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; PrefetchFields that = (PrefetchFields) o; if (accountId != null ? !accountId.equals(that.accountId) : that.accountId != null) return false; if (metricName != null ? !metricName.equals(that.metricName) : that.metricName != null) return false; if (namespace != null ? !namespace.equals(that.namespace) : that.namespace != null) return false; return true; } @Override public int hashCode() { int result = accountId != null ? accountId.hashCode() : 0; result = 31 * result + (namespace != null ? namespace.hashCode() : 0); result = 31 * result + (metricName != null ? metricName.hashCode() : 0); return result; } } public static void addMetricBatch(List<ListMetric> dataBatch) { // sort the collection by common items to require fewer lookups Multimap<PrefetchFields, ListMetric> dataBatchPrefetchMap = LinkedListMultimap.create(); for (final ListMetric item: dataBatch) { PrefetchFields prefetchFields = new PrefetchFields(item.getAccountId(), item.getNamespace(), item.getMetricName()); dataBatchPrefetchMap.put(prefetchFields, item); } // do db stuff in a certain number of operations per connection for (List<PrefetchFields> prefetchFieldsListPartial : Iterables.partition(dataBatchPrefetchMap.keySet(), LIST_METRIC_NUM_DB_OPERATIONS_PER_TRANSACTION)) { try (final TransactionResource db = Entities.transactionFor(ListMetric.class)) { int numOperations = 0; for (PrefetchFields prefetchFields: prefetchFieldsListPartial) { // Prefetch all list metrics with same metric name/namespace/account id Map<NonPrefetchFields, ListMetric> dataCache = Maps.newHashMap(); Criteria criteria = Entities.createCriteria(ListMetric.class) .add(Restrictions.eq("accountId", prefetchFields.getAccountId())) .add(Restrictions.eq("namespace", prefetchFields.getNamespace())) .add(Restrictions.eq("metricName", prefetchFields.getMetricName())); List<ListMetric> results = (List<ListMetric>) criteria.list(); for (ListMetric result : results) { dataCache.put(new NonPrefetchFields(result.getMetricType(), result.getDimensionMap()), result); } for (ListMetric listMetric : dataBatchPrefetchMap.get(prefetchFields)) { NonPrefetchFields cacheKey = new NonPrefetchFields(listMetric.getMetricType(), listMetric.getDimensionMap()); if (dataCache.containsKey(cacheKey)) { dataCache.get(cacheKey).updateTimeStamps(); } else { Entities.persist(listMetric); dataCache.put(cacheKey, listMetric); } } numOperations++; if (numOperations % LIST_METRIC_NUM_DB_OPERATIONS_UNTIL_SESSION_FLUSH == 0) { Entities.flushSession(ListMetric.class); Entities.clearSession(ListMetric.class); } } db.commit(); } } } public static ListMetric createListMetric(String accountId, String metricName, MetricEntity.MetricType metricType, String namespace, Map<String, String> dimensionMap) { if (dimensionMap == null) { dimensionMap = new HashMap<String, String>(); } else if (dimensionMap.size() > ListMetric.MAX_DIM_NUM) { throw new IllegalArgumentException("Too many dimensions for metric, " + dimensionMap.size()); } TreeSet<DimensionEntity> dimensions = new TreeSet<DimensionEntity>(); for (Map.Entry<String,String> entry: dimensionMap.entrySet()) { DimensionEntity d = new DimensionEntity(); d.setName(entry.getKey()); d.setValue(entry.getValue()); dimensions.add(d); } ListMetric metric = new ListMetric(); metric.setAccountId(accountId); metric.setMetricName(metricName); metric.setMetricType(metricType); metric.setNamespace(namespace); metric.setDimensions(dimensions); return metric; } }