# ConnectionsView.java

    /*
    *  Licensed to the Apache Software Foundation (ASF) under one
    *  or more contributor license agreements.  See the NOTICE file
    *  distributed with this work for additional information
    *  regarding copyright ownership.  The ASF licenses this file
    *  to you 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 org.apache.synapse.transport.passthru.jmx;

    import org.apache.axis2.AxisFault;

    import java.util.*;
    import java.util.concurrent.Executors;
    import java.util.concurrent.ScheduledExecutorService;
    import java.util.concurrent.TimeUnit;
    import java.util.concurrent.atomic.AtomicInteger;

    /**
     * ConnectionsView MBean can be used to collect and monitor statistics on HTTP connections
     * created by the NHTTP transport. Connection statistics can be divided into two categories,
     * namely short term data and long term data. Short term data is related to the last 15
     * minutes of execution and they are updated every minute. Long term data is related to
     * the last 24 hours of execution and they get updated every 5 minutes.  In addition to the
     * connection statistics this MBean also provides information on the request and response
     * sizes received over the HTTP connections. All messages are divided into six categories
     * based on their sizes and the resulting counts are made available as a table.
     */
    public class ConnectionsView implements ConnectionsViewMBean {

        private static final String PASS_THROUGH_CONNECTIONS = "PassThroughConnections";

        // Bucket definitions
        private static final int LESS_THAN_1K       = 0;
        private static final int LESS_THAN_10K      = 1;
        private static final int LESS_THAN_100K     = 2;
        private static final int LESS_THAN_1M       = 3;
        private static final int LESS_THAN_10M      = 4;
        private static final int GREATER_THAN_10M   = 5;

        private static final int SHORT_DATA_COLLECTION_PERIOD = 60;
        private static final int LONG_DATA_COLLECTION_PERIOD = 60 * 5;

        private static final int SAMPLES_PER_HOUR = (60 * 60)/LONG_DATA_COLLECTION_PERIOD;
        private Queue<Integer> lastSecondRequestQueue = new LinkedList<Integer>();
        private Queue<Integer> openedConnectionsDataQueue = new LinkedList<Integer>();
        private Queue<Integer> shortTermDataQueue = new LinkedList<Integer>();
        private Queue<Integer> longTermDataQueue = new LinkedList<Integer>();

        private AtomicInteger requestsReceived = new AtomicInteger(0);
        private AtomicInteger openedConnections = new AtomicInteger(0);
        private AtomicInteger activeConnections = new AtomicInteger(0);
        private AtomicInteger shortTermOpenedConnections = new AtomicInteger(0);
        private AtomicInteger longTermOpenedConnections = new AtomicInteger(0);

        private AtomicInteger unservedRequests = new AtomicInteger(0);

        // The array length must be equal to the number of buckets
        private AtomicInteger[] requestSizeCounters = new AtomicInteger[6];
        private AtomicInteger[] responseSizeCounters = new AtomicInteger[6];

        private Date resetTime = Calendar.getInstance().getTime();

        private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

        private String name;

        public ConnectionsView(String name) throws AxisFault {
            this.name = name;

            initCounters(requestSizeCounters);
            initCounters(responseSizeCounters);

            Runnable requestTask = new Runnable() {
                public void run() {
                    // We only need historical data for the last 1 minutes
                    // Therefore no need to keep data older than that...
                    if (lastSecondRequestQueue.size() == 60) {
                        lastSecondRequestQueue.remove();
                    }
                    lastSecondRequestQueue.offer(requestsReceived.getAndSet(0));
                }
            };
            // Delay the timer by 1 minute to prevent the task from starting immediately
            scheduler.scheduleAtFixedRate(requestTask, 1,
                    1, TimeUnit.SECONDS);

            Runnable secondsTask = new Runnable() {
                public void run() {
                    // We only need historical data for the last 1 minutes
                    // Therefore no need to keep data older than that...
                    if (openedConnectionsDataQueue.size() == 60) {
                        openedConnectionsDataQueue.remove();
                    }
                    openedConnectionsDataQueue.offer(openedConnections.getAndSet(0));
                }
            };
            // Delay the timer by 1 minute to prevent the task from starting immediately
            scheduler.scheduleAtFixedRate(secondsTask, 1,
                    1, TimeUnit.SECONDS);

            Runnable task = new Runnable() {
                public void run() {
                    // We only need historical data for the last 15 minutes
                    // Therefore no need to keep data older than that...
                    if (shortTermDataQueue.size() == 15) {
                        shortTermDataQueue.remove();
                    }
                    shortTermDataQueue.offer(shortTermOpenedConnections.getAndSet(0));
                }
            };
            // Delay the timer by 1 minute to prevent the task from starting immediately
            scheduler.scheduleAtFixedRate(task, SHORT_DATA_COLLECTION_PERIOD,
                    SHORT_DATA_COLLECTION_PERIOD, TimeUnit.SECONDS);

            Runnable longTermCollector = new Runnable() {
                public void run() {
                    // We only need historical data for the last 24 hours
                    // Therefore no need to keep data older than that...
                    if (longTermDataQueue.size() == 24 * SAMPLES_PER_HOUR) {
                        longTermDataQueue.remove();
                    }
                    longTermDataQueue.offer(longTermOpenedConnections.getAndSet(0));
                }
            };
            scheduler.scheduleAtFixedRate(longTermCollector, LONG_DATA_COLLECTION_PERIOD,
                    LONG_DATA_COLLECTION_PERIOD, TimeUnit.SECONDS);

            MBeanRegistrar.getInstance().registerMBean(this, PASS_THROUGH_CONNECTIONS, name);

        }

        public void destroy() {
            MBeanRegistrar.getInstance().unRegisterMBean(PASS_THROUGH_CONNECTIONS, name);
            scheduler.shutdownNow();
        }

        private void initCounters(AtomicInteger[] counters) {
            for (int i = 0; i < counters.length; i++) {
                if (counters[i] == null) {
                    counters[i] = new AtomicInteger(0);
                } else {
                    counters[i].set(0);
                }
            }
        }

        protected void connected() {
            activeConnections.incrementAndGet();
            openedConnections.incrementAndGet();
            shortTermOpenedConnections.incrementAndGet();
            longTermOpenedConnections.incrementAndGet();
        }

        protected void disconnected() {
            activeConnections.decrementAndGet();
        }

        protected void requestReceived() {

            requestsReceived.incrementAndGet();
            unservedRequests.incrementAndGet();
        }

        protected void requestServed() {
            unservedRequests.decrementAndGet();
        }

        protected void timeoutOccured() {
            unservedRequests.decrementAndGet();
        }

        protected void exceptionOccured() {
            unservedRequests.decrementAndGet();
        }

        public int getUnServedRequests() {
            return unservedRequests.get();
        }

        protected void notifyMessageSize(long size, boolean isRequest) {
            // This logic gets executed for each and every transaction. For a typical
            // mediation scenario this method will be called 4 times. Therefore I'm using
            // arrays of integers to keep the overhead down to a minimum. Since the number
            // of buckets is 6, this can be easily managed without using a slow data structure
            // like a HashMap. This approach guarantees O(1) complexity.

            AtomicInteger[] counters = isRequest ? requestSizeCounters : responseSizeCounters;

            if (size < 1024) {
                counters[LESS_THAN_1K].incrementAndGet();
            } else if (size < 10240) {
                counters[LESS_THAN_10K].incrementAndGet();
            } else if (size < 102400) {
                counters[LESS_THAN_100K].incrementAndGet();
            } else if (size < 1048576) {
                counters[LESS_THAN_1M].incrementAndGet();
            } else if (size < 10485760) {
                counters[LESS_THAN_10M].incrementAndGet();
            } else {
                counters[GREATER_THAN_10M].incrementAndGet();
            }
        }

        public int getActiveConnections() {
            return activeConnections.get();
        }

        public int getLastSecondRequests() {

            return getTotalRequests(1);
        }

        public int getLast15SecondRequests() {

            return getTotalRequests(15);
        }

        public int getLastMinuteRequests() {

            return getTotalRequests(60);
        }

        public int getLastSecondConnections() {

            return getSecondConnections(1);
        }

        public int getLast5SecondConnections() {

            return getSecondConnections(5);
        }

        public int getLast15SecondConnections() {

            return getSecondConnections(15);
        }

        public int getLast30SecondConnections() {

            return getSecondConnections(30);
        }

        public int getLastMinuteConnections() {
            return getTotalConnections(1);
        }

        public int getLast5MinuteConnections() {
            return getTotalConnections(5);
        }

        public int getLast15MinuteConnections() {
            return getTotalConnections(15);
        }

        public int getLastHourConnections() {
            return getTotalConnectionsByHour(1);
        }

        public int getLast8HourConnections() {
            return getTotalConnectionsByHour(8);
        }

        public int getLast24HourConnections() {
            return getTotalConnectionsByHour(24);
        }

        public Map getRequestSizesMap() {
            return getCountersMap(requestSizeCounters);
        }

        public Map getResponseSizesMap() {
            return getCountersMap(responseSizeCounters);
        }

        public Date getLastResetTime() {
            return resetTime;
        }

        private Map<String,Integer> getCountersMap(AtomicInteger[] counters) {
            // This ensures that keys are returned in the same order we insert them
            // Provides better readability in the JMX consoles
            Map<String,Integer> map = new LinkedHashMap<String,Integer>();
            map.put("< 1 K", counters[LESS_THAN_1K].get());
            map.put("< 10 K", counters[LESS_THAN_10K].get());
            map.put("< 100 K", counters[LESS_THAN_100K].get());
            map.put("< 1 M", counters[LESS_THAN_1M].get());
            map.put("< 10 M", counters[LESS_THAN_10M].get());
            map.put("> 10 M", counters[GREATER_THAN_10M].get());
            return map;
        }

        public void reset() {
            initCounters(requestSizeCounters);
            initCounters(responseSizeCounters);
            shortTermDataQueue.clear();
            longTermDataQueue.clear();
            resetTime = Calendar.getInstance().getTime();
        }

        /**
         * Return the number of total connections opened during last 'n' munites
         * of execution
         *
         * @param n Number of minutes in the execution history
         * @return The number of connections opened
         */
        private int getTotalConnections(int n) {

            Integer[] array = shortTermDataQueue.toArray(new Integer[shortTermDataQueue.size()]);
            int sum = getTotal(array, n);
            return sum;
        }

        private int getTotalRequests(int n) {

            Integer[] array = lastSecondRequestQueue.toArray(new Integer[lastSecondRequestQueue.size()]);
            int sum = getTotal(array, n);
            return sum;
        }

        private int getSecondConnections(int n) {

            Integer[] array = openedConnectionsDataQueue.toArray(new Integer[openedConnectionsDataQueue.size()]);
            int sum = getTotal(array, n);
            return sum;
        }

        private int getTotal(Integer[] array, int n) {

            int sum = 0;
            if (n > array.length) {
                for (int i = 0; i < array.length; i++) {
                    sum += array[i];
                }
            } else {
                for (int i = 0; i < n; i++) {
                    sum += array[array.length - 1 - i];
                }
            }
            return sum;
        }

        /**
         * Return the number of total connections opened during last 'n' hours
         * of execution
         *
         * @param n Number of hours in the execution history
         * @return The number of connections opened
         */
        private int getTotalConnectionsByHour(int n) {
            int samples = n * SAMPLES_PER_HOUR;
            int sum = 0;
            Integer[] array = longTermDataQueue.toArray(new Integer[longTermDataQueue.size()]);

            if (samples > array.length) {
                for (int i = 0; i < array.length; i++) {
                    sum += array[i];
                }
            } else {
                for (int i = 0; i < samples; i++) {
                    sum += array[array.length - 1 - i];
                }
            }
            return sum;
        }
    }