/* * 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.activemq.artemis.core.messagecounter; import java.text.DateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.List; import org.apache.activemq.artemis.core.server.Queue; /** * This class stores message count informations for a given queue * * At intervals this class samples the queue for message count data * * Note that the underlying queue *does not* update statistics every time a message * is added since that would reall slow things down, instead we *sample* the queues at * regular intervals - this means we are less intrusive on the queue */ public class MessageCounter { // Constants ----------------------------------------------------- // Attributes ---------------------------------------------------- // destination related information private final String destName; private final String destSubscription; private final boolean destTopic; private final boolean destDurable; private final Queue serverQueue; // counter private long countTotal; private long countTotalLast; private long depthLast; private long timeLastUpdate; private long timeLastAdd; // per hour day counter history private int dayCounterMax; private final List<DayCounter> dayCounters; private long lastMessagesAdded; // Static -------------------------------------------------------- // Constructors -------------------------------------------------- /** * Constructor * * @param name destination name * @param subscription subscription name * @param serverQueue internal queue object * @param topic topic destination flag * @param durable durable subscription flag * @param daycountmax max message history day count */ public MessageCounter(final String name, final String subscription, final Queue serverQueue, final boolean topic, final boolean durable, final int daycountmax) { // store destination related information destName = name; destSubscription = subscription; destTopic = topic; destDurable = durable; this.serverQueue = serverQueue; // initialize counter resetCounter(); // initialize message history dayCounters = new ArrayList<>(); setHistoryLimit(daycountmax); } private final Runnable onTimeExecutor = new Runnable() { @Override public void run() { long latestMessagesAdded = serverQueue.getMessagesAdded(); long newMessagesAdded = latestMessagesAdded - lastMessagesAdded; countTotal += newMessagesAdded; lastMessagesAdded = latestMessagesAdded; if (newMessagesAdded > 0) { timeLastAdd = System.currentTimeMillis(); } // update timestamp timeLastUpdate = System.currentTimeMillis(); // update message history updateHistory(newMessagesAdded); } }; // Public -------------------------------------------------------- /* * This method is called periodically to update statistics from the queue */ public synchronized void onTimer() { // Actor approach here: Instead of having the Counter locking the queue, we will use the Queue's executor // instead of possibly making a lock on the queue. // This way the scheduled Threads will be free to keep doing their pings in case the server is busy with paging or // any other deliveries serverQueue.getExecutor().execute(onTimeExecutor); } public String getDestinationName() { return destName; } public String getDestinationSubscription() { return destSubscription; } public boolean isDestinationTopic() { return destTopic; } public boolean isDestinationDurable() { return destDurable; } /** * Gets the total message count since startup or * last counter reset */ public long getCount() { return countTotal; } /** * Gets the message count delta since last method call */ public long getCountDelta() { long delta = countTotal - countTotalLast; countTotalLast = countTotal; return delta; } /** * Gets the current message count of pending messages * within the destination waiting for dispatch */ public long getMessageCount() { return serverQueue.getMessageCount(); } /** * Gets the message count delta of pending messages * since last method call. */ public long getMessageCountDelta() { long current = serverQueue.getMessageCount(); int delta = (int) (current - depthLast); depthLast = current; return delta; } public long getLastUpdate() { return timeLastUpdate; } public long getLastAddedMessageTime() { return timeLastAdd; } public void resetCounter() { countTotal = 0; countTotalLast = 0; depthLast = 0; timeLastUpdate = 0; timeLastAdd = 0; } private void setHistoryLimit(final int daycountmax) { boolean bInitialize = false; // store new maximum day count dayCounterMax = daycountmax; // update day counter array synchronized (dayCounters) { if (dayCounterMax > 0) { // limit day history to specified day count int delta = dayCounters.size() - dayCounterMax; for (int i = 0; i < delta; i++) { // reduce array size to requested size by dropping // oldest day counters dayCounters.remove(0); } // create initial day counter when empty bInitialize = dayCounters.isEmpty(); } else if (dayCounterMax == 0) { // disable history dayCounters.clear(); } else { // unlimited day history // create initial day counter when empty bInitialize = dayCounters.isEmpty(); } // optionally initialize first day counter entry if (bInitialize) { dayCounters.add(new DayCounter(new GregorianCalendar(), true)); } } } public void resetHistory() { int max = dayCounterMax; setHistoryLimit(0); setHistoryLimit(max); } public List<DayCounter> getHistory() { updateHistory(0); return new ArrayList<>(dayCounters); } /** * Get message counter history data as string in format * * "day count\n * Date 1, hour counter 0, hour counter 1, ..., hour counter 23\n * Date 2, hour counter 0, hour counter 1, ..., hour counter 23\n * ..... * ..... * Date n, hour counter 0, hour counter 1, ..., hour counter 23\n" * * @return String message history data string */ public String getHistoryAsString() { StringBuilder ret = new StringBuilder(); // ensure history counters are up to date updateHistory(0); // compile string synchronized (dayCounters) { // first line: history day count ret.append(dayCounters.size() + "\n"); // following lines: day counter data for (DayCounter counter : dayCounters) { ret.append(counter.getDayCounterAsString() + "\n"); } } return ret.toString(); } @Override public String toString() { return "MessageCounter[destName" + destName + ", destSubscription=" + destSubscription + ", destTopic=" + destTopic + ", destDurable=" + destDurable + ", serverQueue =" + serverQueue + "]"; } // Package protected --------------------------------------------- // Protected ----------------------------------------------------- // Private ------------------------------------------------------- /** * Update message counter history * * @param newMessages number of new messages to add to the latest day counter */ private void updateHistory(final long newMessages) { // check history activation if (dayCounters.isEmpty()) { return; } // calculate day difference between current date and date of last day counter entry synchronized (dayCounters) { DayCounter counterLast = dayCounters.get(dayCounters.size() - 1); GregorianCalendar calNow = new GregorianCalendar(); GregorianCalendar calLast = counterLast.getDate(); // clip day time part for day delta calulation calNow.clear(Calendar.AM_PM); calNow.clear(Calendar.HOUR); calNow.clear(Calendar.HOUR_OF_DAY); calNow.clear(Calendar.MINUTE); calNow.clear(Calendar.SECOND); calNow.clear(Calendar.MILLISECOND); calLast.clear(Calendar.AM_PM); calLast.clear(Calendar.HOUR); calLast.clear(Calendar.HOUR_OF_DAY); calLast.clear(Calendar.MINUTE); calLast.clear(Calendar.SECOND); calLast.clear(Calendar.MILLISECOND); long millisPerDay = 86400000; // 24 * 60 * 60 * 1000 long millisDelta = calNow.getTime().getTime() - calLast.getTime().getTime(); int dayDelta = (int) (millisDelta / millisPerDay); if (dayDelta > 0) { // finalize last day counter counterLast.finalizeDayCounter(); // add new intermediate empty day counter entries DayCounter counterNew; for (int i = 1; i < dayDelta; i++) { // increment date calLast.add(Calendar.DAY_OF_YEAR, 1); counterNew = new DayCounter(calLast, false); counterNew.finalizeDayCounter(); dayCounters.add(counterNew); } // add new day counter entry for current day counterNew = new DayCounter(calNow, false); dayCounters.add(counterNew); // ensure history day count limit setHistoryLimit(dayCounterMax); } // update last day counter entry counterLast = dayCounters.get(dayCounters.size() - 1); counterLast.updateDayCounter(newMessages); } } // Inner classes ------------------------------------------------- /** * Internal day counter class for one day hour based counter history */ public static final class DayCounter { static final int HOURS = 24; GregorianCalendar date = null; long[] counters = new long[DayCounter.HOURS]; /** * Constructor * * @param date day counter date * @param isStartDay true first day counter * false follow up day counter */ DayCounter(final GregorianCalendar date, final boolean isStartDay) { // store internal copy of creation date this.date = (GregorianCalendar) date.clone(); // initialize the array with '0'- values to current hour (if it is not the // first monitored day) and the rest with default values ('-1') int hour = date.get(Calendar.HOUR_OF_DAY); for (int i = 0; i < DayCounter.HOURS; i++) { if (i < hour) { if (isStartDay) { counters[i] = -1L; } else { counters[i] = 0L; } } else { counters[i] = -1L; } } // set the array element of the current hour to '0' counters[hour] = 0L; } /** * Gets copy of day counter date * * @return GregorianCalendar day counter date */ public GregorianCalendar getDate() { return (GregorianCalendar) date.clone(); } public long[] getCounters() { return counters; } /** * Update day counter hour array elements * * @param newMessages number of new messages since the counter was last updated. */ void updateDayCounter(final long newMessages) { // get the current hour of the day GregorianCalendar cal = new GregorianCalendar(); int currentIndex = cal.get(Calendar.HOUR_OF_DAY); // check if the last array update is more than 1 hour ago, if so fill all // array elements between the last index and the current index with '0' values boolean bUpdate = false; for (int i = 0; i <= currentIndex; i++) { if (counters[i] > -1) { // found first initialized hour counter // -> set all following uninitialized // counter values to 0 bUpdate = true; } if (bUpdate == true) { if (counters[i] == -1) { counters[i] = 0; } } } // increment current counter with the new messages counters[currentIndex] += newMessages; } /** * Finalize day counter hour array elements */ private void finalizeDayCounter() { // a new day has began, so fill all array elements from index to end with // '0' values boolean bFinalize = false; for (int i = 0; i < DayCounter.HOURS; i++) { if (counters[i] > -1) { // found first initialized hour counter // -> finalize all following uninitialized // counter values bFinalize = true; } if (bFinalize) { if (counters[i] == -1) { counters[i] = 0; } } } } /** * Return day counter data as string with format<br> * "Date, hour counter 0, hour counter 1, ..., hour counter 23". * * @return String day counter data */ private String getDayCounterAsString() { // first element day counter date DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.SHORT); StringBuilder strData = new StringBuilder(dateFormat.format(date.getTime())); // append 24 comma separated hour counter values for (int i = 0; i < DayCounter.HOURS; i++) { strData.append("," + counters[i]); } return strData.toString(); } } }