com.eventsourcing.h2.MVStoreJournal Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of eventsourcing-h2 Show documentation
Show all versions of eventsourcing-h2 Show documentation
Event capture and querying framework for Java
The newest version!
/**
* Copyright (c) 2016, All Contributors (see CONTRIBUTORS file)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package com.eventsourcing.h2;
import com.eventsourcing.*;
import com.eventsourcing.layout.Layout;
import com.eventsourcing.layout.ObjectSerializer;
import com.eventsourcing.layout.Serialization;
import com.eventsourcing.layout.binary.BinarySerialization;
import com.google.common.collect.Iterators;
import com.google.common.io.BaseEncoding;
import com.google.common.primitives.Bytes;
import com.google.common.util.concurrent.AbstractService;
import com.googlecode.cqengine.index.support.CloseableIterator;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.h2.mvstore.MVMap;
import org.h2.mvstore.MVStore;
import org.h2.mvstore.db.TransactionStore;
import org.h2.mvstore.db.TransactionStore.TransactionMap;
import org.h2.mvstore.type.ObjectDataType;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
@Component(
service = Journal.class,
property = {"filename=journal.db", "type=MVStoreJournal", "jmx.objectname=com.eventsourcing:type=journal,name=MVStoreJournal"})
@Slf4j
public class MVStoreJournal extends AbstractService implements Journal {
@Getter @Setter
private Repository repository;
private final EntityLayoutExtractor entityLayoutExtractor = new EntityLayoutExtractor();
@Getter(AccessLevel.PACKAGE) @Setter(AccessLevel.PACKAGE) // getter and setter for tests
private MVStore store;
private TransactionMap commandPayloads;
private TransactionMap commandHashes;
private TransactionMap hashCommands;
private TransactionMap eventPayloads;
private TransactionMap hashEvents;
private TransactionMap eventHashes;
private TransactionStore transactionStore;
TransactionStore.Transaction readTx;
public MVStoreJournal(MVStore store) {
this();
this.store = store;
}
private final static Serialization serialization = BinarySerialization.getInstance();
@SneakyThrows
public MVStoreJournal() {
}
@Activate
protected void activate(ComponentContext ctx) {
store = MVStore.open((String) ctx.getProperties().get("filename"));
}
@Deactivate
protected void deactivate(ComponentContext ctx) {
store.close();
}
@Override
protected void doStart() {
if (repository == null) {
notifyFailed(new IllegalStateException("repository == null"));
}
if (store == null) {
notifyFailed(new IllegalStateException("store == null"));
}
initializeStore();
notifyStarted();
}
@Override
public void onCommandsAdded(Set> commands) {
commands.forEach(entityLayoutExtractor);
}
@Override
public void onEventsAdded(Set> events) {
events.forEach(entityLayoutExtractor);
}
void initializeStore() {
MVMap info = store.openMap("info");
info.putIfAbsent("version", 1);
store.commit();
transactionStore = new TransactionStore(this.store);
transactionStore.init();
initReadTx();
}
private void initReadTx() {
readTx = transactionStore.begin();
commandPayloads = readTx.openMap("commandPayloads", new ObjectDataType(), new ByteBufferDataType());
commandHashes = readTx.openMap("commandHashes");
hashCommands = readTx.openMap("hashCommands");
eventPayloads = readTx.openMap("eventPayloads", new ObjectDataType(), new ByteBufferDataType());
eventHashes = readTx.openMap("eventHashes");
hashEvents = readTx.openMap("hashEvents");
}
@Override
protected void doStop() {
transactionStore.close();
store.close();
notifyStopped();
}
private Map layoutsByHash = new HashMap<>();
private Map layoutsByClass = new HashMap<>();
private Layout getLayout(Class extends Entity> klass) {
Layout layout = layoutsByClass.computeIfAbsent(klass.getName(), new LayoutFunction<>(klass));
if (getLayout(layout.getHash()) == null) {
String encoded = BaseEncoding.base16().encode(layout.getHash());
layoutsByHash.put(encoded, layout);
}
return layout;
}
private Layout getLayout(byte[] hash) {
String encoded = BaseEncoding.base16().encode(hash);
return layoutsByHash.get(encoded);
}
static class Transaction implements Journal.Transaction {
@Getter
final TransactionStore.Transaction tx;
private final TransactionMap txEventHashes;
private final TransactionMap txHashEvents;
private final TransactionMap txEventPayloads;
Transaction(TransactionStore.Transaction tx) {
this.tx = tx;
txEventPayloads = tx.openMap("eventPayloads", new ObjectDataType(), new ByteBufferDataType());
txHashEvents = tx.openMap("hashEvents");
txEventHashes = tx.openMap("eventHashes");
}
@Override public void commit() {
tx.prepare();
tx.commit();
}
@Override public void rollback() {
tx.rollback();
}
}
@Override public Transaction beginTransaction() {
return new Transaction(transactionStore.begin());
}
@Override public Command journal(Journal.Transaction tx, Command command) {
TransactionStore.Transaction tx0 = ((Transaction) tx).getTx();
TransactionMap txCommandPayloads = tx0.openMap("commandPayloads", new ObjectDataType(),
new ByteBufferDataType());
TransactionMap txHashCommands = tx0.openMap("hashCommands");
TransactionMap txCommandHashes = tx0.openMap("commandHashes");
Layout commandLayout = getLayout(command.getClass());
ByteBuffer hashBuffer = ByteBuffer.allocate(16 + 20); // based on SHA-1
hashBuffer.put(commandLayout.getHash());
hashBuffer.putLong(command.uuid().getMostSignificantBits());
hashBuffer.putLong(command.uuid().getLeastSignificantBits());
ByteBuffer buffer = serialization.getSerializer(command.getClass()).serialize(command);
buffer.rewind();
txCommandPayloads.tryPut(command.uuid(), ByteBuffer.wrap(buffer.array()));
txHashCommands.tryPut(hashBuffer.array(), true);
txCommandHashes.tryPut(command.uuid(), commandLayout.getHash());
buffer.rewind();
Command command1 = (Command) serialization.getDeserializer(command.getClass()).deserialize(buffer);
command1.uuid(command.uuid());
return command1;
}
@SneakyThrows
@Override public Event journal(Journal.Transaction tx, Event event) {
Transaction tx0 = ((Transaction) tx);
Layout layout = getLayout(event.getClass());
ObjectSerializer serializer = serialization.getSerializer(event.getClass());
int size = serializer.size(event);
ByteBuffer payloadBuffer = ByteBuffer.allocate(size);
serializer.serialize(event, payloadBuffer);
payloadBuffer.rewind();
tx0.txEventPayloads.tryPut(event.uuid(), ByteBuffer.wrap(payloadBuffer.array()));
ByteBuffer hashBuffer = ByteBuffer.allocate(20 + 16); // Based on SHA-1
hashBuffer.rewind();
hashBuffer.put(layout.getHash());
hashBuffer.putLong(event.uuid().getMostSignificantBits());
hashBuffer.putLong(event.uuid().getLeastSignificantBits());
tx0.txHashEvents.tryPut(hashBuffer.array(), true);
tx0.txEventHashes.tryPut(event.uuid(), layout.getHash());
payloadBuffer.rewind();
Event event1 = (Event) serialization.getDeserializer(event.getClass()).deserialize(payloadBuffer);
event1.uuid(event.uuid());
return event1;
}
@Override
@SneakyThrows @SuppressWarnings("unchecked")
public Optional get(UUID uuid) {
if (commandPayloads.containsKey(uuid)) {
ByteBuffer payload = commandPayloads.get(uuid);
payload.rewind();
assert payload.array().length > 0;
byte[] bytes = commandHashes.get(uuid);
Layout> layout = getLayout(bytes);
Command command = (Command) serialization.getDeserializer(layout.getLayoutClass()).deserialize(payload);
command.uuid(uuid);
return Optional.of((T) command);
}
if (eventPayloads.containsKey(uuid)) {
ByteBuffer payload = eventPayloads.get(uuid);
payload.rewind();
assert payload.array().length > 0;
byte[] bytes = eventHashes.get(uuid);
Layout layout = getLayout(bytes);
Event event = (Event) serialization.getDeserializer(layout.getLayoutClass()).deserialize(payload);
event.uuid(uuid);
return Optional.of((T) event);
}
return Optional.empty();
}
@Override
public > CloseableIterator> commandIterator(Class klass) {
return entityIterator(klass, hashCommands);
}
@Override
public CloseableIterator> eventIterator(Class klass) {
return entityIterator(klass, hashEvents);
}
@SneakyThrows
private CloseableIterator> entityIterator(Class klass, TransactionMap map) {
Layout layout = getLayout(klass);
if (layout == null) {
layout = Layout.forClass(klass);
layoutsByClass.put(klass.getName(), layout);
}
byte[] hash = layout.getHash();
Iterator> iterator = map.entryIterator(map.higherKey(hash));
return new EntityHandleIterator<>(iterator, bytes -> Bytes.indexOf(bytes, hash) == 0,
new EntityFunction<>(hash));
}
@Override
public void clear() {
commandPayloads.clear();
hashCommands.clear();
eventPayloads.clear();
eventHashes.clear();
hashEvents.clear();
}
@Override @SuppressWarnings("unchecked")
public long size(Class klass) {
if (Event.class.isAssignableFrom(klass)) {
return Iterators.size(eventIterator((Class) klass));
}
if (Command.class.isAssignableFrom(klass)) {
return Iterators.size(commandIterator((Class>) klass));
}
throw new IllegalArgumentException();
}
@Override
public boolean isEmpty(Class klass) {
if (Event.class.isAssignableFrom(klass)) {
return !eventIterator((Class) klass).hasNext();
}
if (Command.class.isAssignableFrom(klass)) {
return !commandIterator((Class>) klass).hasNext();
}
throw new IllegalArgumentException();
}
private static class LayoutFunction implements Function {
private final Class extends Entity> klass;
public LayoutFunction(Class extends Entity> klass) {this.klass = klass;}
@SneakyThrows
@Override public Layout apply(X n) {return Layout.forClass(klass);}
}
private class EntityLayoutExtractor implements Consumer> {
@Override
@SneakyThrows
public void accept(Class extends Entity> aClass) {
Layout extends Entity> layout = Layout.forClass(aClass);
byte[] hash = layout.getHash();
String encodedHash = BaseEncoding.base16().encode(hash);
layoutsByHash.put(encodedHash, layout);
layoutsByClass.put(aClass.getName(), layout);
}
}
static private class EntityHandleIterator implements CloseableIterator {
private final Iterator> iterator;
private Function hasNext;
private final BiFunction function;
private Map.Entry entry;
public EntityHandleIterator(Iterator> iterator, Function hasNext,
BiFunction function) {
this.iterator = iterator;
this.hasNext = hasNext;
this.function = function;
}
@Override
public boolean hasNext() {
if (iterator.hasNext()) {
entry = iterator.next();
return hasNext.apply(entry.getKey());
} else {
return false;
}
}
@Override
public R next() {
if (entry == null) {
entry = iterator.next();
}
R result = function.apply(entry.getKey(), entry.getValue());
entry = null;
return result;
}
@Override
public void close() {
}
}
private class EntityFunction implements BiFunction> {
private final byte[] hash;
public EntityFunction(byte[] hash) {
this.hash = hash;
}
@Override
public EntityHandle apply(byte[] bytes, V value) {
ByteBuffer buffer = ByteBuffer.wrap(bytes);
UUID uuid = new UUID(buffer.getLong(hash.length), buffer.getLong(hash.length + 8));
return new JournalEntityHandle<>(MVStoreJournal.this, uuid);
}
}
}