/*
* Copyright (c) MuleSoft, Inc. All rights reserved. http://www.mulesoft.com
* The software in this package is published under the terms of the CPAL v1.0
* license, a copy of which has been included with this distribution in the
* LICENSE.txt file.
*/
package org.mule.runtime.core.util.queue;
import org.mule.runtime.api.exception.MuleRuntimeException;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Basic queueing functionality with file storage.
*/
class RandomAccessFileQueueStore {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
protected static final int CONTROL_DATA_SIZE = 5;
private static final byte NOT_REMOVED = 0;
private static final byte REMOVED = 1;
private final QueueFileProvider queueFileProvider;
private LinkedList<Long> orderedKeys = new LinkedList<Long>();
private long fileTotalSpace = 0;
public RandomAccessFileQueueStore(QueueFileProvider queueFileProvider) {
this.queueFileProvider = queueFileProvider;
initialise();
}
/**
* @return the File where the content is stored.
*/
public File getFile() {
return this.queueFileProvider.getFile();
}
/**
* Adds element at the end of the queue.
*
* @param element element to add
*/
public synchronized void addLast(byte[] element) {
long filePointer = writeData(element);
orderedKeys.addLast(filePointer);
}
/**
* Remove and returns data from the queue.
*
* @return data from the beginning of the queue.
* @throws InterruptedException
*/
public synchronized byte[] removeFirst() throws InterruptedException {
try {
if (orderedKeys.isEmpty()) {
return null;
}
Long filePosition = orderedKeys.getFirst();
queueFileProvider.getRandomAccessFile().seek(filePosition);
queueFileProvider.getRandomAccessFile().writeByte(RandomAccessFileQueueStore.REMOVED);
byte[] data = readDataInCurrentPosition();
orderedKeys.removeFirst();
return data;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Retrieves the first element from the queue without removing it.
*
* @return first element from the queue.
* @throws InterruptedException
*/
public synchronized byte[] getFirst() throws InterruptedException {
return readFirstValue();
}
/**
* Adds an element in the beginning of the queue.
*
* @param item element to add.
* @throws InterruptedException
*/
public synchronized void addFirst(byte[] item) throws InterruptedException {
orderedKeys.addFirst(writeData(item));
}
/**
* @return the size of the queue.
*/
public int getSize() {
return orderedKeys.size();
}
/**
* removes all the elements from the queue.
*/
public synchronized void clear() {
try {
queueFileProvider.getRandomAccessFile().close();
orderedKeys.clear();
fileTotalSpace = 0;
queueFileProvider.recreate();
} catch (IOException e) {
throw new MuleRuntimeException(e);
}
}
/**
* Adds a collection of elements at the end of the queue.
*
* @param items collection of elements to add.
* @return true if it were able to add them all, false otherwise.
*/
public synchronized boolean addAll(Collection<? extends byte[]> items) {
for (byte[] item : items) {
addLast(item);
}
return true;
}
/**
* Use this method carefully since it required bit amount of IO.
*
* @return all the elements from the queue.
*/
public synchronized Collection<byte[]> allElements() {
List<byte[]> elements = new LinkedList<byte[]>();
try {
queueFileProvider.getRandomAccessFile().seek(0);
while (true) {
boolean removed = queueFileProvider.getRandomAccessFile().readBoolean();
if (!removed) {
elements.add(readDataInCurrentPosition());
} else {
moveFilePointerToNextData();
}
}
} catch (IOException e) {
if (logger.isDebugEnabled()) {
logger.debug("Error reading queue elements", e);
}
}
return elements;
}
/**
* @return true if there's no elements in the queue, false otherwise
*/
public boolean isEmpty() {
return orderedKeys.isEmpty();
}
/**
* Removes data from the queue according to a {@link RawDataSelector} instance that determines if a certain element must be
* removed.
*
* @param rawDataSelector to determine if the element must be removed.
* @return true if an element was removed
*/
public synchronized boolean remove(RawDataSelector rawDataSelector) {
try {
queueFileProvider.getRandomAccessFile().seek(0);
while (true) {
long currentPosition = queueFileProvider.getRandomAccessFile().getFilePointer();
byte removed = queueFileProvider.getRandomAccessFile().readByte();
if (removed == 0) {
byte[] data = readDataInCurrentPosition();
if (rawDataSelector.isSelectedData(data)) {
queueFileProvider.getRandomAccessFile().seek(currentPosition);
queueFileProvider.getRandomAccessFile().writeByte(REMOVED);
orderedKeys.remove(currentPosition);
return true;
}
}
}
} catch (EOFException e) {
if (logger.isDebugEnabled()) {
logger.debug("Error removing queue element", e);
}
return false;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Free all resources held for the queue.
* <p/>
* Do not removes elements from the queue.
*/
public synchronized void close() {
try {
this.queueFileProvider.close();
} catch (IOException e) {
logAndIgnore(e);
}
}
private void logAndIgnore(IOException e) {
logger.warn(e.getMessage());
if (logger.isDebugEnabled()) {
logger.debug("Error closing queue store", e);
}
}
/**
* Deletes the files backing this queue. This method must only be invoked after {@link #close()} has been executed on
* {@code this} instance
*/
public synchronized void delete() {
queueFileProvider.delete();
}
private byte[] readDataInCurrentPosition() throws IOException {
int serializedValueSize = queueFileProvider.getRandomAccessFile().readInt();
byte[] data = new byte[serializedValueSize];
queueFileProvider.getRandomAccessFile().read(data, 0, serializedValueSize);
return data;
}
private long writeData(byte[] data) {
try {
if (getSize() > 0) {
queueFileProvider.getRandomAccessFile().seek(fileTotalSpace);
}
long filePointer = queueFileProvider.getRandomAccessFile().getFilePointer();
int totalBytesRequired = CONTROL_DATA_SIZE + data.length;
ByteBuffer byteBuffer = ByteBuffer.allocate(totalBytesRequired);
byteBuffer.put(NOT_REMOVED);
byteBuffer.putInt(data.length);
byteBuffer.put(data);
queueFileProvider.getRandomAccessFile().write(byteBuffer.array());
fileTotalSpace += totalBytesRequired;
return filePointer;
} catch (IOException e) {
throw new MuleRuntimeException(e);
}
}
private void initialise() {
try {
queueFileProvider.getRandomAccessFile().seek(0);
while (true) {
long position = queueFileProvider.getRandomAccessFile().getFilePointer();
byte removed = queueFileProvider.getRandomAccessFile().readByte();
if (removed == NOT_REMOVED) {
orderedKeys.add(position);
moveFilePointerToNextData();
} else {
moveFilePointerToNextData();
}
}
} catch (EOFException e) {
try {
fileTotalSpace = queueFileProvider.getRandomAccessFile().length();
} catch (IOException ioe) {
throw new MuleRuntimeException(e);
}
if (logger.isDebugEnabled()) {
logger.debug("Error initializing queue store", e);
}
} catch (Exception e) {
throw new MuleRuntimeException(e);
}
}
private byte[] readFirstValue() {
try {
if (orderedKeys.isEmpty()) {
return null;
}
Long filePointer = orderedKeys.getFirst();
queueFileProvider.getRandomAccessFile().seek(filePointer);
queueFileProvider.getRandomAccessFile().readByte(); // Always true since it's a key
return readDataInCurrentPosition();
} catch (IOException e) {
throw new MuleRuntimeException(e);
}
}
private void moveFilePointerToNextData() throws IOException {
int serializedValueSize = queueFileProvider.getRandomAccessFile().readInt();
queueFileProvider.getRandomAccessFile().seek(queueFileProvider.getRandomAccessFile().getFilePointer() + serializedValueSize);
}
/**
* @return the length of the file in bytes.
*/
public long getLength() {
return fileTotalSpace;
}
/**
* Searches for data within the queue store using a {@link RawDataSelector}
*
* @param rawDataSelector to determine if the element is the one we are looking for
* @return true if an element exists within the queue, false otherwise
*/
public synchronized boolean contains(RawDataSelector rawDataSelector) {
try {
queueFileProvider.getRandomAccessFile().seek(0);
while (true) {
byte removed = queueFileProvider.getRandomAccessFile().readByte();
if (removed == NOT_REMOVED) {
byte[] data = readDataInCurrentPosition();
if (rawDataSelector.isSelectedData(data)) {
return true;
}
} else {
moveFilePointerToNextData();
}
}
} catch (EOFException e) {
if (logger.isDebugEnabled()) {
logger.debug("Cannot determine if element is contained in the queue store", e);
}
return false;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}