package org.infinispan.persistence.sifs;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.infinispan.util.logging.LogFactory;
/**
* Provides resource management for files - only limited amount of files may be opened in one moment, and opened file
* should not be deleted. Also allows to generate file indexes.
*
* @author Radim Vansa <rvansa@redhat.com>
*/
class FileProvider {
private static final org.infinispan.util.logging.Log log = LogFactory.getLog(FileProvider.class);
private final File dataDir;
private final int openFileLimit;
private final ArrayBlockingQueue<Record> recordQueue;
private final ConcurrentMap<Integer, Record> openFiles = new ConcurrentHashMap<Integer, Record>();
private final AtomicInteger currentOpenFiles = new AtomicInteger(0);
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Set<Integer> logFiles = new HashSet<Integer>();
private int nextFileId = 0;
public FileProvider(String dataDir, int openFileLimit) {
this.openFileLimit = openFileLimit;
this.recordQueue = new ArrayBlockingQueue<Record>(openFileLimit);
this.dataDir = new File(dataDir);
this.dataDir.mkdirs();
}
public Handle getFile(int fileId) throws IOException {
lock.readLock().lock();
try {
for (;;) {
Record record = openFiles.get(fileId);
if (record == null) {
for (;;) {
int open = currentOpenFiles.get();
if (open >= openFileLimit) {
// we'll continue only after some other file will be closed
if (tryCloseFile()) break;
} else {
if (currentOpenFiles.compareAndSet(open, open + 1)) {
break;
}
}
}
// now we have either removed some other opened file or incremented the value below limit
for (;;) {
FileChannel fileChannel;
try {
fileChannel = openChannel(fileId);
} catch (FileNotFoundException e) {
currentOpenFiles.decrementAndGet();
log.debug("File " + fileId + " was not found", e);
return null;
}
Record newRecord = new Record(fileChannel, fileId);
Record other = openFiles.putIfAbsent(fileId, newRecord);
if (other != null) {
fileChannel.close();
synchronized (other) {
if (other.isOpen()) {
// we have allocated opening a new file but then we use an old one
currentOpenFiles.decrementAndGet();
return new Handle(other);
}
}
} else {
Handle handle;
synchronized (newRecord) {
// the new file cannot be closed but it can be simultaneously fetched multiple times
if (!newRecord.isOpen()) {
throw new IllegalStateException();
}
handle = new Handle(newRecord);
}
try {
recordQueue.put(newRecord);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return handle;
}
}
}
synchronized (record) {
if (record.isOpen()) {
return new Handle(record);
}
}
}
} finally {
lock.readLock().unlock();
}
}
public long getFileSize(int file) {
lock.readLock().lock();
try {
if (logFiles.contains(file)) {
return -1;
}
return new File(dataDir, String.valueOf(file)).length();
} finally {
lock.readLock().unlock();
}
}
private boolean tryCloseFile() throws IOException {
Record removed;
try {
removed = recordQueue.take();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (removed) {
if (removed.isUsed()) {
try {
recordQueue.put(removed);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
if (removed.isOpen()) {
// if the file was marked deleteOnClose it may have been already closed, but it couldn't be removed from
// the queue
removed.close();
openFiles.remove(removed.getFileId(), removed);
}
return true;
}
}
return false;
}
protected FileChannel openChannel(int fileId) throws FileNotFoundException {
return new RandomAccessFile(new File(dataDir, String.valueOf(fileId)), "r").getChannel();
}
public Log getFileForLog() throws IOException {
lock.writeLock().lock();
try {
for (;;) {
File f = new File(dataDir, String.valueOf(nextFileId));
if (f.exists()) {
if (nextFileId == Integer.MAX_VALUE) {
nextFileId = 0;
} else {
nextFileId++;
}
} else {
logFiles.add(nextFileId);
return new Log(nextFileId, new FileOutputStream(new File(dataDir, String.valueOf(nextFileId))).getChannel());
}
}
} finally {
lock.writeLock().unlock();
}
}
public Iterator<Integer> getFileIterator() {
Set<Integer> set = new HashSet<Integer>();
for (String file : dataDir.list()) {
if (file.matches("[0-9]*")) {
set.add(Integer.parseInt(file));
}
}
return set.iterator();
}
public void clear() throws IOException {
lock.writeLock().lock();
log.debug("Dropping all data");
while (currentOpenFiles.get() > 0) {
if (tryCloseFile()) {
if (currentOpenFiles.decrementAndGet() == 0) {
break;
}
}
}
if (!recordQueue.isEmpty()) throw new IllegalStateException();
if (!openFiles.isEmpty()) throw new IllegalStateException();
for (File file : dataDir.listFiles()) {
if (!file.delete()) {
throw new IOException("Cannot delete file " + file);
}
}
lock.writeLock().unlock();
}
public void deleteFile(int fileId) {
lock.readLock().lock();
try {
for (;;) {
Record newRecord = new Record(null, fileId);
Record record = openFiles.putIfAbsent(fileId, newRecord);
if (record == null) {
newRecord.delete();
openFiles.remove(fileId, newRecord);
return;
}
synchronized (record) {
if (openFiles.get(fileId) == record) {
try {
record.deleteOnClose();
} catch (IOException e) {
log.error("Cannot close/delete file " + fileId, e);
}
break;
}
}
}
} finally {
lock.readLock().unlock();
}
}
public void stop() {
int open = currentOpenFiles.get();
while (open > 0) {
try {
if (tryCloseFile()) {
open = currentOpenFiles.decrementAndGet();
} else {
// we can't close any further file
break;
}
} catch (IOException e) {
log.error("Failed to close file", e);
}
}
if (currentOpenFiles.get() != 0) {
for (Map.Entry<Integer, Record> entry : openFiles.entrySet()) {
log.warn("File " + entry.getKey() + " open: " + entry.getValue().handleCount + " handles");
}
}
}
public final class Log implements Closeable {
public final int fileId;
public final FileChannel fileChannel;
public Log(int fileId, FileChannel fileChannel) {
this.fileId = fileId;
this.fileChannel = fileChannel;
}
@Override
public void close() throws IOException {
fileChannel.close();
lock.writeLock().lock();
try {
logFiles.remove(fileId);
} finally {
lock.writeLock().unlock();
}
}
}
public static final class Handle implements Closeable {
private boolean usable = true;
private Record record;
private Handle(Record record) {
this.record = record;
record.increaseHandleCount();
}
public int read(ByteBuffer buffer, long offset) throws IOException {
if (!usable) throw new IllegalStateException();
return record.getFileChannel().read(buffer, offset);
}
@Override
public void close() throws IOException {
usable = false;
synchronized (record) {
record.decreaseHandleCount();
}
}
public long getFileSize() throws IOException {
return record.fileChannel.size();
}
public int getFileId() {
return record.getFileId();
}
}
private class Record {
private final int fileId;
private FileChannel fileChannel;
private int handleCount;
private boolean deleteOnClose = false;
private Record(FileChannel fileChannel, int fileId) {
this.fileChannel = fileChannel;
this.fileId = fileId;
}
FileChannel getFileChannel() {
return fileChannel;
}
void increaseHandleCount() {
handleCount++;
}
void decreaseHandleCount() throws IOException {
handleCount--;
if (handleCount == 0 && deleteOnClose) {
// we cannot easily remove the record from queue - keep it there until collection,
// but physically close and delete the file
fileChannel.close();
fileChannel = null;
openFiles.remove(fileId, this);
delete();
}
}
boolean isOpen() {
return fileChannel != null;
}
boolean isUsed() {
return handleCount > 0;
}
public int getFileId() {
return fileId;
}
public void close() throws IOException {
fileChannel.close();
fileChannel = null;
if (deleteOnClose) {
delete();
}
}
public void delete() {
log.debug("Deleting file " + fileId);
new File(dataDir, String.valueOf(fileId)).delete();
}
public void deleteOnClose() throws IOException {
if (handleCount == 0) {
if (fileChannel != null) {
fileChannel.close();
fileChannel = null;
}
openFiles.remove(fileId, this);
delete();
} else {
log.debug("Marking file " + fileId + " for deletion");
deleteOnClose = true;
}
}
}
}