/*
*
* Copyright 2016 Robert Winkler and Bohdan Storozhuk
*
* Licensed 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 io.github.resilience4j.circularbuffer;
import static java.lang.reflect.Array.newInstance;
import static java.util.Objects.requireNonNull;
import java.util.AbstractQueue;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.concurrent.locks.StampedLock;
import java.util.function.Supplier;
/**
*
* The purpose of this queue is to store the N most recently inserted elements.
* If the {@link ConcurrentEvictingQueue} is already full {@code ConcurrentEvictingQueue.size() == capacity},
* the oldest element (the head) will be evicted, and then the new element added at the tail.
*
* In order to achieve thread-safety it utilizes capability-based locking features of {@link StampedLock}.
* All spins optimistic/pessimistic reads and writes are encapsulated in following methods:
*
* <ul>
* <li> {@link ConcurrentEvictingQueue#readConcurrently(Supplier)}</li>
* <li> {@link ConcurrentEvictingQueue#readConcurrentlyWithoutSpin(Supplier)}</li>
* <li> {@link ConcurrentEvictingQueue#writeConcurrently(Supplier)}</li>
* </ul>
*
* All other logic just relies on this utility methods.
*
* Also please take into account that {@link ConcurrentEvictingQueue#size}
* and {@link ConcurrentEvictingQueue#modificationsCount} are {@code volatile} fields,
* so we can read them and compare against them without any additional synchronizations.
*
* This class IS thread-safe, and does NOT accept null elements.
*
*/
public class ConcurrentEvictingQueue<E> extends AbstractQueue<E> {
private static final String ILLEGAL_CAPACITY = "Capacity must be bigger than 0";
private static final String ILLEGAL_ELEMENT = "Element must not be null";
private static final String ILLEGAL_DESTINATION_ARRAY = "Destination array must not be null";
private static final Object[] DEFAULT_DESTINATION = new Object[0];
private static final int RETRIES = 5;
private final int maxSize;
private volatile int size;
private final StampedLock stampedLock;
private Object[] ringBuffer;
private int headIndex;
private int tailIndex;
private int modificationsCount;
public ConcurrentEvictingQueue(int capacity) {
if (capacity <= 0) {
throw new IllegalArgumentException(ILLEGAL_CAPACITY);
}
maxSize = capacity;
ringBuffer = new Object[capacity];
size = 0;
headIndex = 0;
tailIndex = 0;
modificationsCount = 0;
stampedLock = new StampedLock();
}
/**
* Returns an iterator over the elements in this queue in proper sequence.
* The elements will be returned in order from first (head) to last (tail).
* <p>
* This iterator implementation NOT allow removes and co-modifications.
*
* @return an iterator over the elements in this queue in proper sequence
*/
@Override
public Iterator<E> iterator() {
return readConcurrently(() -> new Iter(headIndex, modificationsCount));
}
/**
* Returns the number of elements in this queue.
*
* @return the number of elements in this queue
*/
@Override
public int size() {
return size;
}
/**
* Inserts the specified element at the tail of this queue if it is
* possible to do so immediately or if capacity limit is exited
* the oldest element (the head) will be evicted, and then the new element added at the tail.
* This method is generally preferable to method {@link #add},
* which can fail to insert an element only by throwing an exception.
*
* @throws NullPointerException if the specified element is null
*/
@Override
public boolean offer(final E e) {
requireNonNull(e, ILLEGAL_ELEMENT);
Supplier<Boolean> offerElement = () -> {
if (size == 0) {
ringBuffer[tailIndex] = e;
modificationsCount++;
size++;
} else if (size == maxSize) {
headIndex = nextIndex(headIndex);
tailIndex = nextIndex(tailIndex);
ringBuffer[tailIndex] = e;
modificationsCount++;
} else {
tailIndex = nextIndex(tailIndex);
ringBuffer[tailIndex] = e;
size++;
modificationsCount++;
}
return true;
};
return writeConcurrently(offerElement);
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings("unchecked")
public E poll() {
Supplier<E> pollElement = () -> {
if (size == 0) {
return null;
}
E result = (E) ringBuffer[headIndex];
ringBuffer[headIndex] = null;
if (size != 1) {
headIndex = nextIndex(headIndex);
}
size--;
modificationsCount++;
return result;
};
return writeConcurrently(pollElement);
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings("unchecked")
public E peek() {
return readConcurrently(() -> {
if (size == 0) {
return null;
}
return (E) this.ringBuffer[this.headIndex];
});
}
/**
* Atomically removes all of the elements from this queue.
* The queue will be empty after this call returns.
*/
@Override
public void clear() {
Supplier<Object> clearStrategy = () -> {
if (size == 0) {
return null;
}
Arrays.fill(ringBuffer, null);
size = 0;
headIndex = 0;
tailIndex = 0;
modificationsCount++;
return null;
};
writeConcurrently(clearStrategy);
}
/**
* Returns an array containing all of the elements in this queue, in
* proper sequence.
* <p>The returned array will be "safe" in that no references to it are
* maintained by this queue. (In other words, this method must allocate
* a new array). The caller is free to modify the returned array.
* <p>This method acts as bridge between array-based and collection-based
* APIs.
*
* @return an array containing all of the elements in this queue
*/
@Override
public Object[] toArray() {
if (size == 0) {
return new Object[0];
}
Object[] destination = toArray(DEFAULT_DESTINATION);
return destination;
}
/**
* Returns an array containing all of the elements in this queue, in
* proper sequence; the runtime type of the returned array is that of
* the specified array. If the queue fits in the specified array, it
* is returned therein. Otherwise, a new array is allocated with the
* runtime type of the specified array and the size of this queue.
*
* <p>Like the {@link #toArray()} method, this method acts as bridge between
* array-based and collection-based APIs. Further, this method allows
* precise control over the runtime type of the output array, and may,
* under certain circumstances, be used to save allocation costs.
* Note that {@code toArray(new Object[0])} is identical in function to
* {@code toArray()}.
*
* @param destination the array into which the elements of the queue are to
* be stored, if it is big enough; otherwise, a new array of the
* same runtime type is allocated for this purpose
* @return an array containing all of the elements in this queue
* @throws ArrayStoreException if the runtime type of the specified array
* is not a supertype of the runtime type of every element in
* this queue
* @throws NullPointerException if the specified array is null
*/
@Override
@SuppressWarnings({"unchecked"})
public <T> T[] toArray(final T[] destination) {
requireNonNull(destination, ILLEGAL_DESTINATION_ARRAY);
Supplier<T[]> copyRingBuffer = () -> {
if (size == 0) {
return destination;
}
T[] result = destination;
if (destination.length < size) {
result = (T[]) newInstance(result.getClass().getComponentType(), size);
}
if (headIndex <= tailIndex) {
System.arraycopy(ringBuffer, headIndex, result, 0, size);
} else {
int toTheEnd = ringBuffer.length - headIndex;
System.arraycopy(ringBuffer, headIndex, result, 0, toTheEnd);
System.arraycopy(ringBuffer, 0, result, toTheEnd, tailIndex + 1);
}
return result;
};
return readConcurrentlyWithoutSpin(copyRingBuffer);
}
private int nextIndex(final int ringIndex) {
return (ringIndex + 1) % maxSize;
}
private class Iter implements Iterator<E> {
private int visitedCount = 0;
private int cursor;
private int expectedModificationsCount;
Iter(final int headIndex, final int modificationsCount) {
this.cursor = headIndex;
this.expectedModificationsCount = modificationsCount;
}
@Override
public boolean hasNext() {
return visitedCount < size;
}
@Override
@SuppressWarnings("unchecked")
public E next() {
Supplier<E> nextElement = () -> {
checkForModification();
if (visitedCount >= size) {
throw new NoSuchElementException();
}
E item = (E) ringBuffer[cursor];
cursor = nextIndex(cursor);
visitedCount++;
return item;
};
return readConcurrently(nextElement);
}
private void checkForModification() {
if (modificationsCount != expectedModificationsCount) {
throw new ConcurrentModificationException();
}
}
}
private <T> T readConcurrently(final Supplier<T> readSupplier) {
T result;
long stamp;
for (int i = 0; i < RETRIES; i++) {
stamp = stampedLock.tryOptimisticRead();
if(stamp == 0) {
continue;
}
result = readSupplier.get();
if (stampedLock.validate(stamp)) {
return result;
}
}
stamp = stampedLock.readLock();
try {
result = readSupplier.get();
} finally {
stampedLock.unlockRead(stamp);
}
return result;
}
private <T> T readConcurrentlyWithoutSpin(final Supplier<T> readSupplier) {
T result;
long stamp = stampedLock.readLock();
try {
result = readSupplier.get();
} finally {
stampedLock.unlockRead(stamp);
}
return result;
}
private <T> T writeConcurrently(final Supplier<T> writeSupplier) {
T result;
long stamp = stampedLock.writeLock();
try {
result = writeSupplier.get();
} finally {
stampedLock.unlockWrite(stamp);
}
return result;
}
}