Updated storage to no longer try to compress the data, and to correctly use input/output streams from the RandomAccessFile. Also updated the tests for storage
This commit is contained in:
parent
545c975d61
commit
72a4380383
@ -34,7 +34,6 @@ import java.util.concurrent.locks.ReentrantLock;
|
|||||||
* Be wary of opening the database file in different JVM instances. Even with file-locks, you can corrupt the data.
|
* Be wary of opening the database file in different JVM instances. Even with file-locks, you can corrupt the data.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public
|
|
||||||
class DiskStorage implements Storage {
|
class DiskStorage implements Storage {
|
||||||
private final DelayTimer timer;
|
private final DelayTimer timer;
|
||||||
private final ByteArrayWrapper defaultKey;
|
private final ByteArrayWrapper defaultKey;
|
||||||
@ -59,18 +58,17 @@ class DiskStorage implements Storage {
|
|||||||
this.timer = null;
|
this.timer = null;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
this.timer = new DelayTimer("Storage Writer", false, new DelayTimer.Callback() {
|
this.timer = new DelayTimer("Storage Writer", false, new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public
|
public
|
||||||
void execute() {
|
void run() {
|
||||||
Map<ByteArrayWrapper, Object> actions = DiskStorage.this.actionMap;
|
|
||||||
|
|
||||||
ReentrantLock actionLock2 = DiskStorage.this.actionLock;
|
ReentrantLock actionLock2 = DiskStorage.this.actionLock;
|
||||||
|
|
||||||
|
Map<ByteArrayWrapper, Object> actions;
|
||||||
try {
|
try {
|
||||||
actionLock2.lock();
|
actionLock2.lock();
|
||||||
|
|
||||||
// do a fast swap on the actionMap.
|
// do a fast swap on the actionMap.
|
||||||
|
actions = DiskStorage.this.actionMap;
|
||||||
DiskStorage.this.actionMap = new ConcurrentHashMap<ByteArrayWrapper, Object>();
|
DiskStorage.this.actionMap = new ConcurrentHashMap<ByteArrayWrapper, Object>();
|
||||||
} finally {
|
} finally {
|
||||||
actionLock2.unlock();
|
actionLock2.unlock();
|
||||||
@ -207,7 +205,7 @@ class DiskStorage implements Storage {
|
|||||||
Object source = get0(key);
|
Object source = get0(key);
|
||||||
|
|
||||||
if (source == null) {
|
if (source == null) {
|
||||||
// returned was null, so we should take value as the default
|
// returned was null, so we should save the default value
|
||||||
putAndSave(key, data);
|
putAndSave(key, data);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@ -412,6 +410,7 @@ class DiskStorage implements Storage {
|
|||||||
throw new RuntimeException("Unable to act on closed storage");
|
throw new RuntimeException("Unable to act on closed storage");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//noinspection SimplifiableIfStatement
|
||||||
if (timer != null) {
|
if (timer != null) {
|
||||||
return this.timer.isWaiting();
|
return this.timer.isWaiting();
|
||||||
}
|
}
|
||||||
|
@ -22,9 +22,8 @@ import java.io.IOException;
|
|||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
* Storage that is in memory only (and is not persisted to disk)
|
||||||
*/
|
*/
|
||||||
public
|
|
||||||
class MemoryStorage implements Storage {
|
class MemoryStorage implements Storage {
|
||||||
private final ConcurrentHashMap<ByteArrayWrapper, Object> storage;
|
private final ConcurrentHashMap<ByteArrayWrapper, Object> storage;
|
||||||
private final ByteArrayWrapper defaultKey;
|
private final ByteArrayWrapper defaultKey;
|
||||||
|
@ -23,11 +23,11 @@ import dorkbox.util.bytes.ByteArrayWrapper;
|
|||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
import java.io.RandomAccessFile;
|
import java.io.RandomAccessFile;
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.nio.channels.FileLock;
|
import java.nio.channels.FileLock;
|
||||||
import java.util.zip.DeflaterOutputStream;
|
|
||||||
import java.util.zip.InflaterInputStream;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public
|
public
|
||||||
@ -81,7 +81,7 @@ class Metadata {
|
|||||||
*/
|
*/
|
||||||
static
|
static
|
||||||
long getMetaDataPointer(int position) {
|
long getMetaDataPointer(int position) {
|
||||||
return StorageBase.FILE_HEADERS_REGION_LENGTH + INDEX_ENTRY_LENGTH * position;
|
return StorageBase.FILE_HEADERS_REGION_LENGTH + ((long) INDEX_ENTRY_LENGTH) * position;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -135,8 +135,10 @@ class Metadata {
|
|||||||
|
|
||||||
FileLock lock = file.getChannel()
|
FileLock lock = file.getChannel()
|
||||||
.lock(origHeaderKeyPointer, INDEX_ENTRY_LENGTH, true);
|
.lock(origHeaderKeyPointer, INDEX_ENTRY_LENGTH, true);
|
||||||
|
|
||||||
file.seek(origHeaderKeyPointer);
|
file.seek(origHeaderKeyPointer);
|
||||||
file.readFully(buf);
|
file.readFully(buf);
|
||||||
|
|
||||||
lock.release();
|
lock.release();
|
||||||
|
|
||||||
Metadata r = new Metadata(ByteArrayWrapper.wrap(buf));
|
Metadata r = new Metadata(ByteArrayWrapper.wrap(buf));
|
||||||
@ -145,10 +147,12 @@ class Metadata {
|
|||||||
long recordHeaderPointer = Metadata.getDataPointer(position);
|
long recordHeaderPointer = Metadata.getDataPointer(position);
|
||||||
lock = file.getChannel()
|
lock = file.getChannel()
|
||||||
.lock(origHeaderKeyPointer, KEY_SIZE, true);
|
.lock(origHeaderKeyPointer, KEY_SIZE, true);
|
||||||
|
|
||||||
file.seek(recordHeaderPointer);
|
file.seek(recordHeaderPointer);
|
||||||
r.dataPointer = file.readLong();
|
r.dataPointer = file.readLong();
|
||||||
r.dataCapacity = file.readInt();
|
r.dataCapacity = file.readInt();
|
||||||
r.dataCount = file.readInt();
|
r.dataCount = file.readInt();
|
||||||
|
|
||||||
lock.release();
|
lock.release();
|
||||||
|
|
||||||
if (r.dataPointer == 0L || r.dataCapacity == 0L || r.dataCount == 0L) {
|
if (r.dataPointer == 0L || r.dataCapacity == 0L || r.dataCount == 0L) {
|
||||||
@ -173,17 +177,19 @@ class Metadata {
|
|||||||
|
|
||||||
FileLock lock = file.getChannel()
|
FileLock lock = file.getChannel()
|
||||||
.lock(recordHeaderPointer, POINTER_INFO_SIZE, false);
|
.lock(recordHeaderPointer, POINTER_INFO_SIZE, false);
|
||||||
|
|
||||||
file.seek(recordHeaderPointer);
|
file.seek(recordHeaderPointer);
|
||||||
file.writeLong(this.dataPointer);
|
file.writeLong(this.dataPointer);
|
||||||
file.writeInt(this.dataCapacity);
|
file.writeInt(this.dataCapacity);
|
||||||
file.writeInt(this.dataCount);
|
file.writeInt(this.dataCount);
|
||||||
|
|
||||||
lock.release();
|
lock.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move a record to the new INDEX
|
* Move a record to the new INDEX
|
||||||
*/
|
*/
|
||||||
void move(RandomAccessFile file, int newIndex) throws IOException {
|
void moveRecord(RandomAccessFile file, int newIndex) throws IOException {
|
||||||
byte[] buf = new byte[KEY_SIZE];
|
byte[] buf = new byte[KEY_SIZE];
|
||||||
|
|
||||||
long origHeaderKeyPointer = Metadata.getMetaDataPointer(this.indexPosition);
|
long origHeaderKeyPointer = Metadata.getMetaDataPointer(this.indexPosition);
|
||||||
@ -192,6 +198,7 @@ class Metadata {
|
|||||||
|
|
||||||
file.seek(origHeaderKeyPointer);
|
file.seek(origHeaderKeyPointer);
|
||||||
file.readFully(buf);
|
file.readFully(buf);
|
||||||
|
|
||||||
lock.release();
|
lock.release();
|
||||||
|
|
||||||
long newHeaderKeyPointer = Metadata.getMetaDataPointer(newIndex);
|
long newHeaderKeyPointer = Metadata.getMetaDataPointer(newIndex);
|
||||||
@ -200,6 +207,7 @@ class Metadata {
|
|||||||
|
|
||||||
file.seek(newHeaderKeyPointer);
|
file.seek(newHeaderKeyPointer);
|
||||||
file.write(buf);
|
file.write(buf);
|
||||||
|
|
||||||
lock.release();
|
lock.release();
|
||||||
|
|
||||||
// System.err.println("updating ptr: " + this.indexPosition + " -> " + newIndex + " @ " + newHeaderKeyPointer + "-" + (newHeaderKeyPointer+INDEX_ENTRY_LENGTH));
|
// System.err.println("updating ptr: " + this.indexPosition + " -> " + newIndex + " @ " + newHeaderKeyPointer + "-" + (newHeaderKeyPointer+INDEX_ENTRY_LENGTH));
|
||||||
@ -230,6 +238,7 @@ class Metadata {
|
|||||||
// save the data
|
// save the data
|
||||||
file.seek(position);
|
file.seek(position);
|
||||||
file.write(data);
|
file.write(data);
|
||||||
|
|
||||||
lock.release();
|
lock.release();
|
||||||
|
|
||||||
// update header pointer info
|
// update header pointer info
|
||||||
@ -249,6 +258,7 @@ class Metadata {
|
|||||||
.lock(this.dataPointer, this.dataCount, true);
|
.lock(this.dataPointer, this.dataCount, true);
|
||||||
file.seek(this.dataPointer);
|
file.seek(this.dataPointer);
|
||||||
file.readFully(buf);
|
file.readFully(buf);
|
||||||
|
|
||||||
lock.release();
|
lock.release();
|
||||||
// Sys.printArray(buf, buf.length, false, 0);
|
// Sys.printArray(buf, buf.length, false, 0);
|
||||||
|
|
||||||
@ -260,30 +270,32 @@ class Metadata {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
static
|
static
|
||||||
<T> T readData(SerializationManager serializationManager, InflaterInputStream inputStream) throws IOException {
|
<T> T readData(final SerializationManager serializationManager, final Input input) throws IOException {
|
||||||
Input input = new Input(inputStream, 1024); // read 1024 at a time
|
// this is to reset the internal buffer of 'input'
|
||||||
|
input.setInputStream(input.getInputStream());
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
T readObject = (T) serializationManager.readFullClassAndObject(null, input);
|
T readObject = (T) serializationManager.readFullClassAndObject(null, input);
|
||||||
return readObject;
|
return readObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes data to the end of the file (which is where the datapointer is at)
|
* Writes data to the end of the file (which is where the datapointer is at). This must be locked/released in calling methods!
|
||||||
*/
|
*/
|
||||||
static
|
static
|
||||||
void writeDataFast(final SerializationManager serializationManager,
|
int writeData(final SerializationManager serializationManager,
|
||||||
final Object data,
|
final Object data,
|
||||||
final RandomAccessFile file,
|
final Output output) throws IOException {
|
||||||
final DeflaterOutputStream outputStream) throws IOException {
|
|
||||||
// HAVE TO LOCK BEFORE THIS IS CALLED! (AND FREE AFTERWARDS!)
|
output.clear();
|
||||||
Output output = new Output(outputStream, 1024); // write 1024 at a time
|
|
||||||
serializationManager.writeFullClassAndObject(null, output, data);
|
serializationManager.writeFullClassAndObject(null, output, data);
|
||||||
output.flush();
|
output.flush();
|
||||||
|
|
||||||
outputStream.flush(); // sync-flush is enabled, so the output stream will finish compressing data.
|
return (int) output.total();
|
||||||
}
|
}
|
||||||
|
|
||||||
void writeData(ByteArrayOutputStream byteArrayOutputStream, RandomAccessFile file) throws IOException {
|
void writeDataRaw(ByteArrayOutputStream byteArrayOutputStream, RandomAccessFile file) throws IOException {
|
||||||
this.dataCount = byteArrayOutputStream.size();
|
this.dataCount = byteArrayOutputStream.size();
|
||||||
|
|
||||||
FileLock lock = file.getChannel()
|
FileLock lock = file.getChannel()
|
||||||
@ -292,7 +304,8 @@ class Metadata {
|
|||||||
FileOutputStream out = new FileOutputStream(file.getFD());
|
FileOutputStream out = new FileOutputStream(file.getFD());
|
||||||
file.seek(this.dataPointer);
|
file.seek(this.dataPointer);
|
||||||
byteArrayOutputStream.writeTo(out);
|
byteArrayOutputStream.writeTo(out);
|
||||||
out.flush();
|
|
||||||
|
|
||||||
lock.release();
|
lock.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,28 +61,31 @@ interface Storage {
|
|||||||
* <p/>
|
* <p/>
|
||||||
* This will check to see if there is an associated key for that data, if not - it will use data as the default
|
* This will check to see if there is an associated key for that data, if not - it will use data as the default
|
||||||
*
|
*
|
||||||
* @param data The data that will hold the copy of the data from disk
|
* @param data This is the default value, and if there is no value with the key in the DB this default value will be saved.
|
||||||
*/
|
*/
|
||||||
<T> T getAndPut(T data) throws IOException;
|
<T> T getAndPut(T data) throws IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the saved data for the specified key.
|
* Returns the saved data for the specified key.
|
||||||
*
|
*
|
||||||
* @param data If there is no object in the DB with the specified key, this value will be the default (and will be saved to the db)
|
* @param key The key used to check if data already exists.
|
||||||
|
* @param data This is the default value, and if there is no value with the key in the DB this default value will be saved.
|
||||||
*/
|
*/
|
||||||
<T> T getAndPut(String key, T data) throws IOException;
|
<T> T getAndPut(String key, T data) throws IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the saved data for the specified key.
|
* Returns the saved data for the specified key.
|
||||||
*
|
*
|
||||||
* @param data If there is no object in the DB with the specified key, this value will be the default (and will be saved to the db)
|
* @param key The key used to check if data already exists.
|
||||||
|
* @param data This is the default value, and if there is no value with the key in the DB this default value will be saved.
|
||||||
*/
|
*/
|
||||||
<T> T getAndPut(byte[] key, T data) throws IOException;
|
<T> T getAndPut(byte[] key, T data) throws IOException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the saved data for the specified key.
|
* Returns the saved data for the specified key.
|
||||||
*
|
*
|
||||||
* @param data If there is no object in the DB with the specified key, this value will be the default (and will be saved to the db)
|
* @param key The key used to check if data already exists.
|
||||||
|
* @param data This is the default value, and if there is no value with the key in the DB this default value will be saved.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
<T> T getAndPut(ByteArrayWrapper key, T data) throws IOException;
|
<T> T getAndPut(ByteArrayWrapper key, T data) throws IOException;
|
||||||
|
@ -16,24 +16,29 @@
|
|||||||
package dorkbox.util.storage;
|
package dorkbox.util.storage;
|
||||||
|
|
||||||
import com.esotericsoftware.kryo.KryoException;
|
import com.esotericsoftware.kryo.KryoException;
|
||||||
|
import com.esotericsoftware.kryo.io.Input;
|
||||||
import com.esotericsoftware.kryo.io.Output;
|
import com.esotericsoftware.kryo.io.Output;
|
||||||
import dorkbox.util.SerializationManager;
|
import dorkbox.util.SerializationManager;
|
||||||
import dorkbox.util.bytes.ByteArrayWrapper;
|
import dorkbox.util.bytes.ByteArrayWrapper;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.RandomAccessFile;
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.nio.channels.Channels;
|
||||||
import java.nio.channels.FileLock;
|
import java.nio.channels.FileLock;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.zip.Deflater;
|
|
||||||
import java.util.zip.DeflaterOutputStream;
|
|
||||||
import java.util.zip.Inflater;
|
|
||||||
import java.util.zip.InflaterInputStream;
|
|
||||||
|
|
||||||
|
|
||||||
// a note on file locking between c and java
|
// a note on file locking between c and java
|
||||||
@ -42,7 +47,6 @@ import java.util.zip.InflaterInputStream;
|
|||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public
|
|
||||||
class StorageBase {
|
class StorageBase {
|
||||||
private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
|
private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
|
||||||
|
|
||||||
@ -70,8 +74,9 @@ class StorageBase {
|
|||||||
private final ReentrantLock referenceLock = new ReentrantLock();
|
private final ReentrantLock referenceLock = new ReentrantLock();
|
||||||
|
|
||||||
|
|
||||||
|
// file/raf that are used
|
||||||
private final File baseFile;
|
private final File baseFile;
|
||||||
private final RandomAccessFile file;
|
private final RandomAccessFile randomAccessFile;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -93,11 +98,12 @@ class StorageBase {
|
|||||||
// save references to these, so they don't have to be created/destroyed any time there is I/O
|
// save references to these, so they don't have to be created/destroyed any time there is I/O
|
||||||
private final SerializationManager serializationManager;
|
private final SerializationManager serializationManager;
|
||||||
|
|
||||||
private final Deflater deflater;
|
private final Output output;
|
||||||
private final DeflaterOutputStream outputStream;
|
private final Input input;
|
||||||
|
|
||||||
|
// input/output write buffer size before flushing to/from the file
|
||||||
|
public static final int BUFFER_SIZE = 1024;
|
||||||
|
|
||||||
private final Inflater inflater;
|
|
||||||
private final InflaterInputStream inputStream;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates or opens a new database file.
|
* Creates or opens a new database file.
|
||||||
@ -109,65 +115,75 @@ class StorageBase {
|
|||||||
|
|
||||||
this.baseFile = filePath;
|
this.baseFile = filePath;
|
||||||
|
|
||||||
File parentFile = this.baseFile.getParentFile();
|
boolean newStorage = !filePath.exists();
|
||||||
if (parentFile != null && !parentFile.exists()) {
|
|
||||||
if (!parentFile.mkdirs()) {
|
if (newStorage) {
|
||||||
throw new IOException("Unable to create dirs for: " + filePath);
|
File parentFile = this.baseFile.getParentFile();
|
||||||
|
if (parentFile != null && !parentFile.exists()) {
|
||||||
|
if (!parentFile.mkdirs()) {
|
||||||
|
throw new IOException("Unable to create dirs for: " + filePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.file = new RandomAccessFile(this.baseFile, "rw");
|
this.randomAccessFile = new RandomAccessFile(this.baseFile, "rw");
|
||||||
|
|
||||||
if (this.file.length() > FILE_HEADERS_REGION_LENGTH) {
|
|
||||||
this.file.seek(VERSION_HEADER_LOCATION);
|
if (newStorage || this.randomAccessFile.length() <= FILE_HEADERS_REGION_LENGTH) {
|
||||||
this.databaseVersion = this.file.readInt();
|
setVersion(this.randomAccessFile, 0);
|
||||||
this.numberOfRecords = this.file.readInt();
|
setRecordCount(this.randomAccessFile, 0);
|
||||||
this.dataPosition = this.file.readLong();
|
|
||||||
|
// pad the metadata with 21 records, so there is about 1k of padding before the data starts
|
||||||
|
long indexPointer = Metadata.getMetaDataPointer(21);
|
||||||
|
setDataStartPosition(indexPointer);
|
||||||
|
// have to make sure we can read header info (even if it's blank)
|
||||||
|
this.randomAccessFile.setLength(indexPointer);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
setVersionNumber(this.file, 0);
|
this.randomAccessFile.seek(VERSION_HEADER_LOCATION);
|
||||||
|
this.databaseVersion = this.randomAccessFile.readInt();
|
||||||
|
this.numberOfRecords = this.randomAccessFile.readInt();
|
||||||
|
this.dataPosition = this.randomAccessFile.readLong();
|
||||||
|
|
||||||
// always start off with 4 records
|
if (this.randomAccessFile.length() < this.dataPosition) {
|
||||||
setRecordCount(this.file, 4);
|
this.logger.error("Corrupted storage file!");
|
||||||
|
throw new IllegalArgumentException("Unable to parse header information from storage. Maybe it's corrupted?");
|
||||||
long indexPointer = Metadata.getMetaDataPointer(4);
|
}
|
||||||
setDataPosition(this.file, indexPointer);
|
|
||||||
// have to make sure we can read header info (even if it's blank)
|
|
||||||
this.file.setLength(indexPointer);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.file.length() < this.dataPosition) {
|
|
||||||
this.logger.error("Corrupted storage file!");
|
|
||||||
throw new IllegalArgumentException("Unable to parse header information from storage. Maybe it's corrupted?");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//noinspection AutoBoxing
|
//noinspection AutoBoxing
|
||||||
this.logger.info("Storage version: {}", this.databaseVersion);
|
this.logger.info("Storage version: {}", this.databaseVersion);
|
||||||
|
|
||||||
this.deflater = new Deflater(7, true);
|
|
||||||
FileOutputStream fileOutputStream = new FileOutputStream(this.file.getFD());
|
|
||||||
this.outputStream = new DeflaterOutputStream(fileOutputStream, this.deflater, 65536);
|
|
||||||
|
|
||||||
this.inflater = new Inflater(true);
|
// If we want to use compression (no need really, since this file is small already),
|
||||||
this.inputStream = new InflaterInputStream(new FileInputStream(this.file.getFD()), this.inflater, 65536);
|
// then we have to make sure it's sync'd on flush AND have actually call outputStream.flush().
|
||||||
|
final InputStream inputStream = Channels.newInputStream(randomAccessFile.getChannel());
|
||||||
|
final OutputStream outputStream = Channels.newOutputStream(randomAccessFile.getChannel());
|
||||||
|
|
||||||
|
// read/write 1024 bytes at a time
|
||||||
|
output = new Output(outputStream, BUFFER_SIZE);
|
||||||
|
input = new Input(inputStream, BUFFER_SIZE);
|
||||||
|
|
||||||
|
|
||||||
//noinspection AutoBoxing
|
//noinspection AutoBoxing
|
||||||
this.weight = 0.5F;
|
this.weight = 0.5F;
|
||||||
this.memoryIndex = new ConcurrentHashMap<ByteArrayWrapper, Metadata>(this.numberOfRecords);
|
this.memoryIndex = new ConcurrentHashMap<ByteArrayWrapper, Metadata>(this.numberOfRecords);
|
||||||
|
|
||||||
Metadata meta;
|
if (!newStorage) {
|
||||||
for (int index = 0; index < this.numberOfRecords; index++) {
|
Metadata meta;
|
||||||
meta = Metadata.readHeader(this.file, index);
|
for (int index = 0; index < this.numberOfRecords; index++) {
|
||||||
if (meta == null) {
|
meta = Metadata.readHeader(this.randomAccessFile, index);
|
||||||
// because we guarantee that empty metadata are AWLAYS at the end of the section, if we get a null one, break!
|
if (meta == null) {
|
||||||
break;
|
// because we guarantee that empty metadata are ALWAYS at the end of the section, if we get a null one, break!
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.memoryIndex.put(meta.key, meta);
|
||||||
}
|
}
|
||||||
this.memoryIndex.put(meta.key, meta);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.memoryIndex.size() != this.numberOfRecords) {
|
if (this.memoryIndex.size() != this.numberOfRecords) {
|
||||||
setRecordCount(this.file, this.memoryIndex.size());
|
setRecordCount(this.randomAccessFile, this.memoryIndex.size());
|
||||||
this.logger.warn("Mismatch record count in storage, auto-correcting size.");
|
this.logger.warn("Mismatch record count in storage, auto-correcting size.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,11 +271,12 @@ class StorageBase {
|
|||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// else, we have to load it from disk
|
// System.err.println("--Reading data from: " + meta.dataPointer);
|
||||||
this.inflater.reset();
|
|
||||||
this.file.seek(meta.dataPointer);
|
|
||||||
|
|
||||||
T readRecordData = Metadata.readData(this.serializationManager, this.inputStream);
|
// else, we have to load it from disk
|
||||||
|
this.randomAccessFile.seek(meta.dataPointer);
|
||||||
|
|
||||||
|
T readRecordData = Metadata.readData(this.serializationManager, this.input);
|
||||||
|
|
||||||
if (readRecordData != null) {
|
if (readRecordData != null) {
|
||||||
// now stuff it into our reference cache for future lookups!
|
// now stuff it into our reference cache for future lookups!
|
||||||
@ -310,13 +327,16 @@ class StorageBase {
|
|||||||
void close() {
|
void close() {
|
||||||
// pending ops flushed (protected by lock)
|
// pending ops flushed (protected by lock)
|
||||||
// not protected by lock
|
// not protected by lock
|
||||||
|
|
||||||
|
this.logger.info("Closing storage file: '{}'", this.baseFile.getAbsolutePath());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.file.getFD()
|
this.randomAccessFile.getFD()
|
||||||
.sync();
|
.sync();
|
||||||
this.file.close();
|
this.input.close();
|
||||||
|
this.randomAccessFile.close();
|
||||||
this.memoryIndex.clear();
|
this.memoryIndex.clear();
|
||||||
|
|
||||||
this.inputStream.close();
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
this.logger.error("Error while closing the file", e);
|
this.logger.error("Error while closing the file", e);
|
||||||
}
|
}
|
||||||
@ -330,7 +350,7 @@ class StorageBase {
|
|||||||
long getFileSize() {
|
long getFileSize() {
|
||||||
// protected by actionLock
|
// protected by actionLock
|
||||||
try {
|
try {
|
||||||
return this.file.length();
|
return this.randomAccessFile.length();
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
this.logger.error("Error getting file size for {}", this.baseFile.getAbsolutePath(), e);
|
this.logger.error("Error getting file size for {}", this.baseFile.getAbsolutePath(), e);
|
||||||
return -1L;
|
return -1L;
|
||||||
@ -355,9 +375,7 @@ class StorageBase {
|
|||||||
* Will also save the object in a cache.
|
* Will also save the object in a cache.
|
||||||
*/
|
*/
|
||||||
private
|
private
|
||||||
void save0(ByteArrayWrapper key, Object object, DeflaterOutputStream fileOutputStream, Deflater deflater) {
|
void save0(ByteArrayWrapper key, Object object) {
|
||||||
deflater.reset();
|
|
||||||
|
|
||||||
Metadata metaData = this.memoryIndex.get(key);
|
Metadata metaData = this.memoryIndex.get(key);
|
||||||
int currentRecordCount = this.numberOfRecords;
|
int currentRecordCount = this.numberOfRecords;
|
||||||
|
|
||||||
@ -367,29 +385,31 @@ class StorageBase {
|
|||||||
if (currentRecordCount == 1) {
|
if (currentRecordCount == 1) {
|
||||||
// if we are the ONLY one, then we can do things differently.
|
// if we are the ONLY one, then we can do things differently.
|
||||||
// just dump the data again to disk.
|
// just dump the data again to disk.
|
||||||
FileLock lock = this.file.getChannel()
|
FileLock lock = this.randomAccessFile.getChannel()
|
||||||
.lock(this.dataPosition,
|
.lock(this.dataPosition,
|
||||||
Long.MAX_VALUE - this.dataPosition,
|
Long.MAX_VALUE - this.dataPosition,
|
||||||
false); // don't know how big it is, so max value it
|
false); // don't know how big it is, so max value it
|
||||||
this.file.seek(this.dataPosition); // this is the end of the file, we know this ahead-of-time
|
|
||||||
Metadata.writeDataFast(this.serializationManager, object, file, fileOutputStream);
|
this.randomAccessFile.seek(this.dataPosition); // this is the end of the file, we know this ahead-of-time
|
||||||
|
Metadata.writeData(this.serializationManager, object, this.output);
|
||||||
// have to re-specify the capacity and size
|
// have to re-specify the capacity and size
|
||||||
//noinspection NumericCastThatLosesPrecision
|
//noinspection NumericCastThatLosesPrecision
|
||||||
int sizeOfWrittenData = (int) (this.file.length() - this.dataPosition);
|
int sizeOfWrittenData = (int) (this.randomAccessFile.length() - this.dataPosition);
|
||||||
|
|
||||||
metaData.dataCapacity = sizeOfWrittenData;
|
metaData.dataCapacity = sizeOfWrittenData;
|
||||||
metaData.dataCount = sizeOfWrittenData;
|
metaData.dataCount = sizeOfWrittenData;
|
||||||
|
|
||||||
lock.release();
|
lock.release();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// this is comparatively slow, since we serialize it first to get the size, then we put it in the file.
|
// this is comparatively slow, since we serialize it first to get the size, then we put it in the file.
|
||||||
ByteArrayOutputStream dataStream = getDataAsByteArray(this.serializationManager, this.logger, object, deflater);
|
ByteArrayOutputStream dataStream = getDataAsByteArray(this.serializationManager, this.logger, object);
|
||||||
|
|
||||||
int size = dataStream.size();
|
int size = dataStream.size();
|
||||||
if (size > metaData.dataCapacity) {
|
if (size > metaData.dataCapacity) {
|
||||||
deleteRecordData(metaData, size);
|
deleteRecordData(metaData, size);
|
||||||
// stuff this record to the end of the file, since it won't fit in it's current location
|
// stuff this record to the end of the file, since it won't fit in it's current location
|
||||||
metaData.dataPointer = this.file.length();
|
metaData.dataPointer = this.randomAccessFile.length();
|
||||||
// have to make sure that the CAPACITY of the new one is the SIZE of the new data!
|
// have to make sure that the CAPACITY of the new one is the SIZE of the new data!
|
||||||
// and since it is going to the END of the file, we do that.
|
// and since it is going to the END of the file, we do that.
|
||||||
metaData.dataCapacity = size;
|
metaData.dataCapacity = size;
|
||||||
@ -398,45 +418,51 @@ class StorageBase {
|
|||||||
|
|
||||||
// TODO: should check to see if the data is different. IF SO, then we write, otherwise nothing!
|
// TODO: should check to see if the data is different. IF SO, then we write, otherwise nothing!
|
||||||
|
|
||||||
metaData.writeData(dataStream, this.file);
|
metaData.writeDataRaw(dataStream, this.randomAccessFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
metaData.writeDataInfo(this.file);
|
metaData.writeDataInfo(this.randomAccessFile);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
this.logger.error("Error while saving data to disk", e);
|
this.logger.error("Error while saving data to disk", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
// metadata == null...
|
||||||
try {
|
try {
|
||||||
// try to move the read head in order
|
// set the number of records that this storage has
|
||||||
setRecordCount(this.file, currentRecordCount + 1);
|
setRecordCount(this.randomAccessFile, currentRecordCount + 1);
|
||||||
|
|
||||||
// This will make sure that there is room to write a new record. This is zero indexed.
|
// This will make sure that there is room to write a new record. This is zero indexed.
|
||||||
// this will skip around if moves occur
|
// this will skip around if moves occur
|
||||||
ensureIndexCapacity(this.file);
|
ensureIndexCapacity(this.randomAccessFile);
|
||||||
|
|
||||||
// append record to end of file
|
// append record to end of file
|
||||||
long length = this.file.length();
|
long length = this.randomAccessFile.length();
|
||||||
|
|
||||||
|
// System.err.println("--Writing data to: " + length);
|
||||||
|
|
||||||
metaData = new Metadata(key, currentRecordCount, length);
|
metaData = new Metadata(key, currentRecordCount, length);
|
||||||
metaData.writeMetaDataInfo(this.file);
|
metaData.writeMetaDataInfo(this.randomAccessFile);
|
||||||
|
|
||||||
// add new entry to the index
|
// add new entry to the index
|
||||||
this.memoryIndex.put(key, metaData);
|
this.memoryIndex.put(key, metaData);
|
||||||
setRecordCount(this.file, currentRecordCount + 1);
|
|
||||||
|
|
||||||
// save out the data. Because we KNOW that we are writing this to the end of the file,
|
// save out the data. Because we KNOW that we are writing this to the end of the file,
|
||||||
// there are some tricks we can use.
|
// there are some tricks we can use.
|
||||||
|
|
||||||
FileLock lock = this.file.getChannel()
|
// don't know how big it is, so max value it
|
||||||
.lock(length, Long.MAX_VALUE - length, false); // don't know how big it is, so max value it
|
FileLock lock = this.randomAccessFile.getChannel()
|
||||||
this.file.seek(length); // this is the end of the file, we know this ahead-of-time
|
.lock(0, Long.MAX_VALUE, false);
|
||||||
Metadata.writeDataFast(this.serializationManager, object, file, fileOutputStream);
|
|
||||||
|
// this is the end of the file, we know this ahead-of-time
|
||||||
|
this.randomAccessFile.seek(length);
|
||||||
|
|
||||||
|
int total = Metadata.writeData(this.serializationManager, object, this.output);
|
||||||
lock.release();
|
lock.release();
|
||||||
|
|
||||||
metaData.dataCount = deflater.getTotalOut();
|
metaData.dataCount = metaData.dataCapacity = total;
|
||||||
metaData.dataCapacity = metaData.dataCount;
|
|
||||||
// have to save it.
|
// have to save it.
|
||||||
metaData.writeDataInfo(this.file);
|
metaData.writeDataInfo(this.randomAccessFile);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
this.logger.error("Error while writing data to disk", e);
|
this.logger.error("Error while writing data to disk", e);
|
||||||
return;
|
return;
|
||||||
@ -449,32 +475,31 @@ class StorageBase {
|
|||||||
|
|
||||||
|
|
||||||
private static
|
private static
|
||||||
ByteArrayOutputStream getDataAsByteArray(SerializationManager serializationManager, Logger logger, Object data, Deflater deflater) throws IOException {
|
ByteArrayOutputStream getDataAsByteArray(SerializationManager serializationManager, Logger logger, Object data) throws IOException {
|
||||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
OutputStream outputStream = new DeflaterOutputStream(byteArrayOutputStream, deflater);
|
|
||||||
Output output = new Output(outputStream, 1024); // write 1024 at a time
|
Output output = new Output(outputStream, 1024); // write 1024 at a time
|
||||||
|
|
||||||
serializationManager.writeFullClassAndObject(logger, output, data);
|
serializationManager.writeFullClassAndObject(logger, output, data);
|
||||||
output.flush();
|
output.flush();
|
||||||
|
|
||||||
outputStream.flush();
|
outputStream.flush();
|
||||||
outputStream.close();
|
outputStream.close();
|
||||||
|
|
||||||
return byteArrayOutputStream;
|
return outputStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
void doActionThings(Map<ByteArrayWrapper, Object> actions) {
|
void doActionThings(Map<ByteArrayWrapper, Object> actions) {
|
||||||
DeflaterOutputStream outputStream2 = this.outputStream;
|
|
||||||
Deflater deflater2 = this.deflater;
|
|
||||||
|
|
||||||
// actions is thrown away after this invocation. GC can pick it up.
|
// actions is thrown away after this invocation. GC can pick it up.
|
||||||
// we are only interested in the LAST action that happened for some data.
|
// we are only interested in the LAST action that happened for some data.
|
||||||
// items to be "autosaved" are automatically injected into "actions".
|
// items to be "autosaved" are automatically injected into "actions".
|
||||||
for (Entry<ByteArrayWrapper, Object> entry : actions.entrySet()) {
|
final Set<Entry<ByteArrayWrapper, Object>> entries = actions.entrySet();
|
||||||
|
for (Entry<ByteArrayWrapper, Object> entry : entries) {
|
||||||
Object object = entry.getValue();
|
Object object = entry.getValue();
|
||||||
ByteArrayWrapper key = entry.getKey();
|
ByteArrayWrapper key = entry.getKey();
|
||||||
|
|
||||||
// our action list is for explicitly saving objects (but not necessarily "registering" them to be auto-saved
|
// our action list is for explicitly saving objects (but not necessarily "registering" them to be auto-saved
|
||||||
save0(key, object, outputStream2, deflater2);
|
save0(key, object);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -501,11 +526,11 @@ class StorageBase {
|
|||||||
|
|
||||||
private
|
private
|
||||||
void deleteRecordData(Metadata deletedRecord, int sizeOfDataToAdd) throws IOException {
|
void deleteRecordData(Metadata deletedRecord, int sizeOfDataToAdd) throws IOException {
|
||||||
if (this.file.length() == deletedRecord.dataPointer + deletedRecord.dataCapacity) {
|
if (this.randomAccessFile.length() == deletedRecord.dataPointer + deletedRecord.dataCapacity) {
|
||||||
// shrink file since this is the last record in the file
|
// shrink file since this is the last record in the file
|
||||||
FileLock lock = this.file.getChannel()
|
FileLock lock = this.randomAccessFile.getChannel()
|
||||||
.lock(deletedRecord.dataPointer, Long.MAX_VALUE - deletedRecord.dataPointer, false);
|
.lock(deletedRecord.dataPointer, Long.MAX_VALUE - deletedRecord.dataPointer, false);
|
||||||
this.file.setLength(deletedRecord.dataPointer);
|
this.randomAccessFile.setLength(deletedRecord.dataPointer);
|
||||||
lock.release();
|
lock.release();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -527,11 +552,11 @@ class StorageBase {
|
|||||||
|
|
||||||
if (endIndexPointer < this.dataPosition && endIndexPointer <= newEndOfDataPointer) {
|
if (endIndexPointer < this.dataPosition && endIndexPointer <= newEndOfDataPointer) {
|
||||||
// one option is to shrink the RECORD section to fit the new data
|
// one option is to shrink the RECORD section to fit the new data
|
||||||
setDataPosition(this.file, newEndOfDataPointer);
|
setDataStartPosition(newEndOfDataPointer);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// option two is to grow the RECORD section, and put the data at the end of the file
|
// option two is to grow the RECORD section, and put the data at the end of the file
|
||||||
setDataPosition(this.file, endOfDataPointer);
|
setDataStartPosition(endOfDataPointer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -539,7 +564,7 @@ class StorageBase {
|
|||||||
if (previous != null) {
|
if (previous != null) {
|
||||||
// append space of deleted record onto previous record
|
// append space of deleted record onto previous record
|
||||||
previous.dataCapacity += deletedRecord.dataCapacity;
|
previous.dataCapacity += deletedRecord.dataCapacity;
|
||||||
previous.writeDataInfo(this.file);
|
previous.writeDataInfo(this.randomAccessFile);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// because there is no "previous", that means we MIGHT be the FIRST record
|
// because there is no "previous", that means we MIGHT be the FIRST record
|
||||||
@ -556,15 +581,15 @@ class StorageBase {
|
|||||||
int currentNumRecords = this.memoryIndex.size();
|
int currentNumRecords = this.memoryIndex.size();
|
||||||
|
|
||||||
if (deleteRecord.indexPosition != currentNumRecords - 1) {
|
if (deleteRecord.indexPosition != currentNumRecords - 1) {
|
||||||
Metadata last = Metadata.readHeader(this.file, currentNumRecords - 1);
|
Metadata last = Metadata.readHeader(this.randomAccessFile, currentNumRecords - 1);
|
||||||
assert last != null;
|
assert last != null;
|
||||||
|
|
||||||
last.move(this.file, deleteRecord.indexPosition);
|
last.moveRecord(this.randomAccessFile, deleteRecord.indexPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.memoryIndex.remove(key);
|
this.memoryIndex.remove(key);
|
||||||
|
|
||||||
setRecordCount(this.file, currentNumRecords - 1);
|
setRecordCount(this.randomAccessFile, currentNumRecords - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -572,13 +597,14 @@ class StorageBase {
|
|||||||
* Writes the number of records header to the file.
|
* Writes the number of records header to the file.
|
||||||
*/
|
*/
|
||||||
private
|
private
|
||||||
void setVersionNumber(RandomAccessFile file, int versionNumber) throws IOException {
|
void setVersion(RandomAccessFile file, int versionNumber) throws IOException {
|
||||||
this.databaseVersion = versionNumber;
|
this.databaseVersion = versionNumber;
|
||||||
|
|
||||||
FileLock lock = this.file.getChannel()
|
FileLock lock = this.randomAccessFile.getChannel()
|
||||||
.lock(VERSION_HEADER_LOCATION, 4, false);
|
.lock(VERSION_HEADER_LOCATION, 4, false);
|
||||||
file.seek(VERSION_HEADER_LOCATION);
|
file.seek(VERSION_HEADER_LOCATION);
|
||||||
file.writeInt(versionNumber);
|
file.writeInt(versionNumber);
|
||||||
|
|
||||||
lock.release();
|
lock.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -587,26 +613,34 @@ class StorageBase {
|
|||||||
*/
|
*/
|
||||||
private
|
private
|
||||||
void setRecordCount(RandomAccessFile file, int numberOfRecords) throws IOException {
|
void setRecordCount(RandomAccessFile file, int numberOfRecords) throws IOException {
|
||||||
this.numberOfRecords = numberOfRecords;
|
if (this.numberOfRecords != numberOfRecords) {
|
||||||
|
this.numberOfRecords = numberOfRecords;
|
||||||
|
|
||||||
FileLock lock = this.file.getChannel()
|
// System.err.println("Set recordCount: " + numberOfRecords);
|
||||||
.lock(NUM_RECORDS_HEADER_LOCATION, 4, false);
|
|
||||||
file.seek(NUM_RECORDS_HEADER_LOCATION);
|
FileLock lock = this.randomAccessFile.getChannel()
|
||||||
file.writeInt(numberOfRecords);
|
.lock(NUM_RECORDS_HEADER_LOCATION, 4, false);
|
||||||
lock.release();
|
file.seek(NUM_RECORDS_HEADER_LOCATION);
|
||||||
|
file.writeInt(numberOfRecords);
|
||||||
|
|
||||||
|
lock.release();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Writes the data start position to the file.
|
* Writes the data start position to the file.
|
||||||
*/
|
*/
|
||||||
private
|
private
|
||||||
void setDataPosition(RandomAccessFile file, long dataPositionPointer) throws IOException {
|
void setDataStartPosition(long dataPositionPointer) throws IOException {
|
||||||
this.dataPosition = dataPositionPointer;
|
FileLock lock = this.randomAccessFile.getChannel()
|
||||||
|
.lock(DATA_START_HEADER_LOCATION, 8, false);
|
||||||
|
|
||||||
|
// System.err.println("Setting data position: " + dataPositionPointer);
|
||||||
|
dataPosition = dataPositionPointer;
|
||||||
|
|
||||||
|
randomAccessFile.seek(DATA_START_HEADER_LOCATION);
|
||||||
|
randomAccessFile.writeLong(dataPositionPointer);
|
||||||
|
|
||||||
FileLock lock = this.file.getChannel()
|
|
||||||
.lock(DATA_START_HEADER_LOCATION, 8, false);
|
|
||||||
file.seek(DATA_START_HEADER_LOCATION);
|
|
||||||
file.writeLong(dataPositionPointer);
|
|
||||||
lock.release();
|
lock.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -616,7 +650,7 @@ class StorageBase {
|
|||||||
|
|
||||||
void setVersion(int versionNumber) {
|
void setVersion(int versionNumber) {
|
||||||
try {
|
try {
|
||||||
setVersionNumber(this.file, versionNumber);
|
setVersion(this.randomAccessFile, versionNumber);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
this.logger.error("Unable to set the version number", e);
|
this.logger.error("Unable to set the version number", e);
|
||||||
}
|
}
|
||||||
@ -650,14 +684,16 @@ class StorageBase {
|
|||||||
*/
|
*/
|
||||||
private
|
private
|
||||||
void ensureIndexCapacity(RandomAccessFile file) throws IOException {
|
void ensureIndexCapacity(RandomAccessFile file) throws IOException {
|
||||||
int numberOfRecords = this.numberOfRecords; // because we are zero indexed, this is ALSO the index where the record will START
|
// because we are zero indexed, this is ALSO the index where the record will START
|
||||||
long endIndexPointer = Metadata.getMetaDataPointer(
|
int numberOfRecords = this.numberOfRecords;
|
||||||
numberOfRecords + 1); // +1 because this is where that index will END (the start of the NEXT one)
|
|
||||||
|
// +1 because this is where that index will END (the start of the NEXT one)
|
||||||
|
long endIndexPointer = Metadata.getMetaDataPointer(numberOfRecords + 1);
|
||||||
|
|
||||||
// just set the data position to the end of the file, since we don't have any data yet.
|
// just set the data position to the end of the file, since we don't have any data yet.
|
||||||
if (endIndexPointer > file.length() && numberOfRecords == 0) {
|
if (endIndexPointer > file.length() && numberOfRecords == 0) {
|
||||||
file.setLength(endIndexPointer);
|
file.setLength(endIndexPointer);
|
||||||
setDataPosition(file, endIndexPointer);
|
setDataStartPosition(endIndexPointer);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -675,13 +711,21 @@ class StorageBase {
|
|||||||
int newNumberOfRecords = getWeightedNewRecordCount(numberOfRecords);
|
int newNumberOfRecords = getWeightedNewRecordCount(numberOfRecords);
|
||||||
endIndexPointer = Metadata.getMetaDataPointer(newNumberOfRecords);
|
endIndexPointer = Metadata.getMetaDataPointer(newNumberOfRecords);
|
||||||
|
|
||||||
|
|
||||||
// sometimes the endIndexPointer is in the middle of data, so we cannot move a record to where
|
// sometimes the endIndexPointer is in the middle of data, so we cannot move a record to where
|
||||||
// data already exists, we have to move it to the end. Since we GUARANTEE that there is never "free space" at the
|
// data already exists, we have to move it to the end. Since we GUARANTEE that there is never "free space" at the
|
||||||
// end of a file, this is ok
|
// end of a file, this is ok
|
||||||
endIndexPointer = Math.max(endIndexPointer, file.length());
|
if (endIndexPointer > file.length()) {
|
||||||
|
// make sure we adjust the file size
|
||||||
|
file.setLength(endIndexPointer);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
endIndexPointer = file.length();
|
||||||
|
}
|
||||||
|
|
||||||
// we know that the start of the NEW data position has to be here.
|
// we know that the start of the NEW data position has to be here.
|
||||||
setDataPosition(file, endIndexPointer);
|
setDataStartPosition(endIndexPointer);
|
||||||
|
|
||||||
|
|
||||||
long writeDataPosition = endIndexPointer;
|
long writeDataPosition = endIndexPointer;
|
||||||
|
|
||||||
|
@ -32,6 +32,21 @@ class Store {
|
|||||||
@SuppressWarnings("SpellCheckingInspection")
|
@SuppressWarnings("SpellCheckingInspection")
|
||||||
private static final Map<File, Storage> storages = new HashMap<File, Storage>(1);
|
private static final Map<File, Storage> storages = new HashMap<File, Storage>(1);
|
||||||
|
|
||||||
|
// Make sure that the timer is run on shutdown. A HARD shutdown will just POW! kill it, a "nice" shutdown will run the hook
|
||||||
|
private static Thread shutdownHook = new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public
|
||||||
|
void run() {
|
||||||
|
Store.shutdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
static {
|
||||||
|
// add a shutdown hook to make sure that we properly flush/shutdown storage.
|
||||||
|
Runtime.getRuntime()
|
||||||
|
.addShutdownHook(shutdownHook);
|
||||||
|
}
|
||||||
|
|
||||||
public static
|
public static
|
||||||
DiskMaker Disk() {
|
DiskMaker Disk() {
|
||||||
return new DiskMaker();
|
return new DiskMaker();
|
||||||
|
@ -18,6 +18,13 @@ import java.util.Arrays;
|
|||||||
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
|
||||||
public
|
public
|
||||||
class StorageTest {
|
class StorageTest {
|
||||||
|
static int total = 10;
|
||||||
|
// the initial size is specified during disk.storage construction, and is based on the number of padded records.
|
||||||
|
private static final long initialSize = 1024L;
|
||||||
|
|
||||||
|
// this is the size for each record (determined by looking at the output when writing the file)
|
||||||
|
private static final int sizePerRecord = 23;
|
||||||
|
|
||||||
|
|
||||||
private static final File TEST_DB = new File("sampleFile.records");
|
private static final File TEST_DB = new File("sampleFile.records");
|
||||||
private static final SerializationManager manager = new SerializationManager() {
|
private static final SerializationManager manager = new SerializationManager() {
|
||||||
@ -126,7 +133,7 @@ class StorageTest {
|
|||||||
long size1 = storage.getFileSize();
|
long size1 = storage.getFileSize();
|
||||||
|
|
||||||
Assert.assertEquals("count is not correct", numberOfRecords1, 0);
|
Assert.assertEquals("count is not correct", numberOfRecords1, 0);
|
||||||
Assert.assertEquals("size is not correct", size1, 208L); // NOTE this will change based on the data size added!
|
Assert.assertEquals("size is not correct", size1, initialSize);
|
||||||
|
|
||||||
Store.close(storage);
|
Store.close(storage);
|
||||||
|
|
||||||
@ -134,6 +141,7 @@ class StorageTest {
|
|||||||
.file(TEST_DB)
|
.file(TEST_DB)
|
||||||
.serializer(manager)
|
.serializer(manager)
|
||||||
.make();
|
.make();
|
||||||
|
|
||||||
int numberOfRecords2 = storage.size();
|
int numberOfRecords2 = storage.size();
|
||||||
long size2 = storage.getFileSize();
|
long size2 = storage.getFileSize();
|
||||||
|
|
||||||
@ -147,13 +155,12 @@ class StorageTest {
|
|||||||
@Test
|
@Test
|
||||||
public
|
public
|
||||||
void testAddAsOne() throws IOException, ClassNotFoundException {
|
void testAddAsOne() throws IOException, ClassNotFoundException {
|
||||||
int total = 100;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Storage storage = Store.Disk()
|
Storage storage = Store.Disk()
|
||||||
.file(TEST_DB)
|
.file(TEST_DB)
|
||||||
.serializer(manager)
|
.serializer(manager)
|
||||||
.make();
|
.make();
|
||||||
|
|
||||||
for (int i = 0; i < total; i++) {
|
for (int i = 0; i < total; i++) {
|
||||||
add(storage, i);
|
add(storage, i);
|
||||||
}
|
}
|
||||||
@ -163,6 +170,7 @@ class StorageTest {
|
|||||||
.file(TEST_DB)
|
.file(TEST_DB)
|
||||||
.serializer(manager)
|
.serializer(manager)
|
||||||
.make();
|
.make();
|
||||||
|
|
||||||
for (int i = 0; i < total; i++) {
|
for (int i = 0; i < total; i++) {
|
||||||
String record1Data = createData(i);
|
String record1Data = createData(i);
|
||||||
String readRecord = readRecord(storage, i);
|
String readRecord = readRecord(storage, i);
|
||||||
@ -177,16 +185,20 @@ class StorageTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds data to storage using the SAME key each time (so each entry is overwritten).
|
||||||
|
* @throws IOException
|
||||||
|
* @throws ClassNotFoundException
|
||||||
|
*/
|
||||||
@Test
|
@Test
|
||||||
public
|
public
|
||||||
void testAddNoKeyRecords() throws IOException, ClassNotFoundException {
|
void testAddNoKeyRecords() throws IOException, ClassNotFoundException {
|
||||||
int total = 100;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Storage storage = Store.Disk()
|
Storage storage = Store.Disk()
|
||||||
.file(TEST_DB)
|
.file(TEST_DB)
|
||||||
.serializer(manager)
|
.serializer(manager)
|
||||||
.make();
|
.make();
|
||||||
|
|
||||||
for (int i = 0; i < total; i++) {
|
for (int i = 0; i < total; i++) {
|
||||||
log("adding record " + i + "...");
|
log("adding record " + i + "...");
|
||||||
String addRecord = createData(i);
|
String addRecord = createData(i);
|
||||||
@ -208,13 +220,15 @@ class StorageTest {
|
|||||||
log("reading record " + (total - 1) + "...");
|
log("reading record " + (total - 1) + "...");
|
||||||
String readData = storage.get();
|
String readData = storage.get();
|
||||||
|
|
||||||
|
// the ONLY entry in storage should be the last one that we added
|
||||||
Assert.assertEquals("Object is not the same", dataCheck, readData);
|
Assert.assertEquals("Object is not the same", dataCheck, readData);
|
||||||
|
|
||||||
int numberOfRecords1 = storage.size();
|
int numberOfRecords1 = storage.size();
|
||||||
long size1 = storage.getFileSize();
|
long size1 = storage.getFileSize();
|
||||||
|
|
||||||
Assert.assertEquals("count is not correct", numberOfRecords1, 1);
|
Assert.assertEquals("count is not correct", numberOfRecords1, 1);
|
||||||
Assert.assertEquals("size is not correct", size1, 235L); // NOTE this will change based on the data size added!
|
|
||||||
|
Assert.assertEquals("size is not correct", size1, initialSize + sizePerRecord);
|
||||||
|
|
||||||
Store.close(storage);
|
Store.close(storage);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@ -226,13 +240,12 @@ class StorageTest {
|
|||||||
@Test
|
@Test
|
||||||
public
|
public
|
||||||
void testAddRecords_DelaySaveA() throws IOException, ClassNotFoundException {
|
void testAddRecords_DelaySaveA() throws IOException, ClassNotFoundException {
|
||||||
int total = 100;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Storage storage = Store.Disk()
|
Storage storage = Store.Disk()
|
||||||
.file(TEST_DB)
|
.file(TEST_DB)
|
||||||
.serializer(manager)
|
.serializer(manager)
|
||||||
.make();
|
.make();
|
||||||
|
|
||||||
for (int i = 0; i < total; i++) {
|
for (int i = 0; i < total; i++) {
|
||||||
add(storage, i);
|
add(storage, i);
|
||||||
}
|
}
|
||||||
@ -272,13 +285,12 @@ class StorageTest {
|
|||||||
@Test
|
@Test
|
||||||
public
|
public
|
||||||
void testAddRecords_DelaySaveB() throws IOException, ClassNotFoundException {
|
void testAddRecords_DelaySaveB() throws IOException, ClassNotFoundException {
|
||||||
int total = 100;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Storage storage = Store.Disk()
|
Storage storage = Store.Disk()
|
||||||
.file(TEST_DB)
|
.file(TEST_DB)
|
||||||
.serializer(manager)
|
.serializer(manager)
|
||||||
.make();
|
.make();
|
||||||
|
|
||||||
for (int i = 0; i < total; i++) {
|
for (int i = 0; i < total; i++) {
|
||||||
add(storage, i);
|
add(storage, i);
|
||||||
}
|
}
|
||||||
@ -296,6 +308,7 @@ class StorageTest {
|
|||||||
.file(TEST_DB)
|
.file(TEST_DB)
|
||||||
.serializer(manager)
|
.serializer(manager)
|
||||||
.make();
|
.make();
|
||||||
|
|
||||||
for (int i = 0; i < total; i++) {
|
for (int i = 0; i < total; i++) {
|
||||||
String dataCheck = createData(i);
|
String dataCheck = createData(i);
|
||||||
String readRecord = readRecord(storage, i);
|
String readRecord = readRecord(storage, i);
|
||||||
@ -313,13 +326,12 @@ class StorageTest {
|
|||||||
@Test
|
@Test
|
||||||
public
|
public
|
||||||
void testLoadRecords() throws IOException, ClassNotFoundException {
|
void testLoadRecords() throws IOException, ClassNotFoundException {
|
||||||
int total = 100;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Storage storage = Store.Disk()
|
Storage storage = Store.Disk()
|
||||||
.file(TEST_DB)
|
.file(TEST_DB)
|
||||||
.serializer(manager)
|
.serializer(manager)
|
||||||
.make();
|
.make();
|
||||||
|
|
||||||
for (int i = 0; i < total; i++) {
|
for (int i = 0; i < total; i++) {
|
||||||
String addRecord = add(storage, i);
|
String addRecord = add(storage, i);
|
||||||
String readRecord = readRecord(storage, i);
|
String readRecord = readRecord(storage, i);
|
||||||
@ -332,6 +344,7 @@ class StorageTest {
|
|||||||
.file(TEST_DB)
|
.file(TEST_DB)
|
||||||
.serializer(manager)
|
.serializer(manager)
|
||||||
.make();
|
.make();
|
||||||
|
|
||||||
for (int i = 0; i < total; i++) {
|
for (int i = 0; i < total; i++) {
|
||||||
String dataCheck = createData(i);
|
String dataCheck = createData(i);
|
||||||
String readRecord = readRecord(storage, i);
|
String readRecord = readRecord(storage, i);
|
||||||
@ -346,8 +359,8 @@ class StorageTest {
|
|||||||
|
|
||||||
storage.put(createKey, data);
|
storage.put(createKey, data);
|
||||||
|
|
||||||
Data data2 = new Data();
|
Data data2;
|
||||||
storage.getAndPut(createKey, data2);
|
data2 = storage.getAndPut(createKey, new Data());
|
||||||
Assert.assertEquals("Object is not the same", data, data2);
|
Assert.assertEquals("Object is not the same", data, data2);
|
||||||
|
|
||||||
Store.close(storage);
|
Store.close(storage);
|
||||||
@ -356,8 +369,7 @@ class StorageTest {
|
|||||||
.serializer(manager)
|
.serializer(manager)
|
||||||
.make();
|
.make();
|
||||||
|
|
||||||
data2 = new Data();
|
data2 = storage.getAndPut(createKey, new Data());
|
||||||
storage.getAndPut(createKey, data2);
|
|
||||||
Assert.assertEquals("Object is not the same", data, data2);
|
Assert.assertEquals("Object is not the same", data, data2);
|
||||||
|
|
||||||
Store.close(storage);
|
Store.close(storage);
|
||||||
@ -371,13 +383,16 @@ class StorageTest {
|
|||||||
@Test
|
@Test
|
||||||
public
|
public
|
||||||
void testAddRecordsDelete1Record() throws IOException, ClassNotFoundException {
|
void testAddRecordsDelete1Record() throws IOException, ClassNotFoundException {
|
||||||
int total = 100;
|
if (total < 4) {
|
||||||
|
throw new IOException("Unable to run test with too few entries.");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Storage storage = Store.Disk()
|
Storage storage = Store.Disk()
|
||||||
.file(TEST_DB)
|
.file(TEST_DB)
|
||||||
.serializer(manager)
|
.serializer(manager)
|
||||||
.make();
|
.make();
|
||||||
|
|
||||||
for (int i = 0; i < total; i++) {
|
for (int i = 0; i < total; i++) {
|
||||||
String addRecord = add(storage, i);
|
String addRecord = add(storage, i);
|
||||||
String readRecord = readRecord(storage, i);
|
String readRecord = readRecord(storage, i);
|
||||||
@ -390,6 +405,7 @@ class StorageTest {
|
|||||||
.file(TEST_DB)
|
.file(TEST_DB)
|
||||||
.serializer(manager)
|
.serializer(manager)
|
||||||
.make();
|
.make();
|
||||||
|
|
||||||
for (int i = 0; i < total; i++) {
|
for (int i = 0; i < total; i++) {
|
||||||
String dataCheck = createData(i);
|
String dataCheck = createData(i);
|
||||||
String readRecord = readRecord(storage, i);
|
String readRecord = readRecord(storage, i);
|
||||||
@ -442,13 +458,12 @@ class StorageTest {
|
|||||||
@Test
|
@Test
|
||||||
public
|
public
|
||||||
void testUpdateRecords() throws IOException, ClassNotFoundException {
|
void testUpdateRecords() throws IOException, ClassNotFoundException {
|
||||||
int total = 100;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Storage storage = Store.Disk()
|
Storage storage = Store.Disk()
|
||||||
.file(TEST_DB)
|
.file(TEST_DB)
|
||||||
.serializer(manager)
|
.serializer(manager)
|
||||||
.make();
|
.make();
|
||||||
|
|
||||||
for (int i = 0; i < total; i++) {
|
for (int i = 0; i < total; i++) {
|
||||||
String addRecord = add(storage, i);
|
String addRecord = add(storage, i);
|
||||||
String readRecord = readRecord(storage, i);
|
String readRecord = readRecord(storage, i);
|
||||||
@ -507,13 +522,12 @@ class StorageTest {
|
|||||||
@Test
|
@Test
|
||||||
public
|
public
|
||||||
void testSaveAllRecords() throws IOException, ClassNotFoundException {
|
void testSaveAllRecords() throws IOException, ClassNotFoundException {
|
||||||
int total = 100;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Storage storage = Store.Disk()
|
Storage storage = Store.Disk()
|
||||||
.file(TEST_DB)
|
.file(TEST_DB)
|
||||||
.serializer(manager)
|
.serializer(manager)
|
||||||
.make();
|
.make();
|
||||||
|
|
||||||
for (int i = 0; i < total; i++) {
|
for (int i = 0; i < total; i++) {
|
||||||
Data data = new Data();
|
Data data = new Data();
|
||||||
makeData(data);
|
makeData(data);
|
||||||
@ -533,8 +547,8 @@ class StorageTest {
|
|||||||
for (int i = 0; i < total; i++) {
|
for (int i = 0; i < total; i++) {
|
||||||
String createKey = createKey(i);
|
String createKey = createKey(i);
|
||||||
|
|
||||||
Data data2 = new Data();
|
Data data2;
|
||||||
storage.getAndPut(createKey, data2);
|
data2 = storage.getAndPut(createKey, new Data());
|
||||||
Assert.assertEquals("Object is not the same", data, data2);
|
Assert.assertEquals("Object is not the same", data, data2);
|
||||||
}
|
}
|
||||||
Store.close(storage);
|
Store.close(storage);
|
||||||
@ -607,19 +621,19 @@ class StorageTest {
|
|||||||
|
|
||||||
data.strings = new String[] {"ab012", "", null, "!@#$", "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"};
|
data.strings = new String[] {"ab012", "", null, "!@#$", "<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>"};
|
||||||
data.ints = new int[] {-1234567, 1234567, -1, 0, 1, Integer.MAX_VALUE, Integer.MIN_VALUE};
|
data.ints = new int[] {-1234567, 1234567, -1, 0, 1, Integer.MAX_VALUE, Integer.MIN_VALUE};
|
||||||
data.shorts = new short[] {-12345, 12345, -1, 0, 1, Short.MAX_VALUE, Short.MIN_VALUE};
|
data.shorts = new short[] {(short) -12345, (short) 12345, (short) -1, (short) 0, (short) 1, Short.MAX_VALUE, Short.MIN_VALUE};
|
||||||
data.floats = new float[] {0, -0, 1, -1, 123456, -123456, 0.1f, 0.2f, -0.3f, (float) Math.PI, Float.MAX_VALUE, Float.MIN_VALUE};
|
data.floats = new float[] {0, -0, 1, -1, 123456, -123456, 0.1f, 0.2f, -0.3f, (float) Math.PI, Float.MAX_VALUE, Float.MIN_VALUE};
|
||||||
data.doubles = new double[] {0, -0, 1, -1, 123456, -123456, 0.1d, 0.2d, -0.3d, Math.PI, Double.MAX_VALUE, Double.MIN_VALUE};
|
data.doubles = new double[] {0, -0, 1, -1, 123456, -123456, 0.1d, 0.2d, -0.3d, Math.PI, Double.MAX_VALUE, Double.MIN_VALUE};
|
||||||
data.longs = new long[] {0, -0, 1, -1, 123456, -123456, 99999999999l, -99999999999l, Long.MAX_VALUE, Long.MIN_VALUE};
|
data.longs = new long[] {0, -0, 1, -1, 123456, -123456, 99999999999L, -99999999999L, Long.MAX_VALUE, Long.MIN_VALUE};
|
||||||
data.bytes = new byte[] {-123, 123, -1, 0, 1, Byte.MAX_VALUE, Byte.MIN_VALUE};
|
data.bytes = new byte[] {(byte) -123, (byte) 123, (byte) -1, (byte) 0, (byte) 1, Byte.MAX_VALUE, Byte.MIN_VALUE};
|
||||||
data.chars = new char[] {32345, 12345, 0, 1, 63, Character.MAX_VALUE, Character.MIN_VALUE};
|
data.chars = new char[] {32345, 12345, 0, 1, 63, Character.MAX_VALUE, Character.MIN_VALUE};
|
||||||
data.booleans = new boolean[] {true, false};
|
data.booleans = new boolean[] {true, false};
|
||||||
data.Ints = new Integer[] {-1234567, 1234567, -1, 0, 1, Integer.MAX_VALUE, Integer.MIN_VALUE};
|
data.Ints = new Integer[] {-1234567, 1234567, -1, 0, 1, Integer.MAX_VALUE, Integer.MIN_VALUE};
|
||||||
data.Shorts = new Short[] {-12345, 12345, -1, 0, 1, Short.MAX_VALUE, Short.MIN_VALUE};
|
data.Shorts = new Short[] {-12345, 12345, -1, 0, 1, Short.MAX_VALUE, Short.MIN_VALUE};
|
||||||
data.Floats = new Float[] {0f, -0f, 1f, -1f, 123456f, -123456f, 0.1f, 0.2f, -0.3f, (float) Math.PI, Float.MAX_VALUE,
|
data.Floats = new Float[] {0.0f, -0.0f, 1.0f, -1.0f, 123456.0f, -123456.0f, 0.1f, 0.2f, -0.3f, (float) Math.PI, Float.MAX_VALUE,
|
||||||
Float.MIN_VALUE};
|
Float.MIN_VALUE};
|
||||||
data.Doubles = new Double[] {0d, -0d, 1d, -1d, 123456d, -123456d, 0.1d, 0.2d, -0.3d, Math.PI, Double.MAX_VALUE, Double.MIN_VALUE};
|
data.Doubles = new Double[] {0.0d, -0.0d, 1.0d, -1.0d, 123456.0d, -123456.0d, 0.1d, 0.2d, -0.3d, Math.PI, Double.MAX_VALUE, Double.MIN_VALUE};
|
||||||
data.Longs = new Long[] {0l, -0l, 1l, -1l, 123456l, -123456l, 99999999999l, -99999999999l, Long.MAX_VALUE, Long.MIN_VALUE};
|
data.Longs = new Long[] {0L, -0L, 1L, -1L, 123456L, -123456L, 99999999999L, -99999999999L, Long.MAX_VALUE, Long.MIN_VALUE};
|
||||||
data.Bytes = new Byte[] {-123, 123, -1, 0, 1, Byte.MAX_VALUE, Byte.MIN_VALUE};
|
data.Bytes = new Byte[] {-123, 123, -1, 0, 1, Byte.MAX_VALUE, Byte.MIN_VALUE};
|
||||||
data.Chars = new Character[] {32345, 12345, 0, 1, 63, Character.MAX_VALUE, Character.MIN_VALUE};
|
data.Chars = new Character[] {32345, 12345, 0, 1, 63, Character.MAX_VALUE, Character.MIN_VALUE};
|
||||||
data.Booleans = new Boolean[] {true, false};
|
data.Booleans = new Boolean[] {true, false};
|
||||||
|
Loading…
Reference in New Issue
Block a user