package org.f1x.v1.state;
import org.gflogger.GFLog;
import org.gflogger.GFLogFactory;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
/**
* Session State backed by memory mapped file. Each update action is persistent.
*
* Thread safe.
*
* NOTE: Not suitable for IKVM environment due to frequent allocations (System.IntPtr ) made by IKVM implementation of DirectByteBuffer.
*
* <pre>
* Offset | Description
* =================================
* 0 | last logon timestamp
* 17 | next sender seq num
* 26 | next target seq num
* </pre>
*/
public class MemoryMappedSessionState extends AbstractSessionState {
protected static final GFLog LOGGER = GFLogFactory.getLog(MemoryMappedSessionState.class);
protected static final int SIZE_OF_BYTE = 1;
protected static final int SIZE_OF_INT = 4;
protected static final int SIZE_OF_LONG = 8;
protected static final int NUM_OF_STORAGE_BYTES = LongTransaction.getSize() + IntTransaction.getSize() + IntTransaction.getSize();
protected static final int LAST_LOGON_TIMESTAMP_OFFSET = 0;
protected static final int NEXT_SENDER_SEQ_NUM_OFFSET = LongTransaction.getSize();
protected static final int NEXT_TARGET_SEQ_NUM_OFFSET = NEXT_SENDER_SEQ_NUM_OFFSET + IntTransaction.getSize();
protected final LongTransaction lastLogonTimestamp;
protected final IntTransaction nextSenderSeqNum;
protected final IntTransaction nextTargetSeqNum;
final MappedByteBuffer buffer; // only for tests
public MemoryMappedSessionState(String filePath) throws IOException {
this(Paths.get(filePath));
}
public MemoryMappedSessionState(File file) throws IOException {
this(file.toPath());
}
public MemoryMappedSessionState(Path file) throws IOException {
boolean justCreated = check(file);
this.buffer = map(file, justCreated);
this.lastLogonTimestamp = new LongTransaction(buffer, LAST_LOGON_TIMESTAMP_OFFSET);
this.nextSenderSeqNum = new IntTransaction(buffer, NEXT_SENDER_SEQ_NUM_OFFSET);
this.nextTargetSeqNum = new IntTransaction(buffer, NEXT_TARGET_SEQ_NUM_OFFSET);
if (justCreated)
setDefaults();
}
@Override
public void flush() {
buffer.force(); // no need to do that?
}
@Override
public final void setLastConnectionTimestamp(long newValue) {
synchronized (lastLogonTimestamp) {
lastLogonTimestamp.write(newValue);
}
}
@Override
public long getLastConnectionTimestamp() {
synchronized (lastLogonTimestamp) {
return lastLogonTimestamp.read();
}
}
@Override
public final void setNextSenderSeqNum(int newValue) {
synchronized (nextSenderSeqNum) {
nextSenderSeqNum.write(newValue);
}
}
@Override
public int getNextSenderSeqNum() {
synchronized (nextSenderSeqNum) {
return nextSenderSeqNum.read();
}
}
@Override
public int consumeNextSenderSeqNum() {
synchronized (nextSenderSeqNum) {
int currentValue = nextSenderSeqNum.read();
nextSenderSeqNum.write(currentValue + 1);
return currentValue;
}
}
@Override
public final void setNextTargetSeqNum(int newValue) {
synchronized (nextTargetSeqNum) {
nextTargetSeqNum.write(newValue);
}
}
@Override
public int getNextTargetSeqNum() {
synchronized (nextTargetSeqNum) {
return nextTargetSeqNum.read();
}
}
@Override
public int consumeNextTargetSeqNum() {
synchronized (nextTargetSeqNum) {
int currentValue = nextTargetSeqNum.read();
nextTargetSeqNum.write(currentValue + 1);
return currentValue;
}
}
protected final void setDefaults() {
setLastConnectionTimestamp(-1);
setNextSenderSeqNum(1);
setNextTargetSeqNum(1);
}
protected static void setToZero(MappedByteBuffer buffer) {
for (int index = 0; index < buffer.capacity(); index++)
buffer.put(index, (byte) 0);
}
/**
* Checks the file path. Creates the file and parent directories if needed.
* @return true if the file has just been created otherwise false
*/
protected static boolean check(Path file) throws IOException {
boolean exists = Files.exists(file);
if (exists) {
if (!Files.isRegularFile(file))
throw new FileNotFoundException(file + " is not file");
// TODO: check content
} else {
Path dir = file.getParent();
if (dir != null && !Files.exists(dir))
createDirectories(file, dir);
createFile(file);
}
return !exists;
}
protected static void createFile(Path file) throws IOException {
try {
Files.createFile(file);
} catch (IOException e) {
LOGGER.error().append("During creating file: ").append(file).append(" error occurred: ").append(e).commit();
throw e;
}
}
protected static void createDirectories(Path file, Path dir) throws IOException {
try {
Files.createDirectories(dir);
} catch (IOException e) {
LOGGER.error().append("During creating parent directories for file: ").append(file).append(" error occurred: ").append(e).commit();
throw e;
}
}
protected static MappedByteBuffer map(Path file, boolean setToZero) throws IOException {
try {
FileChannel channel = null;
try {
channel = FileChannel.open(file, StandardOpenOption.WRITE, StandardOpenOption.READ);
MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, NUM_OF_STORAGE_BYTES);
if (setToZero)
setToZero(buffer);
return buffer;
} finally {
if (channel != null)
channel.close();
}
} catch (Throwable e) {
LOGGER.error().append("During opening file: ").append(file).append(" error occurred: ").append(e).commit();
throw e;
}
}
/**
* Definitions:
* 1. readCell - cell from which to read (with 0 or 1 index)
* 2. writeCell - cell to which to write (with 0 or 1 index)
* 3. index - refers to read cell (0 or 1)
* <p/>
* Transactions:
* 1. read transaction:
* a) read index
* b) read from readCell
* 2. write transaction:
* a) read index
* b) write to writeCell
* c) modify index
* <p/>
* Buffer:
* 1. cell 0 (read or write cell)
* 2. cell 1 (read or write cell)
* 3. index (0 or 1)
*/
protected static abstract class Transaction {
protected final MappedByteBuffer buffer;
protected final int[] cellIndexes;
protected final int indexIndex;
protected Transaction(MappedByteBuffer buffer, int cell0Index, int cell1Index, int indexIndex) {
this.buffer = buffer;
this.cellIndexes = new int[]{cell0Index, cell1Index};
this.indexIndex = indexIndex;
readIndex(); // check file format
}
protected final byte readIndex() {
byte index = buffer.get(indexIndex);
if (index != 0 && index != 1)
throw new IllegalArgumentException("Invalid file format, index value must be 0 or 1 and not: " + index);
return index;
}
protected void writeIndex(byte index) {
buffer.put(indexIndex, index);
}
}
protected static class IntTransaction extends Transaction {
protected static final int SIZE = SIZE_OF_INT + SIZE_OF_INT + SIZE_OF_BYTE;
protected IntTransaction(MappedByteBuffer buffer, int offset) {
super(buffer, offset, offset + SIZE_OF_INT, offset + 2 * SIZE_OF_INT);
}
protected int read() {
int indexToReadCell = readIndex();
int readCellIndex = cellIndexes[indexToReadCell];
return buffer.getInt(readCellIndex);
}
protected void write(int value) {
byte indexToReadCell = readIndex();
byte indexToWriteCell = (byte) (indexToReadCell == 0 ? 1 : 0);
int writeCellIndex = cellIndexes[indexToWriteCell];
buffer.putInt(writeCellIndex, value);
writeIndex(indexToWriteCell);
}
protected static int getSize() {
return SIZE;
}
}
protected static class LongTransaction extends Transaction {
protected static final int SIZE = SIZE_OF_LONG + SIZE_OF_LONG + SIZE_OF_BYTE;
protected LongTransaction(MappedByteBuffer buffer, int offset) {
super(buffer, offset, offset + SIZE_OF_LONG, offset + 2 * SIZE_OF_LONG);
}
protected long read() {
int indexToReadCell = readIndex();
int readCellIndex = cellIndexes[indexToReadCell];
return buffer.getLong(readCellIndex);
}
protected void write(long value) {
byte indexToReadCell = readIndex();
byte indexToWriteCell = (byte) (1 - indexToReadCell);
int writeCellIndex = cellIndexes[indexToWriteCell];
buffer.putLong(writeCellIndex, value);
writeIndex(indexToWriteCell);
}
protected static int getSize() {
return SIZE;
}
}
}