package com.eucalyptus.cloudwatch.domain.listmetrics; /*************************************************************************
* 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.
************************************************************************/
import com.eucalyptus.cloudwatch.common.internal.domain.DimensionEntity;
import com.eucalyptus.cloudwatch.common.internal.domain.listmetrics.ListMetric;
import com.eucalyptus.cloudwatch.common.internal.domain.listmetrics.ListMetricManager;
import com.eucalyptus.cloudwatch.common.internal.domain.metricdata.MetricEntity.MetricType;
import com.google.common.collect.Lists;
import org.apache.log4j.Logger;
import org.junit.Ignore;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
@Ignore("Manual development test")
public class ListMetricsTest {
private static final Logger LOG = Logger.getLogger(ListMetricsTest.class);
// The following situation is being set up
// There are two accounts: THIS and THAT
// So account ids are account_this and account_that
// There are allowed values for metricName:
// metric_name_this, metric_name_that, and metric_name_both
// metric_name_both will be used by both accounts. [This allows for
// more than one value of metric name per account and also a metric
// name that is used by more than one account.
// Similarly there will be namespaces of
// namespace_this, namespace_that, and namespace_both.
//
//
// This means: for example, we may have the following metric combinations:
// account_this, metric_name_this, namespace_this
// account_this, metric_name_this, namespace_both
// account_this, metric_name_both, namespace_this
// account_this, metric_name_both, namespace_both
// account_that, metric_name_that, namespace_that
// account_that, metric_name_that, namespace_both
// account_that, metric_name_both, namespace_that
// account_that, metric_name_both, namespace_both
//
// This allows for 8 different metric combinations before dimensions
// are taken into account.
// Dimension names (if they are present) will always be:
// dimension_1, dimension_2, dimension_3, ... dimension_N (where n is a
// parameter.
// Dimension values will be literally dimension_this, dimension_that, or
// dimension_both. The semantics will be the same as above.
// Fixing a metric (say one of them under account_this), we have three
// possibilities for a given dimension: not included, dimension_this, or
// dimension_both. This means we have 3 possibilities for each of n dimensions,
// Taking everything into account, this is 3^n possible dimension combinations.
// That is 8 * 3^n total dimensions all together, including the differences
// in accounts, namespaces, and metric names. All of account_that metrics
// will be added after all of account_this metrics. This allows for a
// way to predict for a given search parameter how many metrics will be
// returned, and will exhaust all possible combinations.
public static void testReadDelete() {
testReadDelete(10);
}
public static void testReadDelete(int n) {
try {
ListMetricManager.deleteAllMetrics();
// TODO make assertSize like a junit test
LOG.fatal("Assertion Result: " + assertSize(0, ListMetricManager.listMetrics(null, null, null, null, null, null, null, null)));
Date start = new Date();
String accountId = "account_this";
List<List<DimensionEntity>> dimensionChoices = new ArrayList<List<DimensionEntity>>();
for (int i=1;i<=n;i++) {
// create choices for dimensions
DimensionEntity d1 = new DimensionEntity("dimension_" + i, "dimension_this");
DimensionEntity d2 = new DimensionEntity("dimension_" + i, "dimension_both");
dimensionChoices.add(Lists.newArrayList(d1, d2));
}
List<String> metricNames = Lists.newArrayList("metric_name_this", "metric_name_both");
List<String> namespaces = Lists.newArrayList("namespace_this", "namespace_both");
for (String metricName : metricNames) {
for (String namespace : namespaces) {
for (Collection<DimensionEntity> dimensions: new PowerChoiceIterable<DimensionEntity>(dimensionChoices)) {
ListMetricManager.addMetric(accountId, metricName, namespace, toDimensionMap(dimensions), MetricType.Custom);
}
}
}
Date middle = new Date();
accountId = "account_that";
dimensionChoices = new ArrayList<List<DimensionEntity>>();
for (int i=1;i<=n;i++) {
// create choices for dimensions
DimensionEntity d1 = new DimensionEntity("dimension_" + i, "dimension_that");
DimensionEntity d2 = new DimensionEntity("dimension_" + i, "dimension_both");
dimensionChoices.add(Lists.newArrayList(d1, d2));
}
metricNames = Lists.newArrayList("metric_name_that", "metric_name_both");
namespaces = Lists.newArrayList("namespace_that", "namespace_both");
for (String metricName : metricNames) {
for (String namespace : namespaces) {
for (Collection<DimensionEntity> dimensions: new PowerChoiceIterable<DimensionEntity>(dimensionChoices)) {
ListMetricManager.addMetric(accountId, metricName, namespace, toDimensionMap(dimensions), MetricType.Custom);
}
}
}
Date end = new Date();
LOG.fatal("start=" + start);
LOG.fatal("middle=" + middle);
LOG.fatal("end=" + end);
// now check all getters ... and predictive power
List<String> accountNames = new ArrayList<String>();
accountNames.add(null);
accountNames.add("account_this");
accountNames.add("account_that");
metricNames = new ArrayList<String>();
metricNames.add(null);
metricNames.add("metric_name_this");
metricNames.add("metric_name_that");
metricNames.add("metric_name_both");
namespaces = new ArrayList<String>();
namespaces.add(null);
namespaces.add("namespace_this");
namespaces.add("namespace_that");
namespaces.add("namespace_both");
List<Date> afters = new ArrayList<Date>();
afters.add(null);
afters.add(start);
afters.add(middle);
afters.add(end);
List<Date> befores = new ArrayList<Date>();
befores.add(null);
befores.add(start);
befores.add(middle);
befores.add(end);
dimensionChoices = new ArrayList<List<DimensionEntity>>();
for (int i=1;i<=n;i++) {
// create choices for dimensions
DimensionEntity d1 = new DimensionEntity("dimension_" + i, "dimension_that");
DimensionEntity d2 = new DimensionEntity("dimension_" + i, "dimension_both");
DimensionEntity d3 = new DimensionEntity("dimension_" + i, "dimension_this");
dimensionChoices.add(Lists.newArrayList(d1, d2, d3));
}
int totalReads = 0;
int totalExpectedReads = 3 * 4 * 4 * 4 * 4 * intPow(4,n);
for (String accountName: accountNames) {
for (String metricName: metricNames) {
for (String namespace: namespaces) {
for (Date after: afters) {
for (Date before: befores) {
for (Collection<DimensionEntity> dimensions: new PowerChoiceIterable<DimensionEntity>(dimensionChoices)) {
Collection<ListMetric> metrics = ListMetricManager.listMetrics(accountName, metricName, namespace, toDimensionMap(dimensions), after, before, null, null);
int numValues = predictedResults(accountName, metricName,
namespace, toDimensionMap(dimensions),
after, before, start, middle, end, n);
boolean checkAccuracy = assertSize(numValues, metrics);
if (!checkAccuracy) {
LOG.fatal("Expected: " + numValues + ", got " + (metrics == null ? 0 : metrics.size()));
LOG.fatal(paramsToString(accountName, metricName,
namespace, toDimensionMap(dimensions),
after, before, start, middle, end, n));
}
totalReads++;
if (totalReads % 1000 == 0) {
LOG.fatal("Read " + totalReads + " of " + totalExpectedReads);
}
}
}
}
}
}
}
LOG.fatal("Assertion Result: " + assertSize(intPow(2,3) * intPow(3,n), ListMetricManager.listMetrics(null, null, null, null, null, null, null, null)));
ListMetricManager.deleteMetrics(start);
LOG.fatal("Assertion Result: " + assertSize(intPow(2,3) * intPow(3,n), ListMetricManager.listMetrics(null, null, null, null, null, null, null, null)));
ListMetricManager.deleteMetrics(middle);
LOG.fatal("Assertion Result: " + assertSize(intPow(2,2) * intPow(3,n), ListMetricManager.listMetrics(null, null, null, null, null, null, null, null)));
ListMetricManager.deleteMetrics(end);
LOG.fatal("Assertion Result: " + assertSize(0, ListMetricManager.listMetrics(null, null, null, null, null, null, null, null)));
} catch (Throwable ex) {
LOG.fatal(ex, ex);
ex.printStackTrace();
}
}
private static Object paramsToString(String accountName, String metricName,
String namespace, Map<String, String> dimensionMap, Date after,
Date before, Date start, Date middle, Date end, int n) {
StringBuilder sb = new StringBuilder();
sb.append("accountName = " + accountName);
sb.append(", metricName = " + metricName);
sb.append(", namespace = " + namespace);
if (end.equals(after)) {
sb.append(", after = end");
}
if (middle.equals(after)) {
sb.append(", after = middle");
}
if (start.equals(after)) {
sb.append(", after = start");
}
if (after == null) {
sb.append(", after = null");
}
if (end.equals(before)) {
sb.append(", before = end");
}
if (middle.equals(before)) {
sb.append(", before = middle");
}
if (start.equals(before)) {
sb.append(", before = start");
}
if (before == null) {
sb.append(", before = null");
}
sb.append(", dimensions = " + dimensionMap);
return sb.toString();
}
private static Map<String, String> toDimensionMap(
Collection<DimensionEntity> dimensions) {
Map<String, String> retVal = new HashMap<String, String>();
for (DimensionEntity simpleDimension: dimensions) {
retVal.put(simpleDimension.getName(), simpleDimension.getValue());
}
return retVal;
}
private static boolean assertSize(int size,Collection<ListMetric> metricCollection) {
return ((metricCollection == null && size == 0) ||
(metricCollection.size() == size));
}
private static int predictedResults(String accountId, String metricName,
String namespace, Map<String,String> dimensions,
Date after, Date before, Date start, Date middle, Date end, int n) {
// Assumptions: about date values
// start = a non-null value before we start
// middle = a non-null value between inserts of account_this and account_that
// end = a non-null value after we insert all values
// after and before can be null but if not must be one of the above 3 dates.
// How do we determine how many matches we can have?
// It depends on how many degrees of freedom we have
// If we have all degrees of freedom, the total return is
// 2^3 * 3^n, where n = the number of dimensions we pass in.
// The 2 choice degrees of freedom are: accountId, metricName, and namespace
// The 3 choice degrees of freedom are: each dimension: not in, or
// one of two values
// The accountId is tricky as there are many things that can restrict it:
// a) it's own value
// b) any of the other values containing a 'this' or 'that'
// c) date ranges.
// In particular if the search query contains an account id that removes
// that degree of freedom. Any values that contain 'this' forces the
// account id to 'account_this'. Similarly any values that contain
// 'that' forces the accout id to 'account_that'
// This can result in a 'no results' situation.
// For dates, start must mean 'before account_this' data is entered
// middle must mean 'between account entries'
// end must mean 'after account_that data is entered'
// The following values for after and before will show all possibilities
// after | before | result | addressed in clause
//--------+--------+------------------------------+---------------------
// null | null | no restriction on account_id | Clause D
// null | start | no values | Clause A
// null | middle | account_id is account_this | Clause E
// null | end | no restriction on account_id | Clause E
// start | null | no restriction on account_id | Clause E
// start | start | no values | Clause A
// start | middle | account_id is account_this | Clause E
// start | end | no restriction on account_id | Clause D
// middle | null | account_id is account_that | Clause C
// middle | start | no values | Clause A
// middle | middle | no values | Clause B
// middle | end | account_id is account_that | Clause C
// end | null | no values | Clause A
// end | start | no values | Clause A
// end | middle | no values | Clause A
// end | end | no values | Clause A
boolean referencesAccountThis = false;
boolean referencesAccountThat = false;
if (end.equals(after) || start.equals(before)) { // (Clause A)
return 0;
} if (middle.equals(after) && middle.equals(before)) { // (Clause B)
return 0;
} else if (middle.equals(after) &&
((null == before) || end.equals(before))) { // (Clause C)
referencesAccountThat = true;
} else if (middle.equals(before) &&
((null == after) || start.equals(after))) { // (Clause C)
referencesAccountThis = true;
} else { // (Clause E)
; // no restrictions
}
// now we check degrees of freedom. (account id is the last check)
int twoChoiceDegreesOfFreedom = 0;
Collection<String> values = new HashSet<String>();
if (metricName != null) {
values.add(metricName);
} else {
twoChoiceDegreesOfFreedom++;
}
if (namespace != null) {
values.add(namespace);
} else {
twoChoiceDegreesOfFreedom++;
}
if (accountId != null) {
values.add(accountId);
}
// we do the degrees of freedom check for account id last. It depends on
// other values
// now check dimension degrees of freedom
int threeChoiceDegreesOfFreedom = n;
if (dimensions != null) {
values.addAll(dimensions.values());
threeChoiceDegreesOfFreedom -= dimensions.size();
}
for (String value:values) {
if (value.endsWith("this")) {
referencesAccountThis = true;
}
if (value.endsWith("that")) {
referencesAccountThat = true;
}
}
// finally check accountId degrees of freedom (only if null & this/that not referenced)
if (!referencesAccountThis &&
!referencesAccountThat && (accountId == null)) {
twoChoiceDegreesOfFreedom++;
}
// if this and that are referenced, nothing
if (referencesAccountThis && referencesAccountThat) {
return 0;
} else {
return intPow(2, twoChoiceDegreesOfFreedom) * intPow(3, threeChoiceDegreesOfFreedom);
}
}
private static int intPow(int base, int power) {
int result = 1;
for (int i=0;i<power;i++) {
result *= base;
}
return result;
}
private static class PowerChoiceIterable<T> implements Iterable<Collection<T>> {
private int[] listSizes;
private List<List<T>> internalChoiceList;
public PowerChoiceIterable(List<? extends Collection<T>> choiceList) {
listSizes = new int[choiceList.size()];
// todo: deal with illegal argument exceptions later...
int index = 0;
internalChoiceList = new ArrayList<List<T>>();
for (Collection<T> choice: choiceList) {
List<T> copy = Lists.newArrayList();
copy.addAll(choice);
internalChoiceList.add(copy);
listSizes[index++] = copy.size();
}
}
@Override
public Iterator<Collection<T>> iterator() {
return new PowerChoiceIterator<T>(listSizes, internalChoiceList);
}
}
private static class PowerChoiceIterator<T> implements Iterator<Collection<T>> {
int[] listSizes;
int[] currentState;
List<List<T>> internalChoiceList;
boolean done = false;
public PowerChoiceIterator(int[] listSizes,
List<List<T>> internalChoiceList) {
this.internalChoiceList = internalChoiceList;
this.listSizes = listSizes;
this.currentState = new int[listSizes.length];
}
@Override
public boolean hasNext() {
return !done;
}
@Override
public Collection<T> next() {
if (done) throw new NoSuchElementException();
int index = 0;
ArrayList<T> result = new ArrayList<T>();
for (List<T> choice: internalChoiceList) {
if (currentState[index] != listSizes[index]) {
result.add(choice.get(currentState[index]));
}
index++;
}
done = advance();
return result;
}
private boolean advance() {
int position = currentState.length - 1;
while (position >= 0 && currentState[position] == listSizes[position]) {
position--;
}
if (position < 0) return true;
currentState[position]++;
for (int index = position+1;index < currentState.length; index++) {
currentState[index] = 0;
}
return false;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
private static void insertMetricsWithDimensions(int i) throws Exception {
String accountId = "account1";
String metricName = "metric"+i;
String namespace = "namespace"+i;
Map<String,String> dimensions = new HashMap<String,String>();
for (int j=1;j<=i;j++) {
String name = "name" + j;
String value = "value" + j;
dimensions.put(name, value);
}
ListMetricManager.addMetric(accountId, metricName, namespace, dimensions, MetricType.Custom);
}
public static void testInsertUpdate() throws Exception {
ListMetricManager.deleteAllMetrics();
// TODO make assertSize like a junit test
LOG.fatal("Assertion Result: " + assertSize(0, ListMetricManager.listMetrics(null, null, null, null, null, null, null, null)));
for (int i=2;i<=10;i++) {
insertMetricsWithDimensions(i);
}
Thread.sleep(10000L);
for (int i=10;i>=2;i--) {
insertMetricsWithDimensions(i);
}
LOG.fatal("Assertion Result: " + assertSize(10, ListMetricManager.listMetrics(null, null, null, null, null, null, null, null)));
}
}