Better implementation of LightList

This commit is contained in:
Andrea Cavalli 2018-11-22 23:31:41 +01:00
parent d9da3274c2
commit fa692b391a
11 changed files with 223 additions and 84 deletions

View File

@ -1,14 +1,8 @@
package org.warp.jcwdb; package org.warp.jcwdb;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Output;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
public class DBLightListParser extends DBTypeParserImpl<LightList> { public class DBLightListParser extends DBTypeParserImpl<LightList> {
private static final Kryo kryo = new Kryo();
private final JCWDatabase db; private final JCWDatabase db;
public DBLightListParser(JCWDatabase db) { public DBLightListParser(JCWDatabase db) {
@ -16,21 +10,24 @@ public class DBLightListParser extends DBTypeParserImpl<LightList> {
} }
public DBReader<LightList> getReader() { public DBReader<LightList> getReader() {
return (i) -> { return (i, size) -> {
ArrayList<Long> internalList = (ArrayList<Long>) kryo.readClassAndObject(i); ArrayList<Long> internalList = new ArrayList<>();
long max = size / Long.BYTES;
for (int item = 0; item < max; item++){
long itm = i.readLong();
internalList.add(itm);
}
return new LightList(db, internalList); return new LightList(db, internalList);
}; };
} }
public DBDataOutput<LightList> getWriter(final LightList value) { public DBDataOutput<LightList> getWriter(final LightList value) {
// TODO: optimize by writing longs directly, to make length determinable without writing to memory the output. final int elementsCount = value.internalList.size();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
Output out = new Output(baos);
kryo.writeClassAndObject(out, value.internalList);
out.close();
byte[] b = baos.toByteArray();
return DBDataOutput.create((o) -> { return DBDataOutput.create((o) -> {
o.write(b); ArrayList<Long> list = value.internalList;
}, DBStandardTypes.STRING, b.length); for (Long item : list) {
o.writeLong(item);
}
}, DBStandardTypes.LIGHT_LIST, elementsCount * Long.BYTES);
} }
} }

View File

@ -3,5 +3,5 @@ package org.warp.jcwdb;
import com.esotericsoftware.kryo.io.Input; import com.esotericsoftware.kryo.io.Input;
public interface DBReader<T> { public interface DBReader<T> {
T read(Input i); T read(Input i, int size);
} }

View File

@ -1,12 +1,11 @@
package org.warp.jcwdb; package org.warp.jcwdb;
import java.nio.charset.StandardCharsets; import java.io.ByteArrayOutputStream;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output; import com.esotericsoftware.kryo.io.Output;
public class DBStringParser extends DBTypeParserImpl<String> { public class DBStringParser extends DBTypeParserImpl<String> {
private static final DBReader<String> defaultReader = (i) -> { private static final DBReader<String> defaultReader = (i, size) -> {
return i.readString(); return i.readString();
}; };
@ -15,7 +14,12 @@ public class DBStringParser extends DBTypeParserImpl<String> {
} }
public DBDataOutput<String> getWriter(final String value) { public DBDataOutput<String> getWriter(final String value) {
final byte[] bytes = value.getBytes(StandardCharsets.UTF_8); ByteArrayOutputStream baos = new ByteArrayOutputStream();
Output tmpO = new Output(baos);
tmpO.writeString(value);
tmpO.flush();
final byte[] bytes = baos.toByteArray();
tmpO.close();
return DBDataOutput.create((o) -> { return DBDataOutput.create((o) -> {
o.write(bytes); o.write(bytes);
}, DBStandardTypes.STRING, bytes.length); }, DBStandardTypes.STRING, bytes.length);

View File

@ -21,6 +21,13 @@ public class EntryReference<T> implements Castable, AutoCloseable {
this.value = db.indices.get(entryId, parser.getReader()); this.value = db.indices.get(entryId, parser.getReader());
} }
public EntryReference(JCWDatabase db, long entryId, DBTypeParser<T> parser, T value) {
this.db = db;
this.entryIndex = entryId;
this.parser = parser;
this.value = value;
}
public DBTypeParser<T> getParser() { public DBTypeParser<T> getParser() {
return parser; return parser;
} }

View File

@ -15,6 +15,7 @@ public class FileAllocator implements AutoCloseable {
private volatile long allocableOffset; private volatile long allocableOffset;
private volatile boolean closed; private volatile boolean closed;
private final Object closeLock = new Object(); private final Object closeLock = new Object();
private final Object allocateLock = new Object();
public FileAllocator(SeekableByteChannel dataFileChannel) throws IOException { public FileAllocator(SeekableByteChannel dataFileChannel) throws IOException {
this.dataFileChannel = dataFileChannel; this.dataFileChannel = dataFileChannel;
@ -28,9 +29,11 @@ public class FileAllocator implements AutoCloseable {
*/ */
public long allocate(int size) { public long allocate(int size) {
checkClosed(); checkClosed();
long allocatedOffset = allocableOffset; synchronized (allocateLock) {
allocatedOffset += size; long allocatedOffset = allocableOffset;
return allocatedOffset; allocableOffset += size;
return allocatedOffset;
}
} }

View File

@ -10,6 +10,7 @@ import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileAttribute;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Map; import java.util.Map;
@ -39,10 +40,19 @@ public class FileIndexManager implements IndexManager {
loadedIndices = new HashMap<>(); loadedIndices = new HashMap<>();
dirtyLoadedIndices = new HashSet<>(); dirtyLoadedIndices = new HashSet<>();
removedIndices = new HashSet<>(); removedIndices = new HashSet<>();
dataFileChannel = Files.newByteChannel(dataFile, StandardOpenOption.CREATE); if (Files.notExists(dataFile)) {
metadataFileChannel = Files.newByteChannel(metadataFile, StandardOpenOption.CREATE); Files.createFile(dataFile);
}
if (Files.notExists(metadataFile)) {
Files.createFile(metadataFile);
}
dataFileChannel = Files.newByteChannel(dataFile, StandardOpenOption.READ, StandardOpenOption.WRITE);
metadataFileChannel = Files.newByteChannel(metadataFile, StandardOpenOption.READ, StandardOpenOption.WRITE);
fileAllocator = new FileAllocator(dataFileChannel); fileAllocator = new FileAllocator(dataFileChannel);
firstAllocableIndex = metadataFileChannel.size() / (long) IndexDetails.TOTAL_BYTES; firstAllocableIndex = metadataFileChannel.size() / (long) IndexDetails.TOTAL_BYTES;
if (firstAllocableIndex == 0) {
firstAllocableIndex = 1;
}
} }
@Override @Override
@ -50,8 +60,7 @@ public class FileIndexManager implements IndexManager {
checkClosed(); checkClosed();
IndexDetails details = getIndexMetadata(index); IndexDetails details = getIndexMetadata(index);
Input i = new Input(Channels.newInputStream(dataFileChannel.position(details.getOffset()))); Input i = new Input(Channels.newInputStream(dataFileChannel.position(details.getOffset())));
T result = reader.read(i); T result = reader.read(i, details.getSize());
i.close();
return result; return result;
} }
@ -87,14 +96,6 @@ public class FileIndexManager implements IndexManager {
return index; return index;
} }
private long createIndexMetadata(IndexDetails indexDetails) {
long newIndex = firstAllocableIndex++;
loadedIndices.put(newIndex, indexDetails);
dirtyLoadedIndices.add(newIndex);
removedIndices.remove(newIndex);
return newIndex;
}
/** /**
* Write the data at index. * Write the data at index.
* The input size must be equal to the index size! * The input size must be equal to the index size!
@ -110,7 +111,7 @@ public class FileIndexManager implements IndexManager {
final Output o = new Output(Channels.newOutputStream(dataFileChannel.position(offset))); final Output o = new Output(Channels.newOutputStream(dataFileChannel.position(offset)));
data.getWriter().write(o); data.getWriter().write(o);
o.close(); o.flush();
} }
private void allocateAndWrite(final long index, DBDataOutput<?> w) throws IOException { private void allocateAndWrite(final long index, DBDataOutput<?> w) throws IOException {
@ -150,18 +151,36 @@ public class FileIndexManager implements IndexManager {
return indexDetails; return indexDetails;
} }
private void editIndex(long index, IndexDetails details) {
loadedIndices.put(index, details);
dirtyLoadedIndices.add(index);
}
private long createIndexMetadata(IndexDetails indexDetails) {
long newIndex = firstAllocableIndex++;
loadedIndices.put(newIndex, indexDetails);
dirtyLoadedIndices.add(newIndex);
removedIndices.remove(newIndex);
return newIndex;
}
private IndexDetails getIndexMetadataUnsafe(long index) throws IOException { private IndexDetails getIndexMetadataUnsafe(long index) throws IOException {
// Return index details if loaded // Return index details if loaded
IndexDetails details = loadedIndices.getOrDefault(index, null); IndexDetails details = loadedIndices.getOrDefault(index, null);
if (details != null) return details; if (details != null) return details;
// Try to load the details from file // Try to load the details from file
SeekableByteChannel currentMetadataFileChannel = metadataFileChannel.position(index * IndexDetails.TOTAL_BYTES); final long metadataPosition = index * IndexDetails.TOTAL_BYTES;
if (metadataPosition + IndexDetails.TOTAL_BYTES > metadataFileChannel.size()) {
// Avoid underflow exception
return null;
}
SeekableByteChannel currentMetadataFileChannel = metadataFileChannel.position(metadataPosition);
ByteBuffer currentMetadataByteBuffer = ByteBuffer.allocateDirect(IndexDetails.TOTAL_BYTES); ByteBuffer currentMetadataByteBuffer = ByteBuffer.allocateDirect(IndexDetails.TOTAL_BYTES);
currentMetadataFileChannel.read(currentMetadataByteBuffer); currentMetadataFileChannel.read(currentMetadataByteBuffer);
currentMetadataByteBuffer.flip(); currentMetadataByteBuffer.flip();
// If it's not deleted continue // If it's not deleted continue
if ((currentMetadataByteBuffer.get() & IndexDetails.MASK_DELETED) == 0) { if ((currentMetadataByteBuffer.getInt() & IndexDetails.MASK_DELETED) == 0) {
final long offset = currentMetadataByteBuffer.getLong(); final long offset = currentMetadataByteBuffer.getLong();
final int size = currentMetadataByteBuffer.getInt(); final int size = currentMetadataByteBuffer.getInt();
final int type = currentMetadataByteBuffer.getInt(); final int type = currentMetadataByteBuffer.getInt();
@ -182,11 +201,6 @@ public class FileIndexManager implements IndexManager {
return details; return details;
} }
private void editIndex(long index, IndexDetails details) {
loadedIndices.put(index, details);
dirtyLoadedIndices.add(index);
}
@Override @Override
public void close() throws IOException { public void close() throws IOException {
if (closed) { if (closed) {
@ -208,7 +222,8 @@ public class FileIndexManager implements IndexManager {
if (index - lastIndex != 1) { if (index - lastIndex != 1) {
metadata = metadata.position(index * IndexDetails.TOTAL_BYTES); metadata = metadata.position(index * IndexDetails.TOTAL_BYTES);
} }
metadataEntryBuffer.put((byte) 0); metadataEntryBuffer.clear();
metadataEntryBuffer.putInt(0);
metadataEntryBuffer.putLong(indexDetails.getOffset()); metadataEntryBuffer.putLong(indexDetails.getOffset());
metadataEntryBuffer.putInt(indexDetails.getSize()); metadataEntryBuffer.putInt(indexDetails.getSize());
metadataEntryBuffer.putInt(indexDetails.getType()); metadataEntryBuffer.putInt(indexDetails.getType());
@ -221,7 +236,7 @@ public class FileIndexManager implements IndexManager {
ByteBuffer updatedMaskBuffer = ByteBuffer.allocateDirect(1); ByteBuffer updatedMaskBuffer = ByteBuffer.allocateDirect(1);
for (Long index : removedIndices) { for (Long index : removedIndices) {
metadata = metadata.position(index * IndexDetails.TOTAL_BYTES); metadata = metadata.position(index * IndexDetails.TOTAL_BYTES);
updatedMaskBuffer.put(IndexDetails.MASK_DELETED); updatedMaskBuffer.putInt(IndexDetails.MASK_DELETED);
updatedMaskBuffer.flip(); updatedMaskBuffer.flip();
metadata.write(updatedMaskBuffer); metadata.write(updatedMaskBuffer);
} }

View File

@ -6,12 +6,12 @@ public class IndexDetails {
/** /**
* The bitmask is used to determine if an index has been deleted * The bitmask is used to determine if an index has been deleted
*/ */
public static final int BITMASK = 1; // 1 byte public static final int BITMASK_SIZE = Integer.BYTES;
public static final int OFFSET_BYTES = Long.SIZE; public static final int OFFSET_BYTES = Long.BYTES;
public static final int DATA_SIZE_BYTES = Integer.SIZE; public static final int DATA_SIZE_BYTES = Integer.BYTES;
public static final int TYPE_BYTES = Integer.SIZE; public static final int TYPE_BYTES = Integer.BYTES;
public static final int TOTAL_BYTES = BITMASK + OFFSET_BYTES + DATA_SIZE_BYTES + TYPE_BYTES; public static final int TOTAL_BYTES = BITMASK_SIZE + OFFSET_BYTES + DATA_SIZE_BYTES + TYPE_BYTES;
public static final byte MASK_DELETED = 0b00000001; public static final int MASK_DELETED = 0b00000001;
private final long offset; private final long offset;
private final int size; private final int size;
private final int type; private final int type;

View File

@ -2,6 +2,8 @@ package org.warp.jcwdb;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map; import java.util.Map;
import java.util.WeakHashMap; import java.util.WeakHashMap;
@ -10,56 +12,134 @@ public class JCWDatabase implements AutoCloseable {
protected final MixedIndexDatabase indices; protected final MixedIndexDatabase indices;
private final WeakHashMap<Long, EntryReference<?>> references; private final WeakHashMap<Long, EntryReference<?>> references;
private final WeakHashMap<Object, EntryReference<?>> referencesByObject; private final WeakHashMap<Object, EntryReference<?>> referencesByObject;
private volatile boolean closed;
private final Object closeLock = new Object();
private final Object indicesAccessLock = new Object();
private final Object referencesAccessLock = new Object();
public JCWDatabase(Path dataFile, Path metadataFile) throws IOException { public JCWDatabase(Path dataFile, Path metadataFile) throws IOException {
this.typesManager = new TypesManager(this); this.typesManager = new TypesManager(this);
this.indices = new MixedIndexDatabase(typesManager, dataFile, metadataFile); this.indices = new MixedIndexDatabase(typesManager, dataFile, metadataFile);
this.references = new WeakHashMap<>(); this.references = new WeakHashMap<>();
this.referencesByObject = new WeakHashMap<>(); this.referencesByObject = new WeakHashMap<>();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
JCWDatabase.this.close();
} catch (Exception e) {
e.printStackTrace();
}
}));
} }
public <T> EntryReference<T> getRoot() throws IOException { public <T> EntryReference<LightList<T>> getRoot() throws IOException {
try { checkClosed();
if (exists(0)) {
return get(0); return get(0);
} catch (IOException e) { } else {
throw new IOException("Can't load root!", e); LightList<T> newRoot = new LightList<T>(this, new ArrayList<>());
return set(0, newRoot);
} }
} }
public <T> EntryReference<T> get(long index) throws IOException { public <T> EntryReference<T> get(long index) throws IOException {
EntryReference<T> ref = (EntryReference<T>) this.references.getOrDefault(index, null); checkClosed();
if (ref == null) { synchronized (referencesAccessLock) {
int type = this.indices.getType(index); EntryReference<T> ref = (EntryReference<T>) this.references.getOrDefault(index, null);
DBTypeParser<T> typeParser = this.typesManager.get(type); if (ref == null) {
ref = new EntryReference<>(this, index, typeParser); int type;
this.references.put(index, ref); synchronized (indicesAccessLock) {
this.referencesByObject.put(ref.value, ref); type = this.indices.getType(index);
} }
return ref; DBTypeParser<T> typeParser = this.typesManager.get(type);
} ref = new EntryReference<>(this, index, typeParser);
this.references.put(index, ref);
public <T> EntryReference<T> add(T o) throws IOException { this.referencesByObject.put(ref.value, ref);
EntryReference<T> ref = (EntryReference<T>) referencesByObject.getOrDefault(o, null); }
if (ref != null) {
return ref; return ref;
} }
DBTypeParser<T> typeParser = this.typesManager.get((Class<T>) o.getClass()); }
long index = indices.add(typeParser.getWriter(o));
ref = new EntryReference<>(this, index, typeParser); protected <T> EntryReference<T> add(T value) throws IOException {
this.references.put(index, ref); checkClosed();
this.referencesByObject.put(o, ref); synchronized (referencesAccessLock) {
return ref; EntryReference<T> ref = (EntryReference<T>) referencesByObject.getOrDefault(value, null);
if (ref != null) {
return ref;
}
DBTypeParser<T> typeParser = this.typesManager.get((Class<T>) value.getClass());
long index;
synchronized (indicesAccessLock) {
index = indices.add(typeParser.getWriter(value));
}
ref = new EntryReference<>(this, index, typeParser, value);
this.references.put(index, ref);
this.referencesByObject.put(value, ref);
return ref;
}
}
protected boolean exists(long index) {
checkClosed();
synchronized (referencesAccessLock) {
synchronized (indicesAccessLock) {
return this.references.containsKey(index) || this.indices.has(index);
}
}
}
protected <T> EntryReference<T> set(long index, T value) throws IOException {
checkClosed();
synchronized (referencesAccessLock) {
EntryReference<T> ref;
if (exists(index)) {
ref = get(index);
ref.value = value;
return ref;
} else {
DBTypeParser<T> typeParser = this.typesManager.get((Class<T>) value.getClass());
synchronized (indicesAccessLock) {
indices.set(index, typeParser.getWriter(value));
}
ref = new EntryReference<>(this, index, typeParser);
this.references.put(index, ref);
this.referencesByObject.put(value, ref);
return ref;
}
}
} }
protected void removeEntryReference(long index) { protected void removeEntryReference(long index) {
this.references.remove(index); synchronized (referencesAccessLock) {
this.references.remove(index);
}
} }
@Override @Override
public void close() throws Exception { public void close() throws Exception {
for (Map.Entry<Long, EntryReference<?>> reference : references.entrySet()) { if (closed) {
reference.getValue().close(); return;
}
synchronized (closeLock) {
if (closed) {
return;
}
closed = true;
}
synchronized (referencesAccessLock) {
for (Map.Entry<Long, EntryReference<?>> reference : references.entrySet()) {
reference.getValue().save();
}
}
synchronized (indicesAccessLock) {
this.indices.close();
}
System.out.println("Database closed.");
}
private void checkClosed() {
if (closed) {
throw new RuntimeException("Index Manager is closed.");
} }
this.indices.close();
} }
} }

View File

@ -139,8 +139,12 @@ public class LightList<T> implements List<T> {
@Override @Override
public T get(int index) { public T get(int index) {
// TODO: implement try {
return null; return (T) db.get(internalList.get(index)).value;
} catch (IOException e) {
e.printStackTrace();
return null;
}
} }
@Override @Override

View File

@ -7,13 +7,11 @@ import java.io.IOException;
import java.nio.file.Path; import java.nio.file.Path;
public class MixedIndexDatabase implements IndexManager { public class MixedIndexDatabase implements IndexManager {
private final TypesManager typesManager;
private final Long2LongMap mostAccessedIndices; private final Long2LongMap mostAccessedIndices;
private final FileIndexManager fileIndices; private final FileIndexManager fileIndices;
private final CacheIndexManager cacheIndices; private final CacheIndexManager cacheIndices;
public MixedIndexDatabase(TypesManager typesManager, Path dataFile, Path metadataFile) throws IOException { public MixedIndexDatabase(TypesManager typesManager, Path dataFile, Path metadataFile) throws IOException {
this.typesManager = typesManager;
this.mostAccessedIndices = new Long2LongLinkedOpenHashMap(); this.mostAccessedIndices = new Long2LongLinkedOpenHashMap();
this.fileIndices = new FileIndexManager(typesManager, dataFile, metadataFile); this.fileIndices = new FileIndexManager(typesManager, dataFile, metadataFile);
this.cacheIndices = new CacheIndexManager(typesManager); this.cacheIndices = new CacheIndexManager(typesManager);
@ -21,6 +19,7 @@ public class MixedIndexDatabase implements IndexManager {
@Override @Override
public <T> T get(long index, DBReader<T> reader) throws IOException { public <T> T get(long index, DBReader<T> reader) throws IOException {
incrementUsage(index);
if (cacheIndices.has(index)) { if (cacheIndices.has(index)) {
return cacheIndices.get(index, reader); return cacheIndices.get(index, reader);
} else { } else {
@ -62,6 +61,10 @@ public class MixedIndexDatabase implements IndexManager {
return cacheIndices.has(index) || fileIndices.has(index); return cacheIndices.has(index) || fileIndices.has(index);
} }
private void incrementUsage(long index) {
mostAccessedIndices.put(index, mostAccessedIndices.getOrDefault(index, 0) + 1);
}
@Override @Override
public void close() throws IOException { public void close() throws IOException {
// TODO: move all cached indices to filesIndices before closing. // TODO: move all cached indices to filesIndices before closing.

View File

@ -0,0 +1,26 @@
package org.warp.jcwdb.exampleimpl;
import org.warp.jcwdb.EntryReference;
import org.warp.jcwdb.JCWDatabase;
import org.warp.jcwdb.LightList;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) throws Exception {
if (args.length > 2 && Boolean.parseBoolean(args[2])) {
Files.delete(Paths.get(args[0]));
Files.delete(Paths.get(args[1]));
}
JCWDatabase db = new JCWDatabase(Paths.get(args[0]), Paths.get(args[1]));
LightList<String> root = ((EntryReference<LightList<String>>) db.getRoot().cast()).value;
System.out.println("Root:");
for (int i = 0; i < root.size(); i++) {
System.out.println(" - " + root.get(i));
}
for (int i = 0; i < 100; i++) {
root.add("Test " + System.currentTimeMillis());
}
}
}