package com.google.code.joto.eventrecorder.impl;
import java.io.File;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.code.joto.eventrecorder.RecordEventData;
import com.google.code.joto.eventrecorder.RecordEventStore;
import com.google.code.joto.eventrecorder.RecordEventStoreChange.AddRecordEventStoreEvent;
import com.google.code.joto.eventrecorder.RecordEventSummary;
/**
* rolling file implementation of RecordEventStore
*
* This class is the optimal candidate to use for production,
* because it limits total file size (no need to cleanup logs).
* It should be combined with asynchronous writer for avoiding performance bottlenecks.
*
*/
public class RollingFileRecordEventStore extends AbstractRecordEventStore {
private static final Logger log = LoggerFactory.getLogger(RollingFileRecordEventStore.class);
/** Factory pattern for RecordEventStore */
public static class RollingFileRecordEventStoreFactory implements RecordEventStoreFactory {
/** internal for java.io.Serializable */
private static final long serialVersionUID = 1L;
private File parentDir;
private String fileBasename;
private String fileSuffix;
public RollingFileRecordEventStoreFactory(File parentDir,
String fileBasename, String fileSuffix) {
this.parentDir = parentDir;
this.fileBasename = fileBasename;
this.fileSuffix = fileSuffix;
}
public RecordEventStore create() {
return new RollingFileRecordEventStore(parentDir, fileBasename, fileSuffix);
}
}
private File parentDir;
private String fileBasename;
private String fileSuffix;
private int maxRollingFiles = 5;
private int maxFileSize = 10 * 1024 * 1024; // 10Mo
private int currRollingFileCount = 1;
/**
* rolled files names, sorted as most recent first
* current = [0] = <<fileBasename>>.<<fileSuffix>>
* prev = [1] = <<fileBasename>>.1.<<fileSuffix>>
* prev-prev = [2] = <<fileBasename>>.2.<<fileSuffix>>
* ...
* oldest = [currRollingFileCount-1] = <<fileBasename>>.<<x>>.<<fileSuffix>>
*/
private FileRecordEventStore[] rolledFiles; // = new FileRecordEventStore[maxRollingFiles];
//-------------------------------------------------------------------------
public RollingFileRecordEventStore(File parentDir, String fileBasename, String fileSuffix) {
this.parentDir = parentDir;
this.fileBasename = fileBasename;
this.fileSuffix = fileSuffix;
}
// getter/setter
//-------------------------------------------------------------------------
public File getParentDir() {
return parentDir;
}
public String getFileBasename() {
return fileBasename;
}
public String getFileSuffix() {
return fileSuffix;
}
public int getMaxRollingFiles() {
return maxRollingFiles;
}
public void setMaxRollingFiles(int p) {
this.maxRollingFiles = p;
// TODO
}
public int getMaxFileSize() {
return maxFileSize;
}
public void setMaxFileSize(int maxFileSize) {
this.maxFileSize = maxFileSize;
}
// -------------------------------------------------------------------------
@Override
public void open(String mode) {
super.setMode(mode);
if (mode.equals("rw")) {
// delete existing files... re-create empty
deleteFiles();
}
rolledFiles = new FileRecordEventStore[maxRollingFiles];
rolledFiles[0] = new FileRecordEventStore(getNthRotateFile(0));
rolledFiles[0].open(mode);
currRollingFileCount = 1;
for (int i = 1; i < maxRollingFiles; i++) {
File nthFile = getNthRotateFile(i);
if (nthFile.exists()) {
rolledFiles[i] = new FileRecordEventStore(nthFile);
rolledFiles[i].open("r");
currRollingFileCount = i + 1;
} else {
break;
}
}
if (currRollingFileCount == maxRollingFiles-1) {
// TODO delete remaining files?
}
// check re-read firstEventId,lastEventId for each fragment..
// TODO
}
@Override
public void close() {
super.close();
for (int i = 0; i < currRollingFileCount; i++) {
rolledFiles[i].close();
rolledFiles[i] = null;
}
currRollingFileCount = 0;
}
public void deleteFiles() {
deleteFiles(0, -1);
}
public void deleteFiles(int fromFileIndex, int toFileIndex) {
if (toFileIndex == -1) toFileIndex = Integer.MAX_VALUE;
for (int i = fromFileIndex; i < toFileIndex; i++) {
File file = getNthRotateFile(i);
if (file.exists()) {
try {
file.delete();
} catch(Exception ex) {
String msg = "Failed to delete file '" + file + "' for rotate!";
log.error(msg, ex);
throw new RuntimeException(msg, ex);
}
} else {
// file not exists... break or find followings?
if (toFileIndex == Integer.MAX_VALUE) {
toFileIndex = i + 10; // give a chance to delete few more
// break;
}
}
}
}
@Override
public void flush() {
rolledFiles[0].flush();
}
@Override
protected RecordEventData doAddEvent(RecordEventSummary eventInfo, Serializable objData) {
RecordEventData res = rolledFiles[0].addEvent(eventInfo, objData);
// update lastId + fire event in parent
super.setLastEventId(res.getEventId() + 1);
fireStoreEvent(new AddRecordEventStoreEvent(res));
if (rolledFiles[0].getLastFilePosition() > maxFileSize) {
// detected need to rotate
rotateFile();
}
return res;
}
@Override
public RecordEventData getEventData(RecordEventSummary evt) {
int eventId = evt.getEventId();
FileRecordEventStore[] array = rolledFiles;
int fileIndex = getFileIndexForEventId(eventId);
RecordEventData res = array[fileIndex].getEventData(evt);
return res;
}
@Override
public List<RecordEventSummary> getEvents(int fromEventId, int toEventId) {
if (fromEventId == 0) {
fromEventId = getFirstEventId();
}
if (toEventId == -1) {
toEventId = getLastEventId();
} else if (toEventId > getLastEventId()) {
throw new IllegalArgumentException();
}
List<RecordEventSummary> res = new ArrayList<RecordEventSummary>(toEventId - fromEventId);
FileRecordEventStore[] array = rolledFiles;
int currFileIndex = getFileIndexForEventId(fromEventId);
for (int currFromEventId = fromEventId; currFromEventId < toEventId; ) {
int currLastEventId = array[currFileIndex].getLastEventId();
if (toEventId >= currLastEventId) {
// get events fragment and continue
List<RecordEventSummary> tmpres =
array[currFileIndex].getEvents(currFromEventId, currLastEventId);
res.addAll(tmpres);
currFromEventId = currLastEventId;
currFileIndex--;
if (currFileIndex == -1) {
break; // should not occur?!
}
} else {
// finish get
List<RecordEventSummary> tmpres =
array[currFileIndex].getEvents(currFromEventId, toEventId);
res.addAll(tmpres);
break;
}
}
return res;
}
@Override
public synchronized void purgeEvents(int toEventId) {
int fileIndex = getFileIndexForEventId(toEventId);
int purgeAfterFileIndex = fileIndex + 1; // TOCHECK.. round
// int oldFirstEventId = getFirstEventId();
int effectiveNewFirstEventId = rolledFiles[fileIndex].getFirstEventId();
for (int i = purgeAfterFileIndex; i < currRollingFileCount; i++) {
closeAndDeleteNthRolledFile(i);
}
currRollingFileCount = purgeAfterFileIndex;
super.onTruncateSetFirstEventId(effectiveNewFirstEventId, null);
}
// -------------------------------------------------------------------------
public File getNthRotateFile(int index) {
String fn = this.fileBasename
+ ((index == 0)? "" : "." + index)
+ this.fileSuffix;
return new File(parentDir, fn);
}
public synchronized void rotateFile() {
if (currRollingFileCount == maxRollingFiles) {
// delete last file (or truncate+rename to reuse as curr?)
FileRecordEventStore oldToDelete = rolledFiles[maxRollingFiles-1];
int newFirstEventId = oldToDelete.getLastEventId();
onTruncateSetFirstEventId(newFirstEventId, null);
closeAndDeleteNthRolledFile(maxRollingFiles-1);
} else {
currRollingFileCount++;
}
int lastEventId = getLastEventId();
FileRecordEventStore prev0 = rolledFiles[0];
// old file[0] in readwrite mode + is now file[1] in readonly mode
// optim (equivalent?!) to: prev0.close(); prev0.open("r");
prev0.setReadonly();
// rotate files
for(int i = currRollingFileCount-1; i > 0; i--) {
rolledFiles[i] = rolledFiles[i-1];
File dest = getNthRotateFile(i);
rolledFiles[i].renameFile(dest);
}
rolledFiles[0] = null;
// create new(0), in append mode
FileRecordEventStore new0 = new FileRecordEventStore(getNthRotateFile(0));
rolledFiles[0] = new0;
new0.initSetFirstEventId(lastEventId);
new0.setLastEventId(lastEventId);
String mode0 = canRead? "rw" : "w";
new0.open(mode0);
}
protected void closeAndDeleteNthRolledFile(int index) {
FileRecordEventStore rolledFile = rolledFiles[index];
try {
rolledFile.close();
} catch(Exception ex) {
log.warn("Failed to close last file for rotate!", ex);
}
rolledFiles[index] = null;
File file = getNthRotateFile(index);
try {
file.delete();
} catch(Exception ex) {
String msg = "Failed to delete file '" + file + "' for rotate!";
log.error(msg, ex);
throw new RuntimeException(msg, ex);
}
}
protected int getFileIndexForEventId(int eventId) {
int res = -1;
FileRecordEventStore[] array = rolledFiles;
if (eventId > array[0].getLastEventId()) {
throw new IllegalArgumentException("invalid eventId, > lastEventId (: " + eventId + " > " + array[0].getLastEventId() + ")");
}
if (eventId < getFirstEventId()) {
throw new IllegalArgumentException("event already purged: id=" + eventId + " < " + getFirstEventId());
}
for (int i = currRollingFileCount-1; i >= 0; i--) {
if (eventId < array[i].getLastEventId()) {
res = i;
break;
}// else continue with next rolledFile
}
if (res == -1) {
throw new IllegalArgumentException("event already purged: id=" + eventId + " < " + getFirstEventId());
}
return res;
}
}