package rocks.inspectit.shared.cs.storage; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import javax.annotation.Resource; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import rocks.inspectit.shared.all.cmr.cache.IObjectSizes; import rocks.inspectit.shared.all.communication.DefaultData; import rocks.inspectit.shared.all.spring.logger.Log; import rocks.inspectit.shared.cs.indexing.impl.IndexingException; import rocks.inspectit.shared.cs.indexing.storage.IStorageDescriptor; import rocks.inspectit.shared.cs.indexing.storage.IStorageTreeComponent; import rocks.inspectit.shared.cs.storage.StorageWriter.WriteTask; import rocks.inspectit.shared.cs.storage.util.StorageIndexTreeProvider; /** * This class provides a layer of abstraction between {@link StorageWriter} and * {@link IStorageTreeComponent}. * <p> * The class is responsible for correct handling of the indexing trees and their saving. The storage * writer will use this component to get the channel where the write should be executed, and also * signal if the write was successful or not, as well as to signal when is the write completely * finished. * <p> * The class will cache the data that is currently in write with the information to which indexing * tree it is going and which descriptor was assigned to the data in write. Because of this for each * write there is a put and remove from a {@link HashMap} as an overhead, but since the size of the * map is constant (data currently in write can not be greater than the number of threads writing * the data, there should not be any serious performance problems. * * @author Ivan Senic * */ @Component @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) @Lazy public class StorageIndexingTreeHandler { /** * The log of this class. */ @Log Logger log; /** * Time to wait in milliseconds for all operations on indexing tree to be finished, so that tree * can be saved. */ private static final long WAITING_FOR_TREE_TO_BE_READY = 1000; /** * Number of sleeps that will be executed at the {@link #finish()} method. This actually means * that the {@link #finish()} method will maximally wait for * {@value #WAITING_FOR_TREE_TO_BE_READY} * {@value #FINISH_WAITING_ITERATIONS} milliseconds. */ private static final int FINISH_WAITING_ITERATIONS = 30; /** * Delay of rescheduling check tree size task. */ private static final long TREE_CHECK_DELAY = 30; /** * {@link TimeUnit} of the delay of rescheduling check tree size task. */ private static final TimeUnit TREE_CHECK_DELAY_TIME_UNIT = TimeUnit.SECONDS; /** * {@link StorageWriter}. */ private StorageWriter storageWriter; /** * {@link StorageIndexTreeProvider}. */ @Autowired StorageIndexTreeProvider<DefaultData> storageIndexTreeProvider; /** * {@link ExecutorService} for tasks of the tree handling. */ @Autowired @Resource(name = "scheduledExecutorService") ScheduledExecutorService executorService; /** * Indexing tree of the Storage. */ private AtomicReference<IStorageTreeComponent<DefaultData>> storageIndexingTreeReference; /** * Write tasks currently in process. */ private Map<WriteTask, TreeDescriptorPair> writeTasksInProcess = new ConcurrentHashMap<>(16, 0.75f, 2); /** * Object size for indexing tree size calculation. */ @Autowired IObjectSizes objectSizes; /** * Max size of indexing tree after which a new tree is needed, and the old one is saved. */ @Value(value = "${storage.maximumIndexingTreeSize}") long maximumIndexingTreeSize; /** * Future for the task of saving the indexing tree. */ private volatile ScheduledFuture<?> indexingTreeSavingFuture; /** * Prepares for write by creating the new indexing tree. This method must be called before * asking for the position of the data to be written to. */ public void prepare() { storageIndexingTreeReference = new AtomicReference<>(getNewStorageIndexingTree()); indexingTreeSavingFuture = executorService.scheduleWithFixedDelay(new IndexingTreeSavingTask(), TREE_CHECK_DELAY, TREE_CHECK_DELAY, TREE_CHECK_DELAY_TIME_UNIT); } /** * Returns the channel ID where the data should be written. * <p> * Internally this method saves the {@link TreeDescriptorPair} for the given task, so when the * write is done the descriptor can be updated with correct write information. * * @param writeTask * Write task that starts the write. * @return Returns the channel ID where the data should be written. * @throws IndexingException * If indexing fails. */ public int startWrite(WriteTask writeTask) throws IndexingException { DefaultData data = writeTask.getData(); if (null == data) { throw new IndexingException("Indexing failed. Data to index was null."); } TreeDescriptorPair treeDescriptorPair = new TreeDescriptorPair(); // save tree-descriptor pair into the map that holds data that is written writeTasksInProcess.put(writeTask, treeDescriptorPair); // get the descriptor from tree IStorageTreeComponent<DefaultData> indexingTree = storageIndexingTreeReference.get(); IStorageDescriptor storageDescriptor = indexingTree.put(data); if (null == storageDescriptor) { throw new IndexingException("Indexing failed. Storage descriptor was null."); } // update the tree-descriptor pair treeDescriptorPair.setIndexingTree(indexingTree); treeDescriptorPair.setStorageDescriptor(storageDescriptor); return storageDescriptor.getChannelId(); } /** * Signals to the {@link StorageIndexingTreeHandler} that the write has been successful with * correct information about write position and size. * <p> * Internally this method will update the {@link IStorageDescriptor} for the given * {@link DefaultData} object in the write task, and remove the task from the set of tasks being * currently processed. * * @param writeTask * Write task that succeeded. * @param position * Write position. * @param size * Write size. */ public void writeSuccessful(WriteTask writeTask, long position, long size) { // get the data from the map TreeDescriptorPair treeDescriptorPair = writeTasksInProcess.get(writeTask); if (null != treeDescriptorPair) { IStorageDescriptor storageDescriptor = treeDescriptorPair.getStorageDescriptor(); // update the descriptor with the information provided if (null != storageDescriptor) { storageDescriptor.setPositionAndSize(position, size); } } // remove the entry in map after the data has been updated in indexing tree writeTasksInProcess.remove(writeTask); } /** * Signals to the {@link StorageIndexingTreeHandler} that the write has failed. * <p> * Internally this method will update the {@link IStorageTreeComponent} by removing the given * {@link DefaultData} object in the write task, and remove the task from the set of tasks being * currently processed. * * @param writeTask * Write task that failed. */ public void writeFailed(WriteTask writeTask) { // get the data from the map TreeDescriptorPair treeDescriptorPair = writeTasksInProcess.get(writeTask); if (null != treeDescriptorPair) { IStorageTreeComponent<DefaultData> indexingTree = treeDescriptorPair.getIndexingTree(); // if write fails, remove the descriptor for the data from indexing tree if (null != indexingTree) { indexingTree.getAndRemove(writeTask.getData()); } } // remove the entry in map after the indexing tree was informed writeTasksInProcess.remove(writeTask); } /** * Cancels the {@link #indexingTreeSavingFuture}. */ public void cancelIndexingTreeSavingFuture() { if (!indexingTreeSavingFuture.isDone() && !indexingTreeSavingFuture.isCancelled()) { indexingTreeSavingFuture.cancel(false); } } /** * Signals to the {@link StorageIndexingTreeHandler} that the write is finished and current tree * should be saved. */ public void finish() { cancelIndexingTreeSavingFuture(); IStorageTreeComponent<DefaultData> currentIndexingTree = null; while (true) { // try to set the indexing tree to null currentIndexingTree = storageIndexingTreeReference.get(); if (storageIndexingTreeReference.compareAndSet(currentIndexingTree, null)) { break; } } if (null != currentIndexingTree) { // wait until no more data is there int sleepCount = 0; while (!writeTasksInProcess.isEmpty()) { log.info("Indexing tree handler still waiting for " + writeTasksInProcess.size() + " task(s) to be finished. Going for sleep " + (sleepCount + 1) + " out of " + FINISH_WAITING_ITERATIONS + "."); if (sleepCount > FINISH_WAITING_ITERATIONS) { log.warn("Indexing tree handler waited " + (sleepCount * WAITING_FOR_TREE_TO_BE_READY) + " milliseconds for all tasks to be finished. There are " + writeTasksInProcess.size() + " tasks still in-progress. Saving of the indexing tree will continue without waiting for these tasks."); break; } try { Thread.sleep(WAITING_FOR_TREE_TO_BE_READY); sleepCount++; } catch (InterruptedException e) { Thread.interrupted(); } } currentIndexingTree.preWriteFinalization(); boolean written = storageWriter.writeNonDefaultDataObject(currentIndexingTree, getRandomFileName() + StorageFileType.INDEX_FILE.getExtension()); if (!written) { log.error("Indexing tree saving failed. Indexing tree might be lost."); } } } /** * Returns amount of write tasks in progress. * * @return Returns amount of write tasks in progress. */ int getWriteTaskInProgressCount() { return writeTasksInProcess.size(); } /** * Returns random file name. * * @return Returns random file name. * */ private String getRandomFileName() { return UUID.randomUUID().toString(); } /** * This task periodically checks if the size of the indexing tree is bigger that the * {@link #maximumIndexingTreeSize}, and if it is provides new indexing tree for the other * tasks, and safely saves the old tree. * * @author Ivan Senic * */ class IndexingTreeSavingTask implements Runnable { /** * {@inheritDoc} */ @Override public void run() { // the complete run block has to be guarded against exceptions, because the executor // service will throw away any rescheduling of the task if exception is thrown try { while (true) { final IStorageTreeComponent<DefaultData> currentIndexingTree = storageIndexingTreeReference.get(); if (null != currentIndexingTree) { long treeSize = currentIndexingTree.getComponentSize(objectSizes); // check if the tree has grown enough for saving if (treeSize > maximumIndexingTreeSize) { IStorageTreeComponent<DefaultData> newIndexingTree = getNewStorageIndexingTree(); // put new fresh tree to the Atomic reference if (storageIndexingTreeReference.compareAndSet(currentIndexingTree, newIndexingTree)) { // collect the information about tasks currently in write final Collection<WriteTask> writeTasksToWait = new HashSet<>(writeTasksInProcess.keySet()); // here we are safe to know that when all of the tasks in the // collection is gone from the tasks in process map, we can save the // tree Runnable writeOldIndexingTree = new Runnable() { @Override public void run() { boolean safeToSave = Collections.disjoint(writeTasksToWait, writeTasksInProcess.keySet()); if (safeToSave) { currentIndexingTree.preWriteFinalization(); boolean written = storageWriter.writeNonDefaultDataObject(currentIndexingTree, getRandomFileName() + StorageFileType.INDEX_FILE.getExtension()); if (!written) { log.error("Indexing tree saving failed. Indexing tree might be lost."); } } else { executorService.schedule(this, WAITING_FOR_TREE_TO_BE_READY, TimeUnit.MILLISECONDS); } } }; executorService.submit(writeOldIndexingTree); break; } } else { break; } } else { break; } } } catch (Exception e) { log.error("Indexing tree saving task encountered an error.", e); } } } /** * * @return Returns new empty storage indexing tree. */ private IStorageTreeComponent<DefaultData> getNewStorageIndexingTree() { return storageIndexTreeProvider.getStorageIndexingTree(); } /** * Registers the {@link StorageWriter} to work with. * * @param storageWriter * {@link StorageWriter}. */ public void registerStorageWriter(StorageWriter storageWriter) { this.storageWriter = storageWriter; } /** * Utility class that holds a pair of {@link IStorageDescriptor} and * {@link IStorageTreeComponent}. * * @author Ivan Senic * */ private static final class TreeDescriptorPair { /** * {@link IStorageDescriptor}. */ private IStorageDescriptor storageDescriptor; /** * {@link IStorageTreeComponent}. */ private IStorageTreeComponent<DefaultData> indexingTree; /** * @return the storageDescriptor */ public IStorageDescriptor getStorageDescriptor() { return storageDescriptor; } /** * @param storageDescriptor * the storageDescriptor to set */ public void setStorageDescriptor(IStorageDescriptor storageDescriptor) { this.storageDescriptor = storageDescriptor; } /** * @return the indexingTree */ public IStorageTreeComponent<DefaultData> getIndexingTree() { return indexingTree; } /** * @param indexingTree * the indexingTree to set */ public void setIndexingTree(IStorageTreeComponent<DefaultData> indexingTree) { this.indexingTree = indexingTree; } } }