/*
 * Copyright 2013 Rackspace
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package com.rackspacecloud.blueflood.cache;

import com.codahale.metrics.*;
import com.codahale.metrics.Timer;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.CacheStats;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import com.rackspacecloud.blueflood.concurrent.ThreadPoolBuilder;
import com.rackspacecloud.blueflood.exceptions.CacheException;
import com.rackspacecloud.blueflood.io.IOContainer;
import com.rackspacecloud.blueflood.io.MetadataIO;
import com.rackspacecloud.blueflood.service.Configuration;
import com.rackspacecloud.blueflood.service.CoreConfig;
import com.rackspacecloud.blueflood.types.Locator;
import com.rackspacecloud.blueflood.types.MetricMetadata;
import com.rackspacecloud.blueflood.utils.Metrics;
import com.rackspacecloud.blueflood.utils.TimeValue;
import com.rackspacecloud.blueflood.utils.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.management.InstanceAlreadyExistsException;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.*;
import java.util.concurrent.*;

/**
 * This class implements caching of the data that lives in metrics_metadata
 * Cassandra Column Family.
 *
 * The type of metadata we cache corresponds to the enum values
 * in {@link MetricMetadata}
 */
public class MetadataCache extends AbstractJmxCache implements MetadataCacheMBean {
    // todo: give each cache a name.

    private final com.google.common.cache.LoadingCache<CacheKey, String> cache;
    private static final String NULL = "null".intern();
    private static final Logger log = LoggerFactory.getLogger(MetadataCache.class);
    private static final TimeValue defaultExpiration = new TimeValue(Configuration.getInstance().getIntegerProperty(
            CoreConfig.META_CACHE_RETENTION_IN_MINUTES), TimeUnit.MINUTES);
    private static final int defaultConcurrency = Configuration.getInstance().getIntegerProperty(
            CoreConfig.META_CACHE_MAX_CONCURRENCY);
    private final Boolean batchedReads;
    private final Boolean batchedWrites;
    private static final boolean SHOULD_STORE_UNITS = Configuration.getInstance().getBooleanProperty(
            CoreConfig.SHOULD_STORE_UNITS);

    // Specific to batched meta reads

    private static final Integer batchedReadsThreshold = Configuration.getInstance().getIntegerProperty(
            CoreConfig.META_CACHE_BATCHED_READS_THRESHOLD);
    private static final Integer batchedReadsTimerConfig = Configuration.getInstance().getIntegerProperty(
            CoreConfig.META_CACHE_BATCHED_READS_TIMER_MS);
    private static final TimeValue batchedReadsInterval = new TimeValue(batchedReadsTimerConfig, TimeUnit.MILLISECONDS);
    private static final Integer batchedReadsPipelineLimit = Configuration.getInstance().getIntegerProperty(
            CoreConfig.META_CACHE_BATCHED_READS_PIPELINE_DEPTH);

    private final java.util.Timer batchedReadsTimer = new java.util.Timer("MetadataBatchedReads");
    private final ThreadPoolExecutor readThreadPoolExecutor;
    private final Set<Locator> outstandingMetaReads;
    private final Queue<Locator> metaReads; // Guarantees FIFO reads
    private static final Timer batchedReadsTimerMetric = Metrics.timer(MetadataCache.class, "Metadata batched reads timer");

    // Specific to batched meta writes

    private static final Integer batchedWritesThreshold = Configuration.getInstance().getIntegerProperty(
            CoreConfig.META_CACHE_BATCHED_WRITES_THRESHOLD);
    private static final Integer batchedWritesTimerConfig = Configuration.getInstance().getIntegerProperty(
            CoreConfig.META_CACHE_BATCHED_WRITES_TIMER_MS);
    private static final TimeValue batchedWritesInterval = new TimeValue(batchedWritesTimerConfig, TimeUnit.MILLISECONDS);
    private static final Integer batchedWritesPipelineLimit = Configuration.getInstance().getIntegerProperty(
            CoreConfig.META_CACHE_BATCHED_WRITES_PIPELINE_DEPTH);

    private final java.util.Timer batchedWritesTimer = new java.util.Timer("MetadataBatchedWrites");
    private final ThreadPoolExecutor writeThreadPoolExecutor;
    private final Set<CacheKey> outstandingMetaWrites;
    private final Queue<CacheKey> metaWrites; // Guarantees FIFO writes
    private static final Timer batchedWritesTimerMetric = Metrics.timer(MetadataCache.class, "Metadata batched writes timer");

    private static final MetadataCache INSTANCE = new MetadataCache(defaultExpiration, defaultConcurrency);
    private MetadataIO io = IOContainer.fromConfig().getMetadataIO();
    private static Timer cacheSaveTimer = Metrics.timer(MetadataCache.class, "Persistence Save");
    private static Timer cacheLoadTimer = Metrics.timer(MetadataCache.class, "Persistence Load");
    private static final Meter updatedMetricMeter = Metrics.meter(MetadataCache.class, "Received updated metric");
    private static final Histogram totalMetadataSize = Metrics.histogram(MetadataCache.class, "Metadata row size");
    private static final Timer cacheGetTimer = Metrics.timer(MetadataCache.class, "Metadata get timer");
    private static final Timer cachePutTimer = Metrics.timer(MetadataCache.class, "Metadata put timer");
    private final Gauge cacheSizeGauge = new Gauge<Long>() {
        @Override
        public Long getValue() {
            return cache.size();
        }
    };

    private MetadataCache(TimeValue expiration, int concurrency) {
        try {
            final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
            final String name = String.format(MetadataCache.class.getPackage().getName() + ":type=%s,name=Stats", MetadataCache.class.getSimpleName());
            final ObjectName nameObj = new ObjectName(name);
            mbs.registerMBean(this, nameObj);
            instantiateYammerMetrics(MetadataCache.class, "metadata", nameObj);
        } catch (InstanceAlreadyExistsException doNotCare) {
            log.debug(doNotCare.getMessage());
        } catch (Exception ex) {
            log.error("Unable to register mbean for " + getClass().getName(), ex);
        }

        CacheLoader<CacheKey, String> loader = new CacheLoader<CacheKey, String>() {
            @Override
            public String load(CacheKey key) throws Exception {
                return MetadataCache.this.databaseLoad(key.locator, key.keyString);
            }
        };
        cache = CacheBuilder.newBuilder()
                .expireAfterWrite(expiration.getValue(), expiration.getUnit())
                .concurrencyLevel(concurrency)
                .recordStats()
                .build(loader);
        try {
            Metrics.getRegistry().register(MetricRegistry.name(MetadataCache.class, "Cache Size"), this.cacheSizeGauge);
        } catch (Exception e) {
            // pass
        }
        this.outstandingMetaReads = new ConcurrentSkipListSet<Locator>();
        this.metaReads = new ConcurrentLinkedQueue<Locator>();
        this.readThreadPoolExecutor = new ThreadPoolBuilder().withCorePoolSize(batchedReadsPipelineLimit)
                .withMaxPoolSize(batchedReadsPipelineLimit)
                .withBoundedQueue(Configuration.getInstance()
                        .getIntegerProperty(CoreConfig.META_CACHE_BATCHED_READS_QUEUE_SIZE))
                .withName("MetaBatchedReadsThreadPool").build();

        this.batchedReads = Configuration.getInstance().getBooleanProperty(
                CoreConfig.META_CACHE_BATCHED_READS);
        this.batchedWrites = Configuration.getInstance().getBooleanProperty(
                CoreConfig.META_CACHE_BATCHED_WRITES);
        if (batchedReads) {
            this.batchedReadsTimer.schedule(new TimerTask() {
                @Override
                public void run() {
                    fetchMeta(true);
                }
            }, 0, this.batchedReadsInterval.toMillis());
        }
        this.outstandingMetaWrites = new ConcurrentSkipListSet<CacheKey>();
        this.writeThreadPoolExecutor = new ThreadPoolBuilder().withCorePoolSize(batchedWritesPipelineLimit)
                .withMaxPoolSize(batchedWritesPipelineLimit)
                .withBoundedQueue(Configuration.getInstance()
                        .getIntegerProperty(CoreConfig.META_CACHE_BATCHED_WRITES_QUEUE_SIZE))
                .withName("MetaBatchedWritesThreadPool").build();
        this.metaWrites = new ConcurrentLinkedQueue<CacheKey>();

        if (batchedWrites) {
            this.batchedWritesTimer.schedule(new TimerTask() {
                @Override
                public void run() {
                    flushMeta(true);
                }
            }, 0, this.batchedWritesInterval.toMillis());
        }
    }

    public void setIO(MetadataIO io) {
        this.io = io;
        cache.invalidateAll();
    }

    public static MetadataCache getInstance() {
        return INSTANCE;
    }

    public static MetadataCache createLoadingCacheInstance() {
        return new MetadataCache(defaultExpiration, defaultConcurrency);
    }

    public static MetadataCache createLoadingCacheInstance(TimeValue expiration, int concurrency) {
        return new MetadataCache(expiration, concurrency);
    }

    public void save(DataOutputStream out) throws IOException {

        Timer.Context ctx = cacheSaveTimer.time();
        try {
        // convert to a table. this avoids us writing out the locator over and over.
            Map<CacheKey, String> map = new HashMap<CacheKey, String>(cache.asMap());
            Table<Locator, String, String> table = HashBasedTable.create();
            for (Map.Entry<CacheKey, String> entry : map.entrySet()) {
                table.put(entry.getKey().locator, entry.getKey().keyString, entry.getValue());
            }

            Set<Locator> rowKeys = table.rowKeySet();
            out.writeInt(rowKeys.size());

            for (Locator locator : rowKeys) {
                out.writeUTF(locator.toString());

                // how many key/value pairs are there?
                Map<String, String> pairs = table.row(locator);
                out.writeInt(pairs.size());
                for (Map.Entry<String, String> entry : pairs.entrySet()) {
                    out.writeUTF(entry.getKey());
                    out.writeUTF(entry.getValue());
                }
            }
        } finally {
            ctx.stop();
        }
    }

    public void load(DataInputStream in) throws IOException {
        Timer.Context ctx = cacheLoadTimer.time();
        try {
            int numLocators = in.readInt();
            for (int locIndex = 0; locIndex < numLocators; locIndex++) {
                Locator locator = Locator.createLocatorFromDbKey(in.readUTF());
                int numPairs = in.readInt();
                for (int pairIndex = 0; pairIndex < numPairs; pairIndex++) {
                    cache.put(new CacheKey(locator, in.readUTF()), in.readUTF());
                }
            }
        } finally {
            ctx.stop();
        }
    }

    public boolean containsKey(Locator locator, String key) {
        return cache.getIfPresent(new CacheKey(locator, key)) != null;
    }

    public String get(Locator locator, String key) throws CacheException {
        if (!batchedReads) {
            return getImmediately(locator, key);
        }

        String val = cache.getIfPresent(new CacheKey(locator, key));

        if (val == null) {
            databaseLazyLoad(locator); // loads all meta for the locator (optimized to prevent duplicate reads)
        }

        return val;
    }

    /**
     * Similar to {@link #get(Locator, String)}, except this does not throw
     * any {@link CacheException}. Instead, it may return null if the cache
     * does not contain the requested locator for the specified key.
     *
     * @param locator
     * @param key
     * @return
     */
    public String safeGet(Locator locator, String key) {
        String cacheValue = null;
        try {
            cacheValue = get(locator, key);
        } catch (CacheException ex) {
            log.trace(String.format("no cache value found for locator=%s, key=%s", locator.toString(), key));
        }
        return cacheValue;
    }

    public String getImmediately(Locator locator, String key) throws CacheException {
        Timer.Context cacheGetTimerContext = cacheGetTimer.time();
        try {
            CacheKey cacheKey = new CacheKey(locator, key);
            String result = cache.get(cacheKey);
            if (result.equals(NULL)) {
                return null;
            } else {
                return result;
            }
        } catch (ExecutionException ex) {
            throw new CacheException(ex);
        } finally {
            cacheGetTimerContext.stop();
        }
    }

    public <T> T get(Locator locator, String key, Class<T> type) throws CacheException {
        try {
            String val = get(locator, key);
            if (val == null) {
                return null;
            }
            return (T) val;
        } catch (ClassCastException ex) {
            throw new CacheException(ex);
        }
    }

    public String getUnitString(Locator locator) {
        String unitString = Util.UNKNOWN;
        // Only grab units from cassandra, if we have to
        if (SHOULD_STORE_UNITS && !Util.shouldUseESForUnits()) {
            try {
                unitString = get(locator, MetricMetadata.UNIT.name().toLowerCase(), String.class);
            } catch (CacheException ex) {
                log.warn("Cache exception reading unitString from MetadataCache: ", ex);
            }
            if (unitString == null) {
                unitString = Util.UNKNOWN;
            }
        }
        return unitString;
    }

    // todo: synchronization?
    // returns true if updated.
    public boolean put(Locator locator, String key, String value) throws CacheException {
        if (value == null) return false;

        Timer.Context cachePutTimerContext = MetadataCache.cachePutTimer.time();
        boolean dbWrite = false;
        try {
            CacheKey cacheKey = new CacheKey(locator, key);
            String oldValue = cache.getIfPresent(cacheKey);
            // don't care if oldValue == EMPTY.
            // always put new value in the cache. it keeps reads from happening.
            cache.put(cacheKey, value);
            if (oldValue == null || !oldValue.equals(value)) {
                dbWrite = true;
            }

            if (dbWrite) {
                updatedMetricMeter.mark();
                if (!batchedWrites) {
                    databasePut(locator, key, value);
                } else {
                    databaseLazyWrite(locator, key);
                }
            }

            return dbWrite;
        } finally {
            cachePutTimerContext.stop();
        }
    }

    public void invalidate(Locator locator, String key) {
        cache.invalidate(new CacheKey(locator, key));
    }

    private void databasePut(Locator locator, String key, String value) throws CacheException {
        try {
            io.put(locator, key, value);
        } catch (IOException ex) {
            throw new CacheException(ex);
        }
    }

    // implements the CacheLoader interface.
    private String databaseLoad(Locator locator, String key) throws CacheException {
        try {
            CacheKey cacheKey = new CacheKey(locator, key);
            Map<String, String> metadata = io.getAllValues(locator);
            if (metadata == null || metadata.isEmpty()) {
                cache.put(cacheKey, NULL);
                return NULL;
            }

            int metadataRowSize = 0;
            // prepopulate all other metadata other than the key we called the method with
            for (Map.Entry<String, String> meta : metadata.entrySet()) {
                metadataRowSize += meta.getKey().getBytes().length + locator.toString().getBytes().length;
                if (meta.getValue() != null)
                    metadataRowSize += meta.getValue().getBytes().length;
                if (meta.getKey().equals(key)) continue;
                CacheKey metaKey = new CacheKey(locator, meta.getKey());
                cache.put(metaKey, meta.getValue());
            }
            totalMetadataSize.update(metadataRowSize);

            String value = metadata.get(key);

            if (value == null) {
                cache.put(cacheKey, NULL);
                value = NULL;
            }

            return value;
        } catch (IOException ex) {
            throw new CacheException(ex);
        }
    }

    private void databaseLazyLoad(Locator locator) {
        boolean isPresent = outstandingMetaReads.contains(locator);

        if (!isPresent) {
            metaReads.add(locator);
            outstandingMetaReads.add(locator);
        }

        // Kickoff fetch meta if necessary
        if (metaReads.size() > batchedReadsThreshold) {
            fetchMeta(false);
        }
    }

    private void databaseLazyWrite(Locator locator, String metaKey) {
        CacheKey compoundKey = new CacheKey(locator, metaKey);
        if (outstandingMetaWrites.contains(compoundKey)) {
            return; // already queued up to write.
        }

        outstandingMetaWrites.add(compoundKey);
        metaWrites.add(compoundKey);

        if (metaWrites.size() > batchedWritesThreshold) {
            flushMeta(false);
        }

        return;
    }

    private void fetchMeta(boolean forced) { // Only one thread should ever call into this.
        synchronized (metaReads) {
            if (!forced && metaReads.size() < batchedReadsThreshold) {
                return;
            }

            while (!metaReads.isEmpty()) {
                Set<Locator> batch = new HashSet<Locator>();

                for (int i = 0; !metaReads.isEmpty() && i < batchedReadsThreshold; i++) {
                    batch.add(metaReads.poll()); // poll() is a destructive read (removes the head from the queue).
                }

                readThreadPoolExecutor.submit(new BatchedMetaReadsRunnable(batch));
            }
        }
    }

    private void flushMeta(boolean forced) { // Only one thread should ever call into this.
        synchronized (metaWrites) {
            if (!forced && metaWrites.size() < batchedWritesThreshold) {
                return;
            }

            while (!metaWrites.isEmpty()) {
                Table<Locator, String, String> metaBatch = HashBasedTable.create();

                for (int i = 0; !metaWrites.isEmpty() && i < batchedWritesThreshold; i++) {
                    CacheKey compoundKey = metaWrites.poll(); // destructive read.
                    Locator locator = compoundKey.locator();
                    String metaKey = compoundKey.keyString();
                    String metaVal = cache.getIfPresent(compoundKey);
                    if (metaVal != null) {
                        metaBatch.put(locator, metaKey, metaVal);
                    }
                }

                writeThreadPoolExecutor.submit(new BatchedMetaWritesRunnable(metaBatch));
            }
        }
    }

    private final class CacheKey implements Comparable<CacheKey> {
        private final Locator locator;
        private final String keyString;
        private final int hashCode;

        CacheKey(Locator locator, String keyString) {
            this.locator = locator;
            this.keyString = keyString;
            hashCode = (locator.toString() + "," + keyString).hashCode();
        }

        @Override
        public int hashCode() {
            return hashCode;
        }

        public Locator locator() {
            return locator;
        }

        public String keyString() {
            return keyString;
        }

        @Override
        public boolean equals(Object obj) {
            if (!(obj instanceof CacheKey)) return false;
            CacheKey other = (CacheKey)obj;
            // kind of a cop-out.
            return (locator().equals(other.locator) && keyString().equals(other.keyString()));
        }

        @Override
        public String toString() {
            return locator.toString() + "," + keyString;
        }

        @Override
        public int compareTo(CacheKey o) {
            return this.toString().compareTo(o.toString());
        }
    }

    @Override
    public CacheStats getStats() {
        return cache.stats();
    }

    private class BatchedMetaReadsRunnable implements Runnable {
        private final Set<Locator> locators;

        public BatchedMetaReadsRunnable(Set<Locator> locators) {
            this.locators = locators;
        }

        @Override
        public void run() {
            Timer.Context ctx = batchedReadsTimerMetric.time();
            try {
                Table<Locator, String, String> metaTable = io.getAllValues(locators);
                int metadataRowSize = 0;

                for (Locator locator : metaTable.rowKeySet()) {
                    Map<String, String> metaMapForLocator = metaTable.row(locator);

                    for (Map.Entry<String, String> meta : metaMapForLocator.entrySet()) {
                        CacheKey metaKey = new CacheKey(locator, meta.getKey());
                        String existing = cache.getIfPresent(metaKey);

                        if (existing == null) {
                            cache.put(metaKey, meta.getValue());
                        }

                        boolean differs = existing != null && !existing.equals(meta.getValue());
                        if (differs) {
                            log.warn("Meta " + meta.getKey() + " changed from " + existing + " to " + meta.getValue()
                                    + " for locator " + locator); // delayed audit log.
                            // In this case, do not update the cache. DB has stale data.
                            continue;
                        }

                        metadataRowSize += meta.getKey().getBytes().length + locator.toString().getBytes().length;
                        metadataRowSize += meta.getValue().getBytes().length;
                    }

                    // Got the meta for locator. Remove this from the place holder.
                    outstandingMetaReads.remove(locator);
                }

                totalMetadataSize.update(metadataRowSize);
                // Kickoff fetch meta if necessary
                if (metaReads.size() > batchedReadsThreshold) {
                    fetchMeta(false);
                }
            } catch (Exception ex) {
                // Queue up the locators again (at the end)!
                for (Locator locator : locators) {
                    metaReads.add(locator);
                }
                log.error("Exception reading metadata from db (batched reads)", ex);
            } finally {
                ctx.stop();
            }
        }
    }

    private class BatchedMetaWritesRunnable implements Runnable {
        private final Table<Locator, String, String> metaToWrite;

        public BatchedMetaWritesRunnable(Table<Locator, String, String> metaToWrite) {
            this.metaToWrite = metaToWrite;
        }

        @Override
        public void run() {
            Timer.Context ctx = batchedWritesTimerMetric.time();
            try {
                io.putAll(metaToWrite);
            } catch (Exception ex) {
                log.error("Exception writing metadata to db (batched writes)", ex);
                // Queue up writes at the end.
                for (Locator locator : metaToWrite.rowKeySet()) {
                    Map<String, String> metaMapForLocator = metaToWrite.row(locator);

                    for (Map.Entry<String, String> meta : metaMapForLocator.entrySet()) {
                        CacheKey compoundKey = new CacheKey(locator, meta.getKey());
                        metaWrites.add(compoundKey);
                        // This is fine. We always read the latest value from the real cache. So we'll pull the latest
                        // value to write.
                    }
                }
            } finally {
                ctx.stop();
            }
        }
    }
}


/*
 * Copyright 2014 Rackspace
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.rackspacecloud.blueflood.inputs.handlers;

import com.google.gson.internal.LazilyParsedNumber;
import com.rackspacecloud.blueflood.inputs.formats.AggregatedPayload;
import com.rackspacecloud.blueflood.types.*;
import com.rackspacecloud.blueflood.utils.TimeValue;

import java.io.IOError;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PreaggregateConversions {
    private static final Logger log = LoggerFactory.getLogger(PreaggregateConversions.class);

    // todo: punt on TTL
    private static final TimeValue DEFAULT_TTL = new TimeValue(48, TimeUnit.HOURS);
    private static final String NAME_DELIMITER = "//.";

    // NOTE: when you create objects from gson-converted json, you need to make sure to resolve numbers that
    // are not accessed via `doubleValue()` or `longValue()`, i.e., they are treated as `Number` instances.
    // the Number supplied by gson is and instance of LazilyParsedNumber and will cause breakage in certain
    // circumstances. e.g. calling `longValue()` when the number is obviously a double, or is used in the
    // type comparisions we use to determine how to serialize a number.

    public static Collection<IMetric> buildMetricsCollection(AggregatedPayload payload) {
        Collection<IMetric> metrics = new ArrayList<IMetric>();
        metrics.addAll(PreaggregateConversions.convertCounters(payload.getTenantId(), payload.getTimestamp(), payload.getFlushIntervalMillis(), payload.getCounters()));
        metrics.addAll(PreaggregateConversions.convertGauges(payload.getTenantId(), payload.getTimestamp(), payload.getGauges()));
        metrics.addAll(PreaggregateConversions.convertSets(payload.getTenantId(), payload.getTimestamp(), payload.getSets()));
        metrics.addAll(PreaggregateConversions.convertTimers(payload.getTenantId(), payload.getTimestamp(), payload.getTimers()));
        return metrics;
    }

    public static Collection<PreaggregatedMetric> convertCounters(String tenant, long timestamp, long flushIntervalMillis, Collection<BluefloodCounter> counters) {
        List<PreaggregatedMetric> list = new ArrayList<PreaggregatedMetric>(counters.size());
        for (BluefloodCounter counter : counters) {
            Locator locator = Locator.createLocatorFromPathComponents(tenant, counter.getName().split(NAME_DELIMITER, -1));
            // flushIntervalMillis could be zero (if not specified in the statsD config).
            long sampleCount = flushIntervalMillis > 0
                    ? (long)(counter.getRate().doubleValue() * ((double)flushIntervalMillis/1000d))
                    : 1;
            Rollup rollup = new BluefloodCounterRollup()
                    .withCount(resolveNumber(counter.getValue()))
                    .withRate(counter.getRate().doubleValue())
                    .withSampleCount((int)sampleCount);
            PreaggregatedMetric metric = new PreaggregatedMetric(timestamp, locator, DEFAULT_TTL, rollup);
            list.add(metric);
        }
        return list;
    }

    public static Collection<PreaggregatedMetric> convertGauges(String tenant, long timestamp, Collection<BluefloodGauge> gauges) {
        List<PreaggregatedMetric> list = new ArrayList<PreaggregatedMetric>(gauges.size());
        for (BluefloodGauge gauge : gauges) {
            Locator locator = Locator.createLocatorFromPathComponents(tenant, gauge.getName().split(NAME_DELIMITER, -1));
            Points<SimpleNumber> points = new Points<SimpleNumber>();
            points.add(new Points.Point<SimpleNumber>(timestamp, new SimpleNumber(resolveNumber(gauge.getValue()))));
            try {
                Rollup rollup = BluefloodGaugeRollup.buildFromRawSamples(points);
                PreaggregatedMetric metric = new PreaggregatedMetric(timestamp, locator, DEFAULT_TTL, rollup);
                list.add(metric);
            } catch (IOException ex) {
                throw new IOError(ex);
            }
        }
        return list;
    }

    public static Collection<PreaggregatedMetric> convertTimers(String tenant, long timestamp, Collection<BluefloodTimer> timers) {
        List<PreaggregatedMetric> list = new ArrayList<PreaggregatedMetric>(timers.size());
        for (BluefloodTimer timer : timers) {
            Locator locator = Locator.createLocatorFromPathComponents(tenant, timer.getName().split(NAME_DELIMITER, -1));
            BluefloodTimerRollup rollup = new BluefloodTimerRollup()
                    .withCount(timer.getCount().longValue())
                    .withSampleCount(1)
                    .withAverage(resolveNumber(timer.getAvg() == null ? 0.0d : timer.getAvg()))
                    .withMaxValue(resolveNumber(timer.getMax() == null ? 0.0d : timer.getMax()))
                    .withMinValue(resolveNumber(timer.getMin() == null ? 0.0d : timer.getMin()))
                    .withCountPS(timer.getRate() == null ? 0.0d : timer.getRate().doubleValue())
                    .withSum(timer.getSum() == null ? 0L : timer.getSum().doubleValue())
                    .withVariance(Math.pow(timer.getStd() == null ? 0.0d : timer.getStd().doubleValue(), 2d));
            for (Map.Entry<String, Percentile> entry : timer.getPercentiles().entrySet()) {
                // throw away max and sum.
                if (entry.getValue().getAvg() != null) {
                    rollup.setPercentile(entry.getKey(), resolveNumber(entry.getValue().getAvg()));
                }
            }
            PreaggregatedMetric metric = new PreaggregatedMetric(timestamp, locator, DEFAULT_TTL, rollup);
            list.add(metric);
        }
        return list;
    }

    public static Collection<PreaggregatedMetric> convertSets(String tenant, long timestamp, Collection<BluefloodSet> sets) {
        List<PreaggregatedMetric> list = new ArrayList<PreaggregatedMetric>(sets.size());
        for (BluefloodSet set : sets) {
            Locator locator = Locator.createLocatorFromPathComponents(tenant, set.getName().split(NAME_DELIMITER, -1));
            BluefloodSetRollup rollup = new BluefloodSetRollup();
            for (String value : set.getValues()) {
                rollup = rollup.withObject(value);
            }
            PreaggregatedMetric metric = new PreaggregatedMetric(timestamp, locator, DEFAULT_TTL, rollup);
            list.add(metric);
        }
        return list;
    }

    // resolve a number to a Long or double.
    public static Number resolveNumber(Number n) {
        if (n instanceof LazilyParsedNumber) {
            try {
                return n.longValue();
            } catch (NumberFormatException ex) {
                return n.doubleValue();
            }
        } else {
            // already resolved.
            return n;
        }
    }
}


/*
 * Copyright 2016 Rackspace
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package com.rackspacecloud.blueflood.io.serializers.metrics;

import com.google.protobuf.CodedInputStream;
import com.google.protobuf.CodedOutputStream;

import java.io.IOException;
import java.nio.ByteBuffer;

/**
 * This class knows how to serialize/deserialize StringMetadata objects.
 */
public class StringMetadataSerDes {

    private static final byte STRING = 4; // History: this used to support multiple types.

    public ByteBuffer serialize(String str) {
        try {
            byte[] buf = new byte[computeBufLength(str)];
            CodedOutputStream out = CodedOutputStream.newInstance(buf);
            writeToOutputStream(str, out);
            return ByteBuffer.wrap(buf);
        } catch (IOException e) {
            throw new RuntimeException("Error serializing string metadata", e);
        }
    }

    public String deserialize(ByteBuffer byteBuffer) {
        CodedInputStream is = CodedInputStream.newInstance(byteBuffer.array());
        try {
            byte type = is.readRawByte();
            if (type == STRING) {
                return is.readString();
            } else {
                throw new IOException("Unexpected first byte. Expected '4' (string). Got '" + type + "'.");
            }
        } catch (IOException e) {
            throw new RuntimeException("IOException during deserialization", e);
        }
    }

    // writes object to CodedOutputStream.
    private static void writeToOutputStream(Object obj, CodedOutputStream out) throws IOException {
        out.writeRawByte(STRING);
        out.writeStringNoTag((String)obj);
    }

    // figure out how much space it will take to encode an object. this makes it so that we only allocate one buffer
    // during encode.
    private static int computeBufLength(String obj) throws IOException {
        return 1 + CodedOutputStream.computeStringSizeNoTag(obj); // 1 for type
    }
}


/*
 * Copyright (c) 2016 Rackspace.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * 	http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.rackspacecloud.blueflood.io.astyanax;

import com.codahale.metrics.Timer;
import com.netflix.astyanax.MutationBatch;
import com.netflix.astyanax.connectionpool.exceptions.ConnectionException;
import com.netflix.astyanax.connectionpool.exceptions.NotFoundException;
import com.netflix.astyanax.query.RowQuery;
import com.rackspacecloud.blueflood.io.CassandraModel;
import com.rackspacecloud.blueflood.io.Instrumentation;
import com.rackspacecloud.blueflood.io.LocatorIO;
import com.rackspacecloud.blueflood.types.Locator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Collection;
import java.util.Collections;

/**
 * This class uses the Astyanax driver to read/write locators from
 * Cassandra metrics_locator Column Family.
 */
public class ALocatorIO implements LocatorIO {

    private static final Logger LOG = LoggerFactory.getLogger(ALocatorIO.class);

    /**
     * Insert a locator with key = shard long value calculated using Util.getShard()
     * @param locator
     * @throws IOException
     */
    @Override
    public void insertLocator(Locator locator) throws IOException {
        Timer.Context timer = Instrumentation.getWriteTimerContext(CassandraModel.CF_METRICS_LOCATOR_NAME);
        try {
            MutationBatch mutationBatch = AstyanaxIO.getKeyspace().prepareMutationBatch();
            AstyanaxWriter.getInstance().insertLocator(locator, mutationBatch);
            mutationBatch.execute();
        } catch (Exception e) {
            throw new IOException(e);
        } finally {
            timer.stop();
        }
    }

    /**
     * Returns the locators for a shard, i.e. those that should be rolled up, for a given shard.
     * 'Should' means:
     *  1) A locator is capable of rollup.
     *  2) A locator has had new data in the past LOCATOR_TTL seconds.
     *
     * @param shard Number of the shard you want the locators for. 0-127 inclusive.
     * @return Collection of locators
     * @throws IOException
     */
    @Override
    public Collection<Locator> getLocators(long shard) throws IOException {
        Timer.Context ctx = Instrumentation.getReadTimerContext(CassandraModel.CF_METRICS_LOCATOR_NAME);
        try {
            RowQuery<Long, Locator> query = AstyanaxIO.getKeyspace()
                    .prepareQuery(CassandraModel.CF_METRICS_LOCATOR)
                    .getKey(shard);
            if (LOG.isTraceEnabled())
                LOG.trace("ALocatorIO.getLocators() executing: select * from \"" + CassandraModel.KEYSPACE + "\"." + CassandraModel.CF_METRICS_LOCATOR_NAME + " where key=" + Long.toString(shard));
            return query.execute().getResult().getColumnNames();
        } catch (NotFoundException e) {
            Instrumentation.markNotFound(CassandraModel.CF_METRICS_LOCATOR_NAME);
            return Collections.emptySet();
        } catch (ConnectionException ex) {
            Instrumentation.markReadError(ex);
            LOG.error("Connection exception during getLocators(" + Long.toString(shard) + ")", ex);
            throw new IOException("Error reading locators", ex);
        } finally {
            ctx.stop();
        }
    }

}


/*
 * Copyright (c) 2016 Rackspace.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * 	http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.rackspacecloud.blueflood.io;

import com.codahale.metrics.Meter;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.Multimap;
import com.rackspacecloud.blueflood.cache.CombinedTtlProvider;
import com.rackspacecloud.blueflood.cache.TenantTtlProvider;
import com.rackspacecloud.blueflood.rollup.Granularity;
import com.rackspacecloud.blueflood.types.*;
import com.rackspacecloud.blueflood.utils.Clock;
import com.rackspacecloud.blueflood.utils.Metrics;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collection;
import java.util.Map;

/**
 * This is base class of all MetricsRW classes that deals with persisting/reading
 * data from metrics_{granularity} and metrics_preaggregated_{granularity} column
 * family. This contains some utility methods used/shared amongst various
 * implementation/subclasses of MetricsRW.
 */
public abstract class AbstractMetricsRW implements MetricsRW {

    protected static TenantTtlProvider TTL_PROVIDER = CombinedTtlProvider.getInstance();

    private static final Logger LOG = LoggerFactory.getLogger(AbstractMetricsRW.class);

    protected boolean isRecordingDelayedMetrics;
    protected Clock clock;

    /**
     * Convert a collection of {@link com.rackspacecloud.blueflood.types.IMetric}
     * to a {@link com.google.common.collect.Multimap}
     *
     * @param metrics
     * @return
     */
    protected Multimap<Locator, IMetric> asMultimap(Collection<IMetric> metrics) {
        Multimap<Locator, IMetric> map = LinkedListMultimap.create();
        for (IMetric metric: metrics)
            map.put(metric.getLocator(), metric);
        return map;
    }

    /**
     * Gets the TTL for a particular locator, rollupType and granularity.
     *
     * @param locator
     * @param rollupType
     * @param granularity
     * @return
     */
    protected int getTtl(Locator locator, RollupType rollupType, Granularity granularity) {
        return (int) TTL_PROVIDER.getTTL(locator.getTenantId(),
                    granularity,
                    rollupType).get().toSeconds();
    }

    /**
     * Converts the map of timestamp -> {@link Rollup} to
     * {@link Points} object
     *
     * @param timestampToRollupMap  a map of timestamp to rollup
     * @return
     */
    protected <T extends Object> Points<T> convertToPoints(final Map<Long, T> timestampToRollupMap) {
        Points points =  new Points();
        for (Map.Entry<Long, T> value : timestampToRollupMap.entrySet() ) {

            points.add( createPoint( value.getKey(), value.getValue() ) );
        }
        return points;
    }

    protected Points.Point createPoint( Long timestamp, Object value ) {
        if( value instanceof Rollup )
            return new Points.Point( timestamp, value);
        else
            return new Points.Point( timestamp, new SimpleNumber( value ) );
    }
}


/*
 * Copyright (c) 2016 Rackspace.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * 	http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.rackspacecloud.blueflood.io.datastax;

import com.codahale.metrics.Timer;
import com.datastax.driver.core.*;
import com.datastax.driver.core.querybuilder.Insert;
import com.datastax.driver.core.querybuilder.QueryBuilder;
import com.datastax.driver.core.querybuilder.Select;
import com.rackspacecloud.blueflood.cache.TenantTtlProvider;
import com.rackspacecloud.blueflood.io.CassandraModel;
import com.rackspacecloud.blueflood.io.Instrumentation;
import com.rackspacecloud.blueflood.io.LocatorIO;
import com.rackspacecloud.blueflood.rollup.Granularity;
import com.rackspacecloud.blueflood.rollup.SlotKey;
import com.rackspacecloud.blueflood.types.Locator;
import com.rackspacecloud.blueflood.utils.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import static com.datastax.driver.core.querybuilder.QueryBuilder.bindMarker;
import static com.datastax.driver.core.querybuilder.QueryBuilder.eq;
import static com.datastax.driver.core.querybuilder.QueryBuilder.ttl;

/**
 * This class uses the Datastax driver to read/write locators from
 * Cassandra metrics_locator Column Family.
 */
public class DLocatorIO implements LocatorIO {

    private static final Logger LOG = LoggerFactory.getLogger(DLocatorIO.class);

    private static final String KEY = "key";
    private static final String COLUMN1 = "column1";
    private static final String VALUE = "value";

    private PreparedStatement getValue;
    private PreparedStatement putValue;

    /**
     * Constructor
     */
    public DLocatorIO() {
        createPreparedStatements();
    }

    /**
     * Create all prepared statements use in this class for metrics_locator
     */
    private void createPreparedStatements()  {

        // create a generic select statement for retrieving from metrics_locator
        Select.Where select = QueryBuilder
                .select()
                .all()
                .from( CassandraModel.CF_METRICS_LOCATOR_NAME )
                .where( eq ( KEY, bindMarker() ));
        getValue = DatastaxIO.getSession().prepare( select );

        // create a generic insert statement for inserting into metrics_locator
        Insert insert = QueryBuilder.insertInto( CassandraModel.CF_METRICS_LOCATOR_NAME)
                .using(ttl(TenantTtlProvider.LOCATOR_TTL))
                .value(KEY, bindMarker())
                .value(COLUMN1, bindMarker())
                .value(VALUE, bindMarker());
        putValue = DatastaxIO.getSession()
                .prepare(insert)
                .setConsistencyLevel( ConsistencyLevel.LOCAL_ONE );
    }

    /**
     * Insert a locator with key = shard long value calculated using Util.getShard()
     * @param locator
     * @throws IOException
     */
    @Override
    public void insertLocator(Locator locator) throws IOException {
        Session session = DatastaxIO.getSession();

        Timer.Context timer = Instrumentation.getWriteTimerContext(CassandraModel.CF_METRICS_LOCATOR_NAME);
        try {
            // bound values and execute
            BoundStatement bs = getBoundStatementForLocator(locator);
            session.execute(bs);
        } finally {
            timer.stop();
        }
    }

    // package protect so other classes in this package can call it
    BoundStatement getBoundStatementForLocator(Locator locator) {
        // get shard this locator would belong to
        long shard = (long) Util.getShard(locator.toString());
        return putValue.bind(shard, locator.toString(), "");
    }

    /**
     * Returns the locators for a shard, i.e. those that should be rolled up, for a given shard.
     * 'Should' means:
     *  1) A locator is capable of rollup.
     *  2) A locator has had new data in the past LOCATOR_TTL seconds.
     *
     * @param shard Number of the shard you want the locators for. 0-127 inclusive.
     * @return Collection of locators
     * @throws IOException
     */
    @Override
    public Collection<Locator> getLocators(long shard) throws IOException {

        Timer.Context ctx = Instrumentation.getReadTimerContext(CassandraModel.CF_METRICS_LOCATOR_NAME);
        Session session = DatastaxIO.getSession();

        Collection<Locator> locators = new ArrayList<Locator>();

        try {
            // bind value
            BoundStatement bs = getValue.bind(shard);
            List<Row> results = session.execute(bs).all();
            for ( Row row : results ) {
                if ( LOG.isTraceEnabled() ) {
                    LOG.trace( "Read metrics_locators with shard " + shard + ": " +
                            row.getString( KEY ) +
                            row.getString( COLUMN1 ));
                }
                locators.add(Locator.createLocatorFromDbKey(row.getString(COLUMN1)));
            }

            // return results
            if (locators.size() == 0) {
                Instrumentation.markNotFound(CassandraModel.CF_METRICS_LOCATOR_NAME);
                return Collections.emptySet();
            }
            else {
                return locators;
            }
        } finally {
            ctx.stop();
        }
    }

}


/*
 * Copyright 2013-2015 Rackspace
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.rackspacecloud.blueflood.tracker;

import com.rackspacecloud.blueflood.http.HttpRequestWithDecodedQueryParams;
import com.rackspacecloud.blueflood.io.Constants;
import com.rackspacecloud.blueflood.types.Metric;

import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.management.MBeanServer;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Tracker implements TrackerMBean {

    public static final String trackerName = String.format("com.rackspacecloud.blueflood.tracker:type=%s", Tracker.class.getSimpleName());

    private static final Logger log = LoggerFactory.getLogger(Tracker.class);

    private static final String EMPTY_STRING = "";

    private final Pattern patternGetTid = Pattern.compile( "/v\\d+\\.\\d+/([^/]+)/.*" );
    private DateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    // Tracker is a singleton
    private static final Tracker instance = new Tracker();
    private boolean isRegistered = false;

    private Set tenantIds = new HashSet();
    private boolean isTrackingDelayedMetrics = false;
    private Set<String> metricNames = new HashSet<String>();

    // private constructor for singleton
    private Tracker(){
        // set dateFormatter to GMT since collectionTime for metrics should always be GMT/UTC
        dateFormatter.setTimeZone(TimeZone.getTimeZone("GMT"));
    }

    public static Tracker getInstance() {
        return instance;
    }

    public synchronized void register() {
        if (isRegistered) return;

        try {
            ObjectName objectName = new ObjectName(trackerName);

            // register TrackerMBean only if not already registered
            MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
            if (!mBeanServer.isRegistered(objectName)) {
                ManagementFactory.getPlatformMBeanServer().registerMBean(instance, objectName);
            }

            isRegistered = true;
            log.info("MBean registered as " + trackerName);

        } catch (Exception exc) {
            log.error("Unable to register MBean " + trackerName, exc);
        }
    }

    public void addTenant(String tenantId) {
        tenantIds.add(tenantId);
        log.info("[TRACKER] tenantId " + tenantId + " added.");
    }

    public void removeTenant(String tenantId) {
        tenantIds.remove(tenantId);
        log.info("[TRACKER] tenantId " + tenantId + " removed.");
    }

    public void removeAllTenants() {
        tenantIds.clear();
        log.info("[TRACKER] all tenants removed.");
    }

    public Set getTenants() {
        return tenantIds;
    }

    public void addMetricName(String metricName) {
        metricNames.add(metricName);
        log.info("[TRACKER] Metric name "+ metricName + " added.");
    }

    public void removeMetricName(String metricName) {
        metricNames.remove(metricName);
        log.info("[TRACKER] Metric name "+ metricName + " removed.");
    }

    public void removeAllMetricNames() {
        metricNames.clear();
        log.info("[TRACKER] All metric names removed.");
    }

    public Set<String> getMetricNames() {
        return metricNames;
    }

    public void setIsTrackingDelayedMetrics() {
        isTrackingDelayedMetrics = true;
        log.info("[TRACKER] Tracking delayed metrics started");
    }

    public void resetIsTrackingDelayedMetrics() {
        isTrackingDelayedMetrics = false;
        log.info("[TRACKER] Tracking delayed metrics stopped");
    }

    public boolean getIsTrackingDelayedMetrics() {
        return isTrackingDelayedMetrics;
    }

    public boolean isTracking(String tenantId) {
        return tenantIds.contains(tenantId);
    }

    public boolean doesMessageContainMetricNames(String logmessage) {
        boolean toLog = false;

        if (metricNames.size() == 0) {
            toLog = true;
        }
        else {
            for (String name : metricNames) {
                if (logmessage.contains(name)) {
                    toLog = true;
                    break;
                }
            }
        }
        return toLog;
    }

    public void track(HttpRequest request) {
        // check if tenantId is being tracked by JMX TenantTrackerMBean and log the request if it is
        if (request == null) return;

        String tenantId = findTid( request.getUri() );

        if (isTracking(tenantId)) {

            // get headers
            String headers = EMPTY_STRING;
            for (String headerName : request.headers().names()) {
                headers += "\n" + headerName + "\t" + request.headers().get(headerName);
            }

            // get parameters
            String queryParams = getQueryParameters(request);

            // get request content
            String requestContent = EMPTY_STRING;
            if ( request instanceof FullHttpRequest ) {
                FullHttpRequest fullReq = (FullHttpRequest)request;
                requestContent = fullReq.content().toString(Constants.DEFAULT_CHARSET);
                if ((requestContent != null) && (!requestContent.isEmpty())) {
                    requestContent = "\nREQUEST_CONTENT:\n" + requestContent;
                }
            }

            // log request
            String logMessage = "[TRACKER] " +
                    request.getMethod() + " request for tenantId " + tenantId + ": " + request.getUri() + queryParams + "\n" +
                    "HEADERS: " + headers +
                    requestContent;

            if (doesMessageContainMetricNames(logMessage)) {
                log.info(logMessage);
            }
        }
    }

    public void trackResponse(HttpRequest request, FullHttpResponse response) {
        // check if tenantId is being tracked by JMX TenantTrackerMBean and log the response if it is
        // HttpRequest is needed for original request uri and tenantId
        if (request == null) return;
        if (response == null) return;

        String tenantId = findTid( request.getUri() );
        if (isTracking(tenantId)) {
            HttpResponseStatus status = response.getStatus();
            String messageBody = response.content().toString(Constants.DEFAULT_CHARSET);

            // get parameters
            String queryParams = getQueryParameters(request);

            // get headers
            String headers = "";
            for (String headerName : response.headers().names()) {
                headers += "\n" + headerName + "\t" + response.headers().get(headerName);
            }

            // get response content
            String responseContent = "";
            if ((messageBody != null) && (!messageBody.isEmpty())) {
                responseContent = "\nRESPONSE_CONTENT:\n" + messageBody;
            }

            String logMessage = "[TRACKER] " +
                    "Response for tenantId " + tenantId + " " + request.getMethod() + " request " + request.getUri() + queryParams +
                    "\nRESPONSE_STATUS: " + status.code() +
                    "\nRESPONSE HEADERS: " + headers +
                    responseContent;

            log.info(logMessage);
        }

    }

    /**
     * This method is used to log delayed metrics, if tracking delayed metrics
     * is turned on for this Blueflood service.
     * @param tenantid
     * @param delayedMetrics
     */
    public void trackDelayedMetricsTenant(String tenantid, final List<Metric> delayedMetrics) {
        if (isTrackingDelayedMetrics) {
            String logMessage = String.format("[TRACKER][DELAYED METRIC] Tenant sending delayed metrics %s", tenantid);
            log.info(logMessage);

            // log individual delayed metrics locator and collectionTime
            double delayedMinutes;
            long nowMillis = System.currentTimeMillis();
            for (Metric metric : delayedMetrics) {
                delayedMinutes = (double)(nowMillis - metric.getCollectionTime()) / 1000 / 60;
                logMessage = String.format("[TRACKER][DELAYED METRIC] %s has collectionTime %s which is delayed by %.2f minutes",
                        metric.getLocator().toString(),
                        dateFormatter.format(new Date(metric.getCollectionTime())),
                        delayedMinutes);
                log.info(logMessage);
            }
        }
    }

    /**
     * This method logs the delayed aggregated metrics for a particular tenant,
     * if tracking delayed metric is turned on for this Blueflood service.
     * Aggregated metrics have one single timestamp for the group of metrics that
     * are sent in one request.
     *
     * @param tenantId              the tenantId who's the sender of the metrics
     * @param collectionTimeMs      the collection timestamp (ms) in request payload
     * @param delayTimeMs           the delayed time (ms)
     * @param delayedMetricNames    the list of delayed metrics in request payload
     */
    public void trackDelayedAggregatedMetricsTenant(String tenantId, long collectionTimeMs, long delayTimeMs, List<String> delayedMetricNames) {
        if (isTrackingDelayedMetrics) {
            String logMessage = String.format("[TRACKER][DELAYED METRIC] Tenant sending delayed metrics %s", tenantId);
            log.info(logMessage);

            // log individual delayed metrics locator and collectionTime
            double delayMin = delayTimeMs / 1000 / 60;
            logMessage = String.format("[TRACKER][DELAYED METRIC] %s have collectionTime %s which is delayed by %.2f minutes",
                    StringUtils.join(delayedMetricNames, ","),
                        dateFormatter.format(new Date(collectionTimeMs)),
                        delayMin);
            log.info(logMessage);
        }
    }

    String getQueryParameters(HttpRequest httpRequest) {
        String params = "";
        Map<String, List<String>> parameters = ((HttpRequestWithDecodedQueryParams) httpRequest).getQueryParams();

        for (Map.Entry<String, List<String>> param : parameters.entrySet()) {
            String paramName = param.getKey();
            List<String> paramValues = param.getValue();

            for (String paramValue : paramValues) {
                if (!params.equals("")) {
                    params += "&";
                }
                params += paramName + "=" + paramValue;
            }
        }

        if (!params.equals("")) params = "?" + params;
        return params;
    }

    String findTid( String uri ) {

        Matcher m = patternGetTid.matcher( uri );

        if( m.matches() )
            return m.group( 1 );
        else
            return null;
    }
}


/*
 * Copyright 2016 Rackspace
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package com.rackspacecloud.blueflood.io.serializers.metrics;

import com.google.protobuf.CodedInputStream;
import com.google.protobuf.CodedOutputStream;
import com.rackspacecloud.blueflood.io.Constants;
import com.rackspacecloud.blueflood.types.*;

import java.io.IOException;

/**
 * This is an abstract class that most Serialization/Deserialization class
 * extends from. It contains some utility methods.
 */
public abstract class AbstractSerDes {

    protected static StatsSerDes<Average> averageStatDeSer = new StatsSerDes<Average>();
    protected static StatsSerDes<MinValue> minStatDeSer = new StatsSerDes<MinValue>();
    protected static StatsSerDes<MaxValue> maxStatDeSer = new StatsSerDes<MaxValue>();
    protected static StatsSerDes<Variance> varianceStatDeSer = new StatsSerDes<Variance>();

    protected void putRollupStat(AbstractRollupStat stat, CodedOutputStream protobufOut) throws IOException {
        protobufOut.writeRawByte(stat.getStatType());   // stat type
        protobufOut.writeRawByte(stat.isFloatingPoint() ? Constants.B_DOUBLE : Constants.B_I64);

        if (stat.isFloatingPoint()) {
            protobufOut.writeDoubleNoTag(stat.toDouble());
        } else {
            protobufOut.writeRawVarint64(stat.toLong());
        }
    }

    // these two sizeOf*() methods are created mainly
    // for code readability in the subclasses

    /**
     * @return the number of byte(s) used in the serialized byte buffer
     * to indicate the size of the byte buffer. The size of the byte
     * buffer is the first byte in the buffer.
     */
    protected final int sizeOfSize() {
        return 1;
    }

    /**
     * @return the number of byte(s) used in the serialized byte buffer
     * to indicate the type of the object being serialized. The type
     * of the object is the second byte in the buffer.
     */
    protected final int sizeOfType() {
        return 1;
    }

    // read out a type-specified number.
    protected Number getUnversionedDoubleOrLong(CodedInputStream in) throws IOException {
        byte type = in.readRawByte();
        if (type == Constants.B_DOUBLE)
            return in.readDouble();
        else
            return in.readRawVarint64();
    }

    // put out a number prefaced only by a type.
    protected void putUnversionedDoubleOrLong(Number number, CodedOutputStream out) throws IOException {
        if (number instanceof Double) {
            out.writeRawByte(Constants.B_DOUBLE);
            out.writeDoubleNoTag(number.doubleValue());
        } else {
            out.writeRawByte(Constants.B_I64);
            out.writeRawVarint64(number.longValue());
        }
    }
}


/*
 * Copyright 2013 Rackspace
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.rackspacecloud.blueflood.service;

import com.google.common.collect.Lists;

import java.io.IOException;
import java.net.URL;
import java.util.*;

public class Configuration {
    private static final Properties defaultProps = new Properties();
    private static Properties props;
    private static final Configuration INSTANCE = new Configuration();

    public static Configuration getInstance() {
        return INSTANCE;
    }

    private Configuration() {
        try {
            init();
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    public void loadDefaults(ConfigDefaults[] configDefaults) {
        for (ConfigDefaults configDefault : configDefaults) {
            defaultProps.setProperty(configDefault.toString(), configDefault.getDefaultValue());
        }
    }

    public void init() throws IOException {
        props = new Properties(defaultProps);
        // load the configuration.
        String configStr = System.getProperty("blueflood.config");
        if (configStr != null) {
            URL configUrl = new URL(configStr);
            props.load(configUrl.openStream());
        }
    }

    /**
     * Convert the existing configuration values into a Map. Does not include defaults from the defaultProps object.
     *
     * @return an unmodifiable Map
     */
    public Map<Object,Object> getProperties() {
        return Collections.unmodifiableMap(props);
    }

    /**
     * Convert the existing configuration values into a Map, including those specified in defaultProps.
     *
     * @return an unmodifiable Map
     */
    public Map<Object, Object> getAllProperties() {

        Map<Object, Object> map = new HashMap<Object, Object>();
        for (Object key : defaultProps.keySet()) {
            map.put(key, defaultProps.getProperty(key.toString()));
        }
        for (Object key : props.keySet()) {
            map.put(key, props.getProperty(key.toString()));
        }

        return Collections.unmodifiableMap(map);
    }

    public String getStringProperty(Enum<? extends ConfigDefaults> name) {
        return getStringProperty(name.toString());
    }
    public String getStringProperty(String name) {
        if (System.getProperty(name) != null && !props.containsKey("original." + name)) {
            if (props.containsKey(name))
                props.put("original." + name, props.get(name));
            props.put(name, System.getProperty(name));
        }
        return props.getProperty(name);
    }

    public String getRawStringProperty(Enum<? extends ConfigDefaults> name) {
        return getRawStringProperty(name.toString());
    }
    public String getRawStringProperty(String name) {
        return props.getProperty(name);
    }

    public boolean containsKey(String key) {
        return props.containsKey(key);
    }

    public int getIntegerProperty(Enum<? extends ConfigDefaults> name) {
        return getIntegerProperty(name.toString());
    }
    public int getIntegerProperty(String name) {
        return intFromString(getStringProperty(name));
    }
    public static int intFromString(String value) {
        return Integer.parseInt(value);
    }

    public float getFloatProperty(Enum<? extends ConfigDefaults> name) {
        return getFloatProperty(name.toString());
    }
    public float getFloatProperty(String name) {
        return floatFromString(getStringProperty(name));
    }
    public static float floatFromString(String value) {
        return Float.parseFloat(value);
    }

    public long getLongProperty(Enum<? extends ConfigDefaults> name) {
        return getLongProperty(name.toString());
    }
    public long getLongProperty(String name) {
        return longFromString(getStringProperty(name));
    }
    public static long longFromString(String value) {
        return Long.parseLong(value);
    }

    public boolean getBooleanProperty(Enum<? extends ConfigDefaults> name) {
        return getBooleanProperty(name.toString());
    }
    public boolean getBooleanProperty(String name) {
        return booleanFromString(getStringProperty(name));
    }
    public static boolean booleanFromString(String value) {
        return "true".equalsIgnoreCase(value);
    }

    public List<String> getListProperty(Enum<? extends ConfigDefaults> name) {
        return getListProperty(name.toString());
    }
    public List<String> getListProperty(String name) {
        return stringListFromString(getStringProperty(name));
    }
    public static List<String> stringListFromString(String value) {
        List<String> list = Lists.newArrayList(value.split("\\s*,\\s*"));
        list.removeAll(Arrays.asList("", null));
        return list;
    }

    public void setProperty(String name, String val) {
      props.setProperty(name, val);
    }
    public void setProperty(Enum<? extends ConfigDefaults> name, String value) {
        setProperty(name.toString(), value);
    }

    public void clearProperty(String name) {
        props.remove(name);
    }
    public void clearProperty(Enum<? extends ConfigDefaults> name) {
        clearProperty(name.toString());
    }
}


/*
 * Copyright 2014 Rackspace
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.rackspacecloud.blueflood.outputs.cloudfiles;

import com.codahale.metrics.Timer;
import com.google.common.io.Closeables;
import com.rackspacecloud.blueflood.service.CloudfilesConfig;
import com.rackspacecloud.blueflood.service.Configuration;
import com.rackspacecloud.blueflood.utils.Metrics;
import org.jclouds.ContextBuilder;
import org.jclouds.blobstore.BlobStore;
import org.jclouds.blobstore.BlobStoreContext;
import org.jclouds.blobstore.domain.Blob;
import org.jclouds.location.reference.LocationConstants;

import java.io.Closeable;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Properties;

public class CloudFilesPublisher implements Closeable {
    private final BlobStore blobStore;

    public static final String PROVIDER = "cloudfiles-us";
    public static final String ZONE;

    public static final String USERNAME;
    public static final String API_KEY;
    public static final SimpleDateFormat CONTAINER_DATE_FORMAT;
    public static final Timer uploadTimer = Metrics.timer(CloudFilesPublisher.class, "Rollup Upload Timer");
    private String lastContainerCreated = "";

    static {
        Configuration conf = Configuration.getInstance();
        USERNAME = conf.getStringProperty(CloudfilesConfig.CLOUDFILES_USERNAME);
        API_KEY = conf.getStringProperty(CloudfilesConfig.CLOUDFILES_API_KEY);
        CONTAINER_DATE_FORMAT = new SimpleDateFormat(conf.getStringProperty(CloudfilesConfig.CLOUDFILES_CONTAINER_FORMAT));
        ZONE = conf.getStringProperty(CloudfilesConfig.CLOUDFILES_ZONE);
    }

    public CloudFilesPublisher() {
        Properties overrides = new Properties();
        overrides.setProperty(LocationConstants.PROPERTY_ZONE, ZONE);

        BlobStoreContext context = ContextBuilder.newBuilder(PROVIDER)
                .credentials(USERNAME, API_KEY)
                .overrides(overrides)
                .buildView(BlobStoreContext.class);
        blobStore = context.getBlobStore();
    }

    // idempotent other than when the month changes between two calls
    private void createContainer() {
        String containerName = CONTAINER_DATE_FORMAT.format(new Date());
        blobStore.createContainerInLocation(null, containerName);
        lastContainerCreated = containerName;
    }

    public void close() throws IOException {
        Closeables.close(blobStore.getContext(), true);
    }

    public void publish(String remoteName, byte[] payload) throws IOException {
        Timer.Context ctx = uploadTimer.time();
        try {
            Blob blob = blobStore.blobBuilder(remoteName).payload(payload)
                    .contentType("application/json")
                    .contentEncoding(remoteName.endsWith(".gz") ? "gzip" : "identity")
                    .calculateMD5().build();

            String containerName = CONTAINER_DATE_FORMAT.format(new Date());
            if (!lastContainerCreated.matches(containerName)) {
                createContainer();
            }
            blobStore.putBlob(containerName, blob);
        } finally {
            ctx.stop();
        }
    }
}


/*
 * Copyright (c) 2016 Rackspace.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * 	http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.rackspacecloud.blueflood.io.datastax;

import com.datastax.driver.core.BoundStatement;
import com.datastax.driver.core.PreparedStatement;
import com.rackspacecloud.blueflood.exceptions.InvalidDataException;
import com.rackspacecloud.blueflood.io.serializers.metrics.CounterSerDes;
import com.rackspacecloud.blueflood.rollup.Granularity;
import com.rackspacecloud.blueflood.types.*;

import java.nio.ByteBuffer;

/**
 * This class holds the utility methods to read/write counter metrics
 * using Datastax driver.
 */
public class DCounterIO extends DAbstractMetricIO {

    private CounterSerDes serDes = new CounterSerDes();

    /**
     * Provides a way for the sub class to get a {@link java.nio.ByteBuffer}
     * representation of a certain Rollup object.
     *
     * @param value
     * @return
     */
    @Override
    protected ByteBuffer toByteBuffer(Object value) {
        if ( ! (value instanceof BluefloodCounterRollup) ) {
            throw new IllegalArgumentException("toByteBuffer(): expecting BluefloodCounterRollup class but got " + value.getClass().getSimpleName());
        }
        BluefloodCounterRollup counterRollup = (BluefloodCounterRollup) value;
        return serDes.serialize(counterRollup);
    }

    /**
     * Provides a way for the sub class to construct the right Rollup
     * object from a {@link java.nio.ByteBuffer}
     *
     * @param byteBuffer
     * @return
     */
    @Override
    protected BluefloodCounterRollup fromByteBuffer(ByteBuffer byteBuffer) {
        return serDes.deserialize(byteBuffer);
    }

    /**
     * Retrieves the {@link BoundStatement} for a particular counter metric
     * @param metric
     * @param granularity
     * @return
     */
    @Override
    protected BoundStatement getBoundStatementForMetric(IMetric metric, Granularity granularity) {
        Object metricValue = metric.getMetricValue();
        if (!(metricValue instanceof BluefloodCounterRollup)) {
            throw new InvalidDataException(
                    String.format("getBoundStatementForMetric(locator=%s, granularity=%s): metric value %s is not type BluefloodCounterRollup",
                            metric.getLocator(), granularity, metric.getMetricValue().getClass().getSimpleName())
            );
        }
        PreparedStatement statement = metricsCFPreparedStatements.preaggrGranToInsertStatement.get(granularity);
        return statement.bind(
                metric.getLocator().toString(),
                metric.getCollectionTime(),
                serDes.serialize( (BluefloodCounterRollup) metricValue ),
                metric.getTtlInSeconds() );
    }
}


/*
 * Copyright 2013 Rackspace
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package com.rackspacecloud.blueflood.service;

import com.codahale.metrics.Meter;
import com.codahale.metrics.Timer;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ticker;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.rackspacecloud.blueflood.io.Constants;
import com.rackspacecloud.blueflood.rollup.Granularity;
import com.rackspacecloud.blueflood.rollup.SlotKey;
import com.rackspacecloud.blueflood.utils.Clock;
import com.rackspacecloud.blueflood.utils.DefaultClockImpl;
import com.rackspacecloud.blueflood.utils.Metrics;
import com.rackspacecloud.blueflood.utils.TimeValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.management.MBeanServer;
import javax.management.ObjectName;
import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;

/**
 *
 * The {@code ScheduleContext} class coordinates access to the states of slots.
 * It mediates between the rollup service, the ingestion service, and the
 * database.
 *

 *
 * The class's chief purpose is to coordinate access to slots' states and the
 * list of slots that need to be re-rolled between {@link RollupService},
 * {@link IngestionService}, and the database. There's several other classes
 * and components involved, but those are the most important.
 *

 *
 * When ingestion services receive incoming metrics, they will change the
 * associated slot's state to
 * {@link com.rackspacecloud.blueflood.service.UpdateStamp.State#Active Active}
 * (needs to be re-rolled) by calling {@link #update(long, int)}. That state
 * information will be persisted to the database by the
 * {@link ShardStatePusher} class. On the rollup nodes, the
 * {@link ShardStatePuller} will read the state info into memory by calling
 * {@link com.rackspacecloud.blueflood.service.ShardStateManager.SlotStateManager#updateSlotOnRead(SlotState)}.
 * The rollup service will then identity slots that need to be re-rolled (by
 * calling {@link #scheduleEligibleSlots(long, long, long)}), pick a slot to rollup and
 * mark it as
 * {@link com.rackspacecloud.blueflood.service.UpdateStamp.State#Running Running}
 * (via {@link #getNextScheduled()} ), do the rollup, and then mark the slot as
 * {@link com.rackspacecloud.blueflood.service.UpdateStamp.State#Rolled Rolled}
 * (via {@link #clearFromRunning(SlotKey)}).
 *

 *
 * If doing the rollup fails for some reason, the rollup service will call
 * {@link #pushBackToScheduled(SlotKey, boolean)}, to return the slot to the
 * queue of slots to be rolled.
 *

 *
 * There are additional methods for managing shards ({@link #addShard(int)} and
 * {@link #removeShard(int)}) which don't appear to be used.
 *

 *
 * There are also methods which provide information about the state of things
 * (e.g. {@link #getScheduledCount()},
 * {@link #getSlotStamps(Granularity, int)},
 * {@link #getRecentlyScheduledShards()},
 * {@link #getMetricsState(int, String, int)}) which either aren't used, are
 * only used for testing, or are used to provide information to external system
 * via JMX or yammer metrics.
 *

 *
 *
 * Previous comments, left in case we need them:
 * Keeps track of dirty slots in memory. Operations must be threadsafe.
 *
 * todo: explore using ReadWrite locks (might not make a difference).
 *
 * Each node is responsible for sharded slots (time ranges) of rollups. This
 * class keeps track of the execution of those rollups and the states they are
 * in.
 *
 * When synchronizing multiple collections, do it in this order: scheduled -> running.
 */
public class ScheduleContext implements IngestionContext, ScheduleContextMBean {
    private static final Logger log = LoggerFactory.getLogger(ScheduleContext.class);
    private final Timer markSlotDirtyTimer = Metrics.timer(ScheduleContext.class, "Slot Mark Dirty Duration");

    private final ShardStateManager shardStateManager;
    private transient long scheduleTime = 0L;

    /** these shards have been scheduled in the last 10 minutes. */
    private final Cache<Integer, Long> recentlyScheduledShards = CacheBuilder.newBuilder()
            .maximumSize(Constants.NUMBER_OF_SHARDS)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .build();

    // state
    //
    private final Meter shardOwnershipChanged = Metrics.meter(ScheduleContext.class, "Shard Change Before Running");

    /**
     * these are all the slots that are scheduled to run in no particular order.
     * the collection is synchronized to control updates.
     */
    private final Set<SlotKey> scheduledSlots = new HashSet<SlotKey>();

    /**
     * same information as {@link #scheduledSlots}, but order is preserved. The
     * ordered property is only needed for getting the the next scheduled slot,
     * but most operations are concerned with if a slot is scheduled or not.
     * When you update one, you must update the other.
     */
    private final List<SlotKey> orderedScheduledSlots = new ArrayList<SlotKey>();

    /** slots that are running are not scheduled. */
    private final Map<SlotKey, Long> runningSlots = new HashMap<SlotKey, Long>();

    /** shard lock manager */
    private final ShardLockManager lockManager;

    private final Clock clock;

    public ScheduleContext(long currentTimeMillis, Collection<Integer> managedShards, Clock clock) {
        this.scheduleTime = currentTimeMillis;
        this.shardStateManager = new ShardStateManager(managedShards, asMillisecondsSinceEpochTicker(), clock);
        this.lockManager = new NoOpShardLockManager();
        this.clock = clock;
        registerMBean();
    }

    public ScheduleContext(long currentTimeMillis, Collection<Integer> managedShards, String zookeeperCluster) {
        this.scheduleTime = currentTimeMillis;
        this.shardStateManager = new ShardStateManager(managedShards, asMillisecondsSinceEpochTicker());
        ZKShardLockManager lockManager = new ZKShardLockManager(zookeeperCluster, new HashSet<Integer>(shardStateManager.getManagedShards()));
        lockManager.init(new TimeValue(5, TimeUnit.SECONDS));
        this.lockManager = lockManager;
        this.clock = new DefaultClockImpl();
        registerMBean();
    }

    public ScheduleContext(long currentTimeMillis, Collection<Integer> managedShards) {
        this(currentTimeMillis, managedShards, new DefaultClockImpl());
    }

    @VisibleForTesting
    public ScheduleContext(long currentTimeMillis,
                           Collection<Integer> managedShards,
                           Clock clock,
                           ShardStateManager shardStateManager,
                           ShardLockManager shardLockManager) {
        this.scheduleTime = currentTimeMillis;
        this.shardStateManager = shardStateManager;
        this.lockManager = shardLockManager;
        this.clock = clock;
        registerMBean();
    }

    public void setCurrentTimeMillis(long millis){ scheduleTime = millis; }
    public long getCurrentTimeMillis() { return scheduleTime; }

    public ShardStateManager getShardStateManager() {
        return this.shardStateManager;
    }

    /**
     * {@inheritDoc}
     */
    public void update(long millis, int shard) {
        // there are two update paths. for managed shards, we must guard the
        // scheduled and running collections. but for unmanaged shards, we just
        // let the update happen uncontested.
        final Timer.Context dirtyTimerCtx = markSlotDirtyTimer.time();
        try {
            if (log.isTraceEnabled()) {
                log.trace("Updating {} to {}", shard, millis);
            }
            boolean isManaged = shardStateManager.contains(shard);
            for (Granularity g : Granularity.rollupGranularities()) {
                ShardStateManager.SlotStateManager slotStateManager = shardStateManager.getSlotStateManager(shard, g);
                int slot = g.slot(millis);

                if (isManaged) {
                    synchronized (scheduledSlots) { //put
                        SlotKey key = SlotKey.of(g, slot, shard);
                        if (scheduledSlots.remove(key) && log.isDebugEnabled()) {
                            // don't worry about orderedScheduledSlots
                            log.debug("descheduled {}.", key);
                        }
                    }
                }
                slotStateManager.createOrUpdateForSlotAndMillisecond(slot, millis);
            }
        } finally {
            dirtyTimerCtx.stop();
        }
    }

    /**
     * Loop through all slots that are eligible for rollup, at all
     * granularities, in all managed shards. If any are found that are not
     * already running or scheduled, then add them to the queue of scheduled
     * slots.
     *
     * Note that {@code maxAgeMillis}, {@code rollupDelayForMetricsWithShortDelay}
     * {@code rollupWaitForMetricsWithLongDelay} are age values, not a timestamp.
     *
     */
    // only one thread should be calling in this puppy.
    void scheduleEligibleSlots(long maxAgeMillis, long rollupDelayForMetricsWithShortDelay, long rollupWaitForMetricsWithLongDelay) {
        long now = scheduleTime;
        ArrayList<Integer> shardKeys = new ArrayList<Integer>(shardStateManager.getManagedShards());
        Collections.shuffle(shardKeys);

        for (int shard : shardKeys) {
            for (Granularity g : Granularity.rollupGranularities()) {
                // sync on map since we do not want anything added to or taken from it while we iterate.
                synchronized (scheduledSlots) { // read
                    synchronized (runningSlots) { // read

                        List<Integer> slotsToWorkOn = shardStateManager.getSlotStateManager(shard, g)
                                .getSlotsEligibleForRollup(now, maxAgeMillis, rollupDelayForMetricsWithShortDelay, rollupWaitForMetricsWithLongDelay);

                        if (slotsToWorkOn.size() == 0) {
                            continue;
                        }
                        if (!canWorkOnShard(shard)) {
                            continue;
                        }

                        for (Integer slot : slotsToWorkOn) {
                            SlotKey slotKey = SlotKey.of(g, slot, shard);
                            if (areChildKeysOrSelfKeyScheduledOrRunning(slotKey)) {
                                continue;
                            }
                            SlotKey key = SlotKey.of(g, slot, shard);
                            scheduledSlots.add(key);
                            orderedScheduledSlots.add(key);
                            recentlyScheduledShards.put(shard, scheduleTime);
                        }
                    }
                }
            }
        }
    }

    boolean isReroll(SlotKey slotKey) {
        return shardStateManager.getSlotStateManager(slotKey.getShard(), slotKey.getGranularity())
                .isReroll(slotKey.getSlot(), scheduleTime);
    }

    boolean areChildKeysOrSelfKeyScheduledOrRunning(SlotKey slotKey) {
        // if any ineligible (children and self) keys are running or scheduled to run, we shouldn't work on this.
        Collection<SlotKey> ineligibleKeys = slotKey.getChildrenKeys();

        if (runningSlots.keySet().contains(slotKey)) {
            return true;
        }
        if (scheduledSlots.contains(slotKey)) {
            return true;
        }

        // if any ineligible keys are running or scheduled to run, do not schedule this key.
        for (SlotKey childrenKey : ineligibleKeys) {
            if (runningSlots.keySet().contains(childrenKey)) {
                return true;
            }
            if (scheduledSlots.contains(childrenKey)) {
                return true;
            }
        }

        return false;
    }

    private boolean canWorkOnShard(int shard) {
        boolean canWork = lockManager.canWork(shard);
        if (!canWork) {
            if (log.isTraceEnabled())
                log.trace("Skipping shard " + shard + " as lock could not be acquired");
        }
        return canWork;
    }

    /**
     * Returns the next scheduled key. It has a few side effects:
     *      1) it resets update tracking for that slot
     *      2) it adds the key to the set of running rollups.
     *
     * @return
     */
    @VisibleForTesting
    SlotKey getNextScheduled() {
        synchronized (scheduledSlots) {
            if (scheduledSlots.size() == 0)
                return null;
            synchronized (runningSlots) {
                SlotKey key = orderedScheduledSlots.remove(0);
                int slot = key.getSlot();
                Granularity gran = key.getGranularity();
                int shard = key.getShard();

                // notice how we change the state, but the timestamp remained
                // the same. this is important.  When the state is evaluated
                // (i.e., in Reader.getShardState()) we need to realize that
                // when timestamps are the same (this will happen), that a
                // remove always wins during the coalesce.
                scheduledSlots.remove(key);

                if (canWorkOnShard(shard)) {
                    UpdateStamp stamp = shardStateManager.getSlotStateManager(shard, gran).getAndSetState(slot, UpdateStamp.State.Running);
                    runningSlots.put(key, stamp.getTimestamp());
                    return key;
                } else {
                    shardOwnershipChanged.mark();
                    return null;
                }
            }
        }
    }

    /**
     * Take the given slot out of the running group, and put it back into the
     * scheduled group. If {@code rescheduleImmediately} is true, the slot will
     * be the next slot returned by a call to {@link #getNextScheduled()}. If
     * {@code rescheduleImmediately} is false, then the given slot will go to
     * the end of the line, as when it was first scheduled by
     * {@link #scheduleEligibleSlots(long, long, long)}.
     *
     * @param key
     * @param rescheduleImmediately
     */
    void pushBackToScheduled(SlotKey key, boolean rescheduleImmediately) {
        synchronized (scheduledSlots) {
            synchronized (runningSlots) {
                int slot = key.getSlot();
                Granularity gran = key.getGranularity();
                int shard = key.getShard();
                // no need to set dirty/clean here.
                shardStateManager.getSlotStateManager(shard, gran).getAndSetState(slot, UpdateStamp.State.Active);
                scheduledSlots.add(key);
                log.debug("pushBackToScheduled -> added to scheduledSlots: " + key + " size:" + scheduledSlots.size());
                if (rescheduleImmediately) {
                    orderedScheduledSlots.add(0, key);
                } else {
                    orderedScheduledSlots.add(key);
                }
            }
        }
    }

    /**
     * Remove the given slot from the running group after it has been
     * successfully re-rolled.
     *
     * @param slotKey
     */
    void clearFromRunning(SlotKey slotKey) {
        synchronized (runningSlots) {
            runningSlots.remove(slotKey);
            UpdateStamp stamp = shardStateManager.getUpdateStamp(slotKey);
            shardStateManager.setAllCoarserSlotsDirtyForSlot(slotKey);

            //When state gets set to "X", before it got persisted, it might get scheduled for rollup
            //again, if we get delayed metrics. To prevent this we temporarily set last rollup time with current
            //time. This value wont get persisted.
            long currentTimeInMillis = clock.now().getMillis();
            stamp.setLastRollupTimestamp(currentTimeInMillis);
            log.debug("SlotKey {} is marked in memory with last rollup time as {}", slotKey, currentTimeInMillis);

            // Update the stamp to Rolled state if and only if the current state
            // is running. If the current state is active, it means we received
            // a delayed put which toggled the status to Active.
            if (stamp.getState() == UpdateStamp.State.Running) {
                stamp.setState(UpdateStamp.State.Rolled);
                // Note: Rollup state will be updated to the last ACTIVE
                // timestamp which caused rollup process to kick in.
                stamp.setDirty(true);
            }
        }
    }

    /**
     * true if anything is scheduled.
     */
    boolean hasScheduled() {
        return getScheduledCount() > 0;
    }

    /**
     * returns the number of scheduled rollups.
     */
    int getScheduledCount() {
        synchronized (scheduledSlots) {
            return scheduledSlots.size();
        }
    }

    /**
     * returns the number of currently running rollups.
     */
    @VisibleForTesting
    int getRunningCount() {
        synchronized (runningSlots) {
            return runningSlots.size();
        }
    }

    public Map<Integer, UpdateStamp> getSlotStamps(Granularity gran, int shard) {
        return shardStateManager.getSlotStateManager(shard, gran).getSlotStamps();
    }

    // precondition: shard is unmanaged.
    void addShard(int shard) {
        shardStateManager.add(shard);
        lockManager.addShard(shard);
    }

    // precondition: shard is managed.
    void removeShard(int shard) {
        shardStateManager.remove(shard);
        lockManager.removeShard(shard);
    }

    Set<Integer> getRecentlyScheduledShards() {
        // Collections.unmodifiableSet(...) upsets JMX.
        return new TreeSet<Integer>(recentlyScheduledShards.asMap().keySet());
    }

    /**
     * Normal {@link com.google.common.base.Ticker Ticker} behavior is to
     * return nanoseconds elapsed since VM started. This returns milliseconds
     * since the epoch based upon {@code ScheduleContext}'s internal
     * representation of time ({@link #scheduleTime}).
     *
     * @return an anonymous Ticker object
     */
    //
    public Ticker asMillisecondsSinceEpochTicker() {
        return new Ticker() {
            @Override
            public long read() {
                return ScheduleContext.this.getCurrentTimeMillis();
            }
        };
    }

    @Override
    public Collection<String> getMetricsState(int shard, String gran, int slot) {
        final List<String> results = new ArrayList<String>();
        Granularity granularity = Granularity.fromString(gran);

        if (granularity == null)
            return results;

        final Map<Integer, UpdateStamp> stateTimestamps = this.getSlotStamps(granularity, shard);

        if (stateTimestamps == null)
            return results;

        final UpdateStamp stamp = stateTimestamps.get(slot);
        if (stamp != null) {
            results.add(new SlotState(granularity, slot, stamp.getState()).withTimestamp(stamp.getTimestamp()).toString());
        }

        return results;
    }

    private boolean isMbeanRegistered = false;
    private synchronized void registerMBean() {

        if (isMbeanRegistered) return;
        isMbeanRegistered = true;

        try {
            final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
            final String name = String.format("com.rackspacecloud.blueflood.io:type=%s", ScheduleContext.class.getSimpleName());
            final ObjectName nameObj = new ObjectName(name);
            mbs.registerMBean(this, nameObj);
        } catch (Exception exc) {
            log.error("Unable to register mbean for " + ScheduleContext.class.getSimpleName(), exc);
        }
    }
}