package rocks.inspectit.server.dao.impl; import java.sql.Timestamp; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; import javax.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Repository; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; import rocks.inspectit.shared.all.communication.data.DatabaseAggregatedTimerData; import rocks.inspectit.shared.all.communication.data.TimerData; /** * Aggregator for the {@link TimerData} objects that need to be persisted to the DB. * * @author Ivan Senic * @see https://inspectit-performance.atlassian.net/wiki/display/DEV/TimerData+Aggregator * */ @Repository public class TimerDataAggregator extends AbstractJpaDao<TimerData> { /** * Period of time in which all timer data should be aggregated. In milliseconds. */ @Value("${cmr.aggregationPeriod}") long aggregationPeriod; /** * Max elements in the cache. */ @Value("${cmr.maxElements}") int maxElements; /** * Sleeping period for thread that is cleaning the cache (persisting the objects). In * milliseconds. */ @Value("${cmr.cacheCleanSleepingPeriod}") long cacheCleanSleepingPeriod; /** * Current element count in cache. */ private AtomicInteger elementCount; /** * Map for caching. */ private Map<Integer, TimerData> map; /** * Queue for knowing the order. */ private ConcurrentLinkedQueue<TimerData> queue; /** * List of objects that are out of the cache and need to be persisted. */ private ConcurrentLinkedQueue<TimerData> persistList; /** * Lock for persist all. */ private ReentrantLock persistAllLock; /** * Cache cleaner. */ private TimerDataAggregatorCacheCleaner timerDataAggregatorCacheCleaner; /** * Transaction template to use to do save work due to cache cleaner. */ private TransactionTemplate tt; /** * Default constructor. * * @param transactionManager * {@link PlatformTransactionManager}. Autowired by Spring. */ @Autowired public TimerDataAggregator(PlatformTransactionManager transactionManager) { super(TimerData.class); elementCount = new AtomicInteger(0); map = new HashMap<>(); queue = new ConcurrentLinkedQueue<>(); persistList = new ConcurrentLinkedQueue<>(); persistAllLock = new ReentrantLock(); this.tt = new TransactionTemplate(transactionManager); } /** * Aggregates the {@link TimerData} object and updates the cache. Note that the given object * will not be modified by this method. * * @param timerData * {@link TimerData} that holds values to be aggregated. */ public void processTimerData(TimerData timerData) { long aggregationTimestamp = getAlteredTimestamp(timerData); int cacheHash = getCacheHash(timerData.getPlatformIdent(), timerData.getMethodIdent(), aggregationTimestamp); persistAllLock.lock(); try { TimerData aggTimerData = map.get(cacheHash); if (aggTimerData == null) { // we create a DB aggregated timer data because we don't want to alter objects that // are in the memory aggTimerData = new DatabaseAggregatedTimerData(new Timestamp(aggregationTimestamp), timerData.getPlatformIdent(), timerData.getSensorTypeIdent(), timerData.getMethodIdent()); map.put(cacheHash, aggTimerData); queue.add(aggTimerData); // set most recently in cleaner if (null != timerDataAggregatorCacheCleaner) { timerDataAggregatorCacheCleaner.setMostRecentlyAdded(aggTimerData); } int count = elementCount.incrementAndGet(); // remove oldest as long as number of elements is higher than maximum while (maxElements < count) { TimerData oldest = queue.poll(); if (null != oldest) { map.remove(getCacheHash(oldest.getPlatformIdent(), oldest.getMethodIdent(), oldest.getTimeStamp().getTime())); persistList.add(oldest); count = elementCount.decrementAndGet(); } } } aggTimerData.aggregateTimerData(timerData); } finally { persistAllLock.unlock(); } } /** * Clears the cache and persists all the data inside. */ public void removeAndPersistAll() { if (!queue.isEmpty()) { persistAllLock.lock(); try { tt.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { TimerData oldest = queue.poll(); while (oldest != null) { map.remove(getCacheHash(oldest.getPlatformIdent(), oldest.getMethodIdent(), oldest.getTimeStamp().getTime())); TimerDataAggregator.super.create(oldest); elementCount.decrementAndGet(); oldest = queue.poll(); } } }); } finally { persistAllLock.unlock(); } } } /** * Persists all objects in the persistence list. */ void saveAllInPersistList() { if (!persistList.isEmpty()) { tt.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus status) { TimerData last = persistList.poll(); while (last != null) { TimerData data = (TimerData) last.finalizeData(); TimerDataAggregator.super.create(data); last = persistList.poll(); } } }); } } /** * Returns the cache hash code. * * @param platformIdent * Platform ident. * @param methodIdent * Method ident. * @param timestampValue * Time stamp value as long. * @return Cache hash for the given set of values. */ private int getCacheHash(long platformIdent, long methodIdent, long timestampValue) { final int prime = 31; int result = 0; result = (prime * result) + (int) (platformIdent ^ (platformIdent >>> 32)); result = (prime * result) + (int) (methodIdent ^ (methodIdent >>> 32)); result = (prime * result) + (int) (timestampValue ^ (timestampValue >>> 32)); return result; } /** * Returns the value of the time stamp based on a aggregation period. * * @param timerData * {@link TimerData} to get aggregation time stamp. * @return Aggregation time stamp. */ private long getAlteredTimestamp(TimerData timerData) { long timestampValue = timerData.getTimeStamp().getTime(); long newTimestampValue = timestampValue - (timestampValue % aggregationPeriod); return newTimestampValue; } /** * Starting the thread in post construct, not in constructor. */ @PostConstruct public void postConstruct() { timerDataAggregatorCacheCleaner = new TimerDataAggregatorCacheCleaner(this); timerDataAggregatorCacheCleaner.start(); } /** * @return the aggregationPeriod */ public long getAggregationPeriod() { return aggregationPeriod; } /** * @return the maxElements */ public int getMaxElements() { return maxElements; } /** * @return the cacheCleanSleepingPeriod */ public long getCacheCleanSleepingPeriod() { return cacheCleanSleepingPeriod; } /** * Gets {@link #elementCount}. * * @return {@link #elementCount} */ public int getElementCount() { return elementCount.get(); } }