package rocks.inspectit.server.cache.impl;
import java.text.NumberFormat;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.annotation.PostConstruct;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rocks.inspectit.server.cache.IBuffer;
import rocks.inspectit.server.cache.IBufferElement;
import rocks.inspectit.server.cache.IBufferElement.BufferElementState;
import rocks.inspectit.shared.all.cmr.cache.IObjectSizes;
import rocks.inspectit.shared.all.cmr.property.spring.PropertyUpdate;
import rocks.inspectit.shared.all.communication.DefaultData;
import rocks.inspectit.shared.all.spring.logger.Log;
import rocks.inspectit.shared.cs.indexing.buffer.IBufferTreeComponent;
/**
* Buffer uses atomic variables and references to handle the synchronization. Thus, non of its
* methods is synchronized, nor synchronized block were used. However, the whole buffer is thread
* safe.
*
* @author Ivan Senic
*
* @param <E>
* Parameterized type of elements buffer can hold.
*/
@Component
public class AtomicBuffer<E extends DefaultData> implements IBuffer<E> {
/** The logger of this class. */
@Log
Logger log;
/**
* Buffer properties.
*/
@Autowired
BufferProperties bufferProperties;
/**
* Correct interface for calculating object sizes.
*/
@Autowired
IObjectSizes objectSizes;
/**
* Indexing tree where the elements will be indexed.
*/
@Autowired
IBufferTreeComponent<E> indexingTree;
/**
* Atomic reference to the first object.
*/
private AtomicReference<IBufferElement<E>> first;
/**
* Max size of the buffer in atomic long.
*/
private AtomicLong maxSize;
/**
* Eviction occupancy percentage. The value triggers the eviction when the occupancy of the
* buffer is greater than it. Although it is a float value, atomic integer is used via
* {@link Float#intBitsToFloat(int)} and {@link Float#floatToIntBits(float)} methods.
*/
private AtomicInteger evictionOccupancyPercentage;
/**
* Current size of the buffer in atomic long.
*/
private AtomicLong currentSize = new AtomicLong();
/**
* Number of elements added to the buffer.
*/
private AtomicLong elementsAdded = new AtomicLong();
/**
* Atomic reference to the last object.
*/
AtomicReference<IBufferElement<E>> last;
/**
* Number of elements evicted from the buffer.
*/
private AtomicLong elementsEvicted = new AtomicLong();
/**
* Number of elements that where indexed into indexing tree.
*/
AtomicLong elementsIndexed = new AtomicLong();
/**
* Number of elements that where analyzed.
*/
AtomicLong elementsAnalyzed = new AtomicLong();
/**
* Atomic reference to the object that was analyzed last.
*/
AtomicReference<IBufferElement<E>> lastAnalyzed;
/**
* Eviction lock.
*/
private ReentrantLock evictLock = new ReentrantLock();
/**
* Condition that states that there is nothing to evict currently.
*/
private Condition nothingToEvict = evictLock.newCondition();
/**
* Analyze lock.
*/
private ReentrantLock analyzeLock = new ReentrantLock();
/**
* Condition that states that there is nothing to analyze currently.
*/
private Condition nothingToAnalyze = analyzeLock.newCondition();
/**
* Indexing lock.
*/
private ReentrantLock indexingLock = new ReentrantLock();
/**
* Condition that states that there is nothing to index currently.
*/
private Condition nothingToIndex = indexingLock.newCondition();
/**
* Atomic reference to the object that was indexed last.
*/
private AtomicReference<IBufferElement<E>> lastIndexed;
/**
* Size of the indexing tree.
*/
AtomicLong indexingTreeSize = new AtomicLong();
/**
* Executor service for cleaning the indexing tree.
*/
ExecutorService indexingTreeCleaningExecutorService;
/**
* Data added to the buffer in bytes.
*/
AtomicLong dataAddedInBytes = new AtomicLong();
/**
* Data removed from the buffer in bytes.
*/
AtomicLong dataRemovedInBytes = new AtomicLong();
/**
* Marker for empty buffer element.
*/
EmptyBufferElement emptyBufferElement = new EmptyBufferElement();
/**
* Amount on bytes flags for tree clean and size update will be set.
*/
volatile long flagsSetOnBytes;
/**
* This is the read lock that has to be acquired when the size of the buffer or indexing tree is
* updated.
*/
Lock clearReadLock;
/**
* This is the write lock and has to be acquired when buffer is cleared. This means that during
* the clear of the buffer all operations will be suspended.
*/
private Lock clearWriteLock;
/**
* {@link AnalyzeBufferElementProcessor} that will analyze the size of the elements.
*/
private AnalyzeBufferElementProcessor<E> analyzeProcessor;
/**
* {@link IndexBufferElementProcessor} that will index the elements.
*/
private IndexBufferElementProcessor<E> indexProcessor;
/**
* Default constructor.
*/
public AtomicBuffer() {
ReadWriteLock readWriteCleanLock = new ReentrantReadWriteLock();
clearReadLock = readWriteCleanLock.readLock();
clearWriteLock = readWriteCleanLock.writeLock();
}
/**
* {@inheritDoc}
* <p>
* This method also set the ID of the object that buffer element is holding, thus overwriting
* any earlier set ID.
* <p>
* This method is designed for multiply thread access.
*/
@Override
public void put(IBufferElement<E> element) {
boolean informAnalyzing = false;
boolean informIndexing = false;
// the element that is now first has to have a empty buffer element as next one
element.setNextElement(emptyBufferElement);
while (true) {
// retrieving currently first element
IBufferElement<E> currentlyFirst = first.get();
// only thread that successfully execute compare and set will be able to perform changes
if (first.compareAndSet(currentlyFirst, element)) {
// increment number of added elements
elementsAdded.incrementAndGet();
// if currently first is not pointing to marker, it means that we already have
// elements in the buffer, so connect elements
if (!emptyBufferElement.equals(currentlyFirst)) {
currentlyFirst.setNextElement(element);
// see if last index or analyzed points to the last added element
// if so, inform
informAnalyzing = currentlyFirst == lastAnalyzed.get();
informIndexing = currentlyFirst == lastIndexed.get();
} else {
// otherwise this is the first element in the buffer, so set last
// and inform both indexing and analyzing
last.set(element);
informAnalyzing = true;
informIndexing = true;
}
// break from while
break;
}
}
if (informAnalyzing) {
analyzeLock.lock();
try {
nothingToAnalyze.signal();
} finally {
analyzeLock.unlock();
}
}
if (informIndexing) {
indexingLock.lock();
try {
nothingToIndex.signal();
} finally {
indexingLock.unlock();
}
}
}
/**
* {@inheritDoc}
* <p>
* The executing thread will wait until the current occupancy percentage of the buffer is
* smaller than eviction occupancy percentage. This method also sets the cleaning flag after
* every {@value #elementsCountForMaintenance}th element evicted.
* <p>
* This method is designed for multiply thread access.
*/
@Override
public void evict() throws InterruptedException {
// wait until there is need for eviction
while (!shouldEvict()) {
evictLock.lock();
try {
// check again for avoiding deadlocks
if (!shouldEvict()) {
nothingToEvict.await();
}
} finally {
evictLock.unlock();
}
}
while (true) {
clearReadLock.lock();
try {
// get the currently last element
IBufferElement<E> currentLastElement = last.get();
// check if we really have concrete elements because clear buffer can happen
// anywhere
if (emptyBufferElement.equals(currentLastElement)) {
break;
}
// set up the values for evicting the fragment of elements
IBufferElement<E> newLastElement = currentLastElement;
long evictionFragmentMaxSize = (long) (this.getMaxSize() * bufferProperties.getEvictionFragmentSizePercentage());
long fragmentSize = 0;
int elementsInFragment = 0;
// iterate until size of the eviction fragment is reached
while (fragmentSize < evictionFragmentMaxSize) {
fragmentSize += newLastElement.getBufferElementSize();
newLastElement.setBufferElementState(BufferElementState.EVICTED);
elementsInFragment++;
newLastElement = newLastElement.getNextElement();
// break if we reach the end of queue
if (emptyBufferElement.equals(newLastElement)) {
break;
}
}
// change the last element to the right one
// only thread that execute compare and set successfully can perform changes
if (last.compareAndSet(currentLastElement, newLastElement)) {
// subtract the fragment size
substractFromCurrentSize(fragmentSize);
// add evicted elements to the total count
elementsEvicted.addAndGet(elementsInFragment);
// if the last is now pointing to the empty buffer element, it means that we
// have
// evicted all elements, so first should also point to empty buffer element
// this can only happen in theory
if (emptyBufferElement == last.get()) {
first.set(emptyBufferElement);
}
// break from while
break;
}
} finally {
clearReadLock.unlock();
}
}
}
/**
* {@inheritDoc}
* <p>
* This method is designed for multiply thread access.
*/
@Override
public void analyzeNext() throws InterruptedException {
analyzeProcessor.process();
}
/**
* {@inheritDoc}
* <p>
* This method also performs the cleaning of the indexing tree if the cleaning flag is on.
* <p>
* This method is designed for multiply thread access.
*/
@Override
public void indexNext() throws InterruptedException {
indexProcessor.process();
}
/**
* {@inheritDoc}
*/
@Override
public long getMaxSize() {
return maxSize.get();
}
/**
* {@inheritDoc}
* <p>
* This method is thread safe.
* <p>
* Using this method does not provide any check for the supplied new maximum size. Thus, it is
* responsibility of the user to assure that the given value is correct.
*/
@Override
public void setMaxSize(long maxSize) {
this.maxSize.set(maxSize);
notifyEvictionIfNeeded();
}
/**
* {@inheritDoc}
*/
@Override
public long getCurrentSize() {
return currentSize.get();
}
/**
* Sets the current size of the buffer.
*
* @param currentSize
* Size in bytes.
*/
public void setCurrentSize(long currentSize) {
this.currentSize.set(currentSize);
notifyEvictionIfNeeded();
}
/**
* Adds size value to the current size.
* <p>
* This method is thread safe.
*
* @param size
* Size in bytes.
* @param areObjects
* Defines if the size that is added to the current size relates to the objects in
* buffer. True means size is related to objects in buffer, false means that the size
* relates to the indexing tree.
*/
void addToCurrentSize(long size, boolean areObjects) {
currentSize.addAndGet(size);
notifyEvictionIfNeeded();
if (areObjects) {
dataAddedInBytes.addAndGet(size);
}
}
/**
* Subtracts size value from the current size.
* <p>
* This method is thread safe.
*
* @param size
* Size in bytes.
*/
private void substractFromCurrentSize(long size) {
currentSize.addAndGet(-(size));
dataRemovedInBytes.addAndGet(size);
}
/**
* {@inheritDoc}
*/
@Override
public float getEvictionOccupancyPercentage() {
return Float.intBitsToFloat(evictionOccupancyPercentage.get());
}
/**
* {@inheritDoc}
* <p>
* This method is thread safe.
*/
@Override
public void setEvictionOccupancyPercentage(float evictionOccupancyPercentage) {
this.evictionOccupancyPercentage.set(Float.floatToIntBits(evictionOccupancyPercentage));
notifyEvictionIfNeeded();
}
/**
* {@inheritDoc}
* <p>
* This method is thread safe.
*/
@Override
public float getOccupancyPercentage() {
return ((float) currentSize.get()) / maxSize.get();
}
/**
* {@inheritDoc}
* <p>
* This method is thread safe.
*/
public boolean shouldEvict() {
return getOccupancyPercentage() > Float.intBitsToFloat(evictionOccupancyPercentage.get());
}
/**
* {@inheritDoc}
*/
@Override
public void clearAll() {
clearWriteLock.lock();
try {
last.set(emptyBufferElement);
lastAnalyzed.set(emptyBufferElement);
lastIndexed.set(emptyBufferElement);
setCurrentSize(0);
elementsAdded.set(0);
elementsAnalyzed.set(0);
elementsIndexed.set(0);
elementsEvicted.set(0);
indexingTree.clearAll();
indexingTreeSize.set(0);
dataAddedInBytes.set(0);
dataRemovedInBytes.set(0);
// reference to first has to be reset at the end
first.set(emptyBufferElement);
} finally {
clearWriteLock.unlock();
}
}
/**
* Returns the number of inserted elements since the buffer has been created.
*
* @return Number of inserted elements.
*/
public long getInsertedElemenets() {
return elementsAdded.get();
}
/**
* Returns the number of evicted elements since the buffer has been created.
*
* @return Number of evicted elements.
*/
public long getEvictedElemenets() {
return elementsEvicted.get();
}
/**
* Returns the number of indexed elements since the buffer has been created.
*
* @return Number of indexed elements.
*/
public long getIndexedElements() {
return elementsIndexed.get();
}
/**
* Returns the number of analyzed elements since the buffer has been created.
*
* @return Number of analyzed elements.
*/
public long getAnalyzedElements() {
return elementsAnalyzed.get();
}
/**
* {@inheritDoc}
*/
@Override
public E getOldestElement() {
IBufferElement<E> bufferElement = last.get();
if (null != bufferElement) {
return bufferElement.getObject();
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public E getNewestElement() {
IBufferElement<E> bufferElement = first.get();
if (null != bufferElement) {
return bufferElement.getObject();
}
return null;
}
/**
* Is executed after dependency injection is done to perform any initialization.
*
* @throws Exception
* if an error occurs during {@link PostConstruct}
*/
@PostConstruct
public void postConstruct() throws Exception {
this.maxSize = new AtomicLong(bufferProperties.getInitialBufferSize());
this.evictionOccupancyPercentage = new AtomicInteger(Float.floatToIntBits(bufferProperties.getEvictionOccupancyPercentage()));
this.objectSizes.setObjectSecurityExpansionRate(bufferProperties.getObjectSecurityExpansionRate(maxSize.get()));
this.first = new AtomicReference<IBufferElement<E>>(emptyBufferElement);
this.last = new AtomicReference<IBufferElement<E>>(emptyBufferElement);
this.lastAnalyzed = new AtomicReference<IBufferElement<E>>(emptyBufferElement);
this.lastIndexed = new AtomicReference<IBufferElement<E>>(emptyBufferElement);
this.indexingTreeCleaningExecutorService = Executors.newFixedThreadPool(bufferProperties.getIndexingTreeCleaningThreads());
this.flagsSetOnBytes = bufferProperties.getFlagsSetOnBytes(this.maxSize.get());
// initialize processors
this.analyzeProcessor = new AnalyzeBufferElementProcessor<>(this, lastAnalyzed, analyzeLock, nothingToAnalyze);
this.indexProcessor = new IndexBufferElementProcessor<>(this, lastIndexed, indexingLock, nothingToIndex);
if (log.isInfoEnabled()) {
log.info("|-Using buffer with maximum size " + NumberFormat.getInstance().format(maxSize) + " bytes...");
log.info("|-Indexing tree maintenance on " + NumberFormat.getInstance().format(flagsSetOnBytes) + " bytes added/removed...");
log.info("|-Using object expansion rate of " + NumberFormat.getInstance().format(objectSizes.getObjectSecurityExpansionRate() * 100) + "%");
}
}
/**
* Updates value of the {@link #evictionOccupancyPercentage}.
*/
@PropertyUpdate(properties = { "buffer.evictionOccupancyPercentage", "buffer.bytesMaintenancePercentage", })
protected void updateEvictionOccupancyPercentage() {
this.evictionOccupancyPercentage.set(Float.floatToIntBits(bufferProperties.getEvictionOccupancyPercentage()));
}
/**
* Updates value of the {@link #evictionOccupancyPercentage}.
*/
@PropertyUpdate(properties = { "buffer.bytesMaintenancePercentage", })
protected void updateBytesMaintenancePercentage() {
this.flagsSetOnBytes = bufferProperties.getFlagsSetOnBytes(this.maxSize.get());
}
/**
* Updates the buffer size and to it related properties.
*/
@PropertyUpdate(properties = { "buffer.minOldSpaceOccupancy", "buffer.maxOldSpaceOccupancy", "buffer.minOldSpaceOccupancyActiveTillOldGenSize", "buffer.maxOldSpaceOccupancyActiveFromOldGenSize" })
protected void updateBufferSizeAndRelated() {
this.maxSize.set(bufferProperties.getInitialBufferSize());
this.objectSizes.setObjectSecurityExpansionRate(bufferProperties.getObjectSecurityExpansionRate(maxSize.get()));
this.flagsSetOnBytes = bufferProperties.getFlagsSetOnBytes(this.maxSize.get());
}
/**
* Updates the object security expansion rate.
*/
@PropertyUpdate(properties = { "buffer.minObjectExpansionRate", "buffer.maxObjectExpansionRate", "buffer.maxObjectExpansionRateActiveTillBufferSize",
"buffer.minObjectExpansionRateActiveFromBufferSize", "buffer.maxObjectExpansionRateActiveFromOccupancy", "buffer.minObjectExpansionRateActiveTillOccupancy" })
protected void updateObjectSecurityExpansionRate() {
this.objectSizes.setObjectSecurityExpansionRate(bufferProperties.getObjectSecurityExpansionRate(maxSize.get()));
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
StringBuffer msg = new StringBuffer(256);
msg.append("The buffer occupancy status: ");
msg.append(NumberFormat.getInstance().format(currentSize.get()));
msg.append(" bytes occupied from total ");
msg.append(NumberFormat.getInstance().format(maxSize.get()));
msg.append(" bytes available (");
msg.append(NumberFormat.getInstance().format(getOccupancyPercentage() * 100));
msg.append("%).\nElements processed in the buffer since last clear buffer:\n-Elements added: ");
msg.append(NumberFormat.getInstance().format(elementsAdded.get()));
msg.append("\n-Elements analyzed: ");
msg.append(NumberFormat.getInstance().format(elementsAnalyzed.get()));
msg.append("\n-Elements indexed: ");
msg.append(NumberFormat.getInstance().format(elementsIndexed.get()));
msg.append("\n-Elements evicted: ");
msg.append(NumberFormat.getInstance().format(elementsEvicted.get()));
msg.append('\n');
return msg.toString();
}
/**
* Checks if the eviction should start, and if it does notifies the right thread.
*/
private void notifyEvictionIfNeeded() {
if (shouldEvict()) {
evictLock.lock();
try {
nothingToEvict.signal();
} finally {
evictLock.unlock();
}
}
}
/**
* Class that serves as a marker for empty buffer element.
*
* @author Ivan Senic
*
*/
private class EmptyBufferElement implements IBufferElement<E> {
@Override
public E getObject() {
return null;
}
@Override
public long getBufferElementSize() {
return 0;
}
@Override
public void setBufferElementSize(long size) {
}
@Override
public void calculateAndSetBufferElementSize(IObjectSizes objectSizes) {
}
@Override
public IBufferElement<E> getNextElement() {
return null;
}
@Override
public void setNextElement(IBufferElement<E> element) {
}
@Override
public boolean isAnalyzed() {
return false;
}
@Override
public boolean isEvicted() {
return false;
}
@Override
public boolean isIndexed() {
return false;
}
@Override
public rocks.inspectit.server.cache.IBufferElement.BufferElementState getBufferElementState() {
return null;
}
@Override
public void setBufferElementState(rocks.inspectit.server.cache.IBufferElement.BufferElementState bufferElementState) {
}
}
}