/* * Copyright (c) 2012 EMC Corporation * All Rights Reserved */ package com.emc.storageos.db.client.impl; import com.emc.storageos.db.client.TimeSeriesMetadata; import com.emc.storageos.db.client.model.*; import com.netflix.astyanax.model.ByteBufferRange; import com.netflix.astyanax.model.ColumnFamily; import com.netflix.astyanax.serializers.StringSerializer; import com.netflix.astyanax.serializers.TimeUUIDSerializer; import com.netflix.astyanax.util.RangeBuilder; import com.netflix.astyanax.util.TimeUUIDUtils; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.annotation.Annotation; import java.util.*; import java.util.concurrent.atomic.AtomicLong; /** * Encapsulate time series information */ public class TimeSeriesType<T extends TimeSeriesSerializer.DataPoint> implements TimeSeriesMetadata { private static final Logger _logger = LoggerFactory.getLogger(TimeSeriesType.class); private Class<? extends TimeSeries> _type; private int _shardCount = 1; private boolean _compactionOptimized = false; private String _cfName; private DateTimeFormatter _prefixFormatter; private TimeBucket _bucketGranularity; private ColumnFamily<String, UUID> _cf; private Integer _ttl; private AtomicLong _bucketIndex = new AtomicLong(); private TimeSeries<T> _timeSeries; private List<TimeBucket> _supportedGranularity; /** * Constructor * * @param clazz */ public TimeSeriesType(Class<? extends TimeSeries> clazz) { _type = clazz; init(); } /** * Gets shard index to use for next data point. Note that shard * index should be calculated by * * getAndIncrementBucketIndex % shard count * * @return */ private long getNextShardIndex() { return _bucketIndex.getAndIncrement() % _shardCount; } /** * Get bucket configuration * * @return */ public TimeBucket getBucketConfig() { return _bucketGranularity; } /** * Override TTL setting * * @param ttl */ public void setTtl(Integer ttl) { _ttl = ttl; } /** * Returns row ID to use for current time (UTC). Takes into account * bucket granularity and shard count. Row ID does not use data * point's time because it's used as a way to * * 1. load balance across rows. Data point's time stamps * do not guarantee such things since time series data source * could be out of whack * 2. serve as collection time stamp * * This means that this time series implementation is not * a good fit for use cases that need to * * 1. retrieve by source time stamp order * or * 2. source vs. insertion timestamp differ by a wide margin * * @return row Id to use for next insertion */ public String getRowId() { return getRowId(null); } public boolean getCompactOptimized() { return _compactionOptimized; } /** * Return row Id to use for given time. * * @param time * @return row id to use for next insertion */ public String getRowId(DateTime time) { if (time == null) { time = new DateTime(DateTimeZone.UTC); } StringBuilder rowId = new StringBuilder(_prefixFormatter.print(time)); rowId.append(getNextShardIndex()); return rowId.toString(); } /** * Returns rows to query for given time bucket * * @return */ public List<String> getRows(DateTime bucket) { List<String> rows = new ArrayList<String>(_shardCount); String prefix = _prefixFormatter.print(bucket); for (int index = 0; index < _shardCount; index++) { rows.add(String.format("%1$s%2$d", prefix, index)); } return rows; } /** * Return column range for given time and bucket granularity * * @param time target query time * @param granularity granularity * @param pageSize page size * @return */ public ByteBufferRange getColumnRange(DateTime time, TimeBucket granularity, int pageSize) { if (time.getZone() != DateTimeZone.UTC) { throw new IllegalArgumentException("Invalid timezone"); } if (granularity.ordinal() > _bucketGranularity.ordinal()) { throw new IllegalArgumentException("Invalid granularity"); } RangeBuilder builder = new RangeBuilder(); builder.setLimit(pageSize); if (granularity.ordinal() < _bucketGranularity.ordinal()) { // finer than specified granularity DateTime start = DateTime.now(); DateTime end = DateTime.now(); switch (granularity) { case MONTH: start = new DateTime(time.getYear(), time.getMonthOfYear(), 1, 0, 0, DateTimeZone.UTC); end = start.plusMonths(1); break; case DAY: start = new DateTime(time.getYear(), time.getMonthOfYear(), time.getDayOfMonth(), 0, 0, DateTimeZone.UTC); end = start.plusDays(1); break; case HOUR: start = new DateTime(time.getYear(), time.getMonthOfYear(), time.getDayOfMonth(), time.getHourOfDay(), 0, DateTimeZone.UTC); end = start.plusHours(1); break; case MINUTE: start = new DateTime(time.getYear(), time.getMonthOfYear(), time.getDayOfMonth(), time.getHourOfDay(), time.getMinuteOfHour(), DateTimeZone.UTC); end = start.plusMinutes(1); break; case SECOND: start = new DateTime(time.getYear(), time.getMonthOfYear(), time.getDayOfMonth(), time.getHourOfDay(), time.getMinuteOfHour(), time.getSecondOfMinute(), DateTimeZone.UTC); end = start.plusSeconds(1); break; } builder.setStart(TimeUUIDUtils.getTimeUUID(start.getMillis())); builder.setEnd(createMaxTimeUUID(end.minusMillis(1).getMillis())); } return builder.build(); } /** * Serializer for data points * * @return */ public TimeSeriesSerializer<T> getSerializer() { return _timeSeries.getSerializer(); } /** * Data point TTL * * @return */ public Integer getTtl() { return _ttl; } /** * Get CF for this time series data * * @return */ public ColumnFamily<String, UUID> getCf() { return _cf; } /** * Process annotations for time series type */ @SuppressWarnings(value = "unchecked") private void init() { Annotation[] annotations = _type.getAnnotations(); for (int i = 0; i < annotations.length; i++) { Annotation a = annotations[i]; if (a instanceof Cf) { _cfName = ((Cf) a).value(); } else if (a instanceof Shards) { _shardCount = ((Shards) a).value(); } else if (a instanceof CompactionOptimized) { _compactionOptimized = true; } else if (a instanceof BucketGranularity) { _bucketGranularity = ((BucketGranularity) a).value(); switch (_bucketGranularity) { case SECOND: _prefixFormatter = DateTimeFormat.forPattern("yyyyMMddHHmmss-"); break; case MINUTE: _prefixFormatter = DateTimeFormat.forPattern("yyyyMMddHHmm-"); break; case HOUR: _prefixFormatter = DateTimeFormat.forPattern("yyyyMMddHH-"); break; case DAY: _prefixFormatter = DateTimeFormat.forPattern("yyyyMMdd-"); break; case MONTH: _prefixFormatter = DateTimeFormat.forPattern("yyyyMM-"); break; case YEAR: _prefixFormatter = DateTimeFormat.forPattern("yyyy-"); break; } _supportedGranularity = new ArrayList<TimeBucket>(); TimeBucket[] buckets = TimeBucket.values(); for (int j = 0; j < buckets.length; j++) { TimeBucket bucket = buckets[j]; if (bucket.ordinal() > _bucketGranularity.ordinal()) { break; } _supportedGranularity.add(bucket); } _supportedGranularity = Collections.unmodifiableList(_supportedGranularity); } else if (a instanceof Ttl) { _ttl = ((Ttl) a).value(); } else { throw new IllegalArgumentException("Unexpected annotation"); } } _cf = new ColumnFamily<String, UUID>(_cfName, StringSerializer.get(), TimeUUIDSerializer.get()); try { _timeSeries = _type.newInstance(); } catch (Exception e) { throw new IllegalArgumentException(e); } } @Override public String getName() { return _type.getSimpleName(); } @Override public List<TimeBucket> getSupportedQueryGranularity() { return _supportedGranularity; } /** * Create max range time UUID for given millisecond - see UUIDGen for algorithm/source. * * @param maxTime * @return */ private UUID createMaxTimeUUID(long maxTime) { long time; // UTC time long timeToUse = (maxTime * 10000) + 0x01b21dd213814000L + 9999; // time low time = timeToUse << 32; // time mid time |= (timeToUse & 0xFFFF00000000L) >> 16; // time hi and version time |= 0x1000 | ((timeToUse >> 48) & 0x0FFF); // version 1 return new UUID(time, 0xffffffffffffffffL); } }