com.arpnetworking.tsdcore.sinks.LimitingSink.java Source code

Java tutorial

Introduction

Here is the source code for com.arpnetworking.tsdcore.sinks.LimitingSink.java

Source

/**
 * Copyright 2014 Groupon.com
 *
 * 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.arpnetworking.tsdcore.sinks;

import com.arpnetworking.metrics.Metrics;
import com.arpnetworking.metrics.MetricsFactory;
import com.arpnetworking.tsdcore.limiter.MetricsLimiter;
import com.arpnetworking.tsdcore.limiter.NoLimitMetricsLimiter;
import com.arpnetworking.tsdcore.model.AggregatedData;
import com.arpnetworking.tsdcore.model.Condition;
import com.arpnetworking.tsdcore.model.FQDSN;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.google.common.base.Function;
import com.google.common.base.MoreObjects;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.name.Names;
import net.sf.oval.constraint.Min;
import net.sf.oval.constraint.NotEmpty;
import net.sf.oval.constraint.NotNull;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Applies a <code>MetricsLimiter</code> to limit the number of
 * <code>AggregatedData</code> instances recorded by the underlying
 * <code>Sink</code> instance.
 *
 * @author Ville Koskela (vkoskela at groupon dot com)
 */
public final class LimitingSink extends BaseSink {

    /**
     * {@inheritDoc}
     */
    @Override
    public void recordAggregateData(final Collection<AggregatedData> data, final Collection<Condition> conditions) {
        final DateTime now = DateTime.now();
        final List<AggregatedData> filteredData = Lists.newArrayListWithExpectedSize(data.size());
        final Map<FQDSN, Condition> conditionsByFQDSN = Maps.uniqueIndex(conditions,
                new Function<Condition, FQDSN>() {
                    @Override
                    public FQDSN apply(final Condition condition) {
                        return condition.getFQDSN();
                    }
                });
        long limited = 0;
        for (final AggregatedData datum : data) {
            if (_metricsLimiter.offer(datum, now)) {
                filteredData.add(datum);
            } else {
                LOGGER.warn(String.format("%s: Skipping publication of limited data; aggregatedData=%s", getName(),
                        datum));
                ++limited;

                // Remove any condition for the FQDSN
                // NOTE: Although limiting also contains period, the data produced
                // in any one invocation of the sink is for a single period we can
                // safely ignore that and find any matching conditions by FQDSN.
                conditionsByFQDSN.remove(datum.getFQDSN());
            }
        }
        _limited.getAndAdd(limited);
        _sink.recordAggregateData(filteredData, conditionsByFQDSN.values());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void close() {
        _sink.close();
        try {
            _executor.shutdown();
            _executor.awaitTermination(EXECUTOR_TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
        } catch (final InterruptedException e) {
            Thread.interrupted();
            Throwables.propagate(e);
        }
        flushMetrics();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this).add("super", super.toString())
                .add("MetricsLimiter", _metricsLimiter).add("Sink", _sink).add("Limited", _limited.get())
                .toString();
    }

    private void flushMetrics() {
        final Metrics newMetrics = createMetrics();
        final Metrics oldMetrics = _metrics.getAndSet(newMetrics);

        // Record statistics and close
        oldMetrics.incrementCounter(_limitedName, _limited.getAndSet(0));
        oldMetrics.close();
    }

    private Metrics createMetrics() {
        final Metrics metrics = _metricsFactory.create();
        metrics.resetCounter(_limitedName);
        return metrics;
    }

    private LimitingSink(final Builder builder) {
        super(builder);
        _sink = builder._sink;

        final MetricsLimiter metricsLimiter;
        if (builder._metricsLimiter != null) {
            LOGGER.debug(String.format("Using injected metrics limiter; limiter=%s", builder._metricsLimiter));
            metricsLimiter = builder._metricsLimiter;
        } else if (builder._injector != null && builder._metricsLimiterName != null) {
            LOGGER.debug(String.format("Using named metrics limiter; limiter=%s", builder._metricsLimiterName));
            metricsLimiter = builder._injector
                    .getInstance(Key.get(MetricsLimiter.class, Names.named(builder._metricsLimiterName)));
        } else {
            LOGGER.debug("Using no limit metrics limiter");
            metricsLimiter = new NoLimitMetricsLimiter();
        }
        _metricsLimiter = metricsLimiter;

        _metricsFactory = builder._metricsFactory;
        _limitedName = "Sinks/LimitingSink/" + getMetricSafeName() + "/Limited";
        _metrics.set(createMetrics());

        // Write the metrics periodically
        _executor.scheduleAtFixedRate(new MetricsLogger(), builder._intervalInSeconds.longValue(),
                builder._intervalInSeconds.longValue(), TimeUnit.SECONDS);
    }

    private final Sink _sink;
    private final MetricsLimiter _metricsLimiter;
    private final MetricsFactory _metricsFactory;
    private final AtomicReference<Metrics> _metrics = new AtomicReference<>();
    private final AtomicLong _limited = new AtomicLong(0);
    private final String _limitedName;
    private final ScheduledExecutorService _executor = Executors.newSingleThreadScheduledExecutor();

    private static final Logger LOGGER = LoggerFactory.getLogger(LimitingSink.class);
    private static final int EXECUTOR_TIMEOUT_IN_SECONDS = 30;

    private final class MetricsLogger implements Runnable {

        /**
         * {@inheritDoc}
         */
        @Override
        public void run() {
            flushMetrics();
        }
    }

    /**
     * Implementation of builder pattern for <code>LimitingSink</code>.
     *
     * @author Ville Koskela (vkoskela at groupon dot com)
     */
    public static final class Builder extends BaseSink.Builder<Builder, LimitingSink> {

        /**
         * Public constructor.
         */
        public Builder() {
            super(LimitingSink.class);
        }

        /**
         * The aggregated data sink to limit. Cannot be null.
         *
         * @param value The aggregated data sink to limit.
         * @return This instance of <code>Builder</code>.
         */
        public Builder setSink(final Sink value) {
            _sink = value;
            return this;
        }

        /**
         * The interval in seconds between statistic flushes. Cannot be null;
         * minimum 1. Default is 1.
         *
         * @param value The interval in seconds between flushes.
         * @return This instance of <code>Builder</code>.
         */
        public Builder setIntervalInSeconds(final Long value) {
            _intervalInSeconds = value;
            return this;
        }

        /**
         * Instance of <code>MetricsFactory</code>. Cannot be null. This field
         * may be injected automatically by Jackson/Guice if setup to do so.
         *
         * @param value Instance of <code>MetricsFactory</code>.
         * @return This instance of <code>Builder</code>.
         */
        public Builder setMetricsFactory(final MetricsFactory value) {
            _metricsFactory = value;
            return this;
        }

        /**
         * The <code>MetricsLimiter</code>. Optional. The default is to use
         * a <code>NoLimitMetricsLimiter</code> instance. This takes place
         * over looking up a <code>MetricsLimiter</code> instance by injection.
         *
         * @param value The state directory.
         * @return This instance of <code>Builder</code>.
         */
        public Builder setMetricsLimiter(final MetricsLimiter value) {
            _metricsLimiter = value;
            return this;
        }

        /**
         * The metrics limiter name. Optional. The default is to use
         * a <code>NoLimitMetricsLimiter</code> instance. This will be overridden
         * by a specified <code>MetricsLimiter</code> instance.
         *
         * @param value The state directory.
         * @return This instance of <code>Builder</code>.
         */
        public Builder setMetricsLimiterName(final String value) {
            _metricsLimiterName = value;
            return this;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        protected Builder self() {
            return this;
        }

        @NotNull
        private Sink _sink;
        private MetricsLimiter _metricsLimiter;
        @NotEmpty
        private String _metricsLimiterName;
        @JacksonInject
        private Injector _injector;
        @NotNull
        @Min(value = 1)
        private Long _intervalInSeconds = Long.valueOf(1);
        @JacksonInject
        @NotNull
        private MetricsFactory _metricsFactory;
    }
}