/*
* Copyright 2014 Higher Frequency Trading
*
* http://www.higherfrequencytrading.com
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.openhft.chronicle.map;
import net.openhft.chronicle.hash.KeyContext;
import net.openhft.chronicle.hash.hashing.LongHashFunction;
import net.openhft.chronicle.hash.impl.ContextFactory;
import net.openhft.chronicle.hash.impl.HashContext;
import net.openhft.chronicle.hash.replication.AbstractReplication;
import net.openhft.chronicle.hash.replication.TimeProvider;
import net.openhft.chronicle.hash.serialization.BytesReader;
import net.openhft.chronicle.hash.serialization.internal.BytesBytesInterop;
import net.openhft.chronicle.hash.serialization.internal.DelegatingMetaBytesInterop;
import net.openhft.chronicle.hash.serialization.internal.MetaBytesInterop;
import net.openhft.lang.Maths;
import net.openhft.lang.collection.ATSDirectBitSet;
import net.openhft.lang.collection.SingleThreadedDirectBitSet;
import net.openhft.lang.io.Bytes;
import net.openhft.lang.threadlocal.ThreadLocalCopies;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Closeable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicReferenceArray;
import static net.openhft.lang.MemoryUnit.*;
import static net.openhft.lang.collection.DirectBitSet.NOT_FOUND;
/**
*
A Replicating Multi Master HashMap
Each remote hash map, mirrors its changes over to
* another remote hash map, neither hash map is considered the master store of data, each hash map
* uses timestamps to reconcile changes. We refer to an instance of a remote hash-map as a node. A
* node will be connected to any number of other nodes, for the first implementation the maximum
* number of nodes will be fixed. The data that is stored locally in each node will become
* eventually consistent. So changes made to one node, for example by calling put() will be
* replicated over to the other node. To achieve a high level of performance and throughput, the
* call to put() won’t block, with concurrentHashMap, It is typical to check the return code of some
* methods to obtain the old value for example remove(). Due to the loose coupling and lock free
* nature of this multi master implementation, this return value will only be the old value on the
* nodes local data store. In other words the nodes are only concurrent locally. Its worth realising
* that another node performing exactly the same operation may return a different value. However
* reconciliation will ensure the maps themselves become eventually consistent.
*
Reconciliation
If two ( or more nodes ) were to receive a change to their maps for
* the same key but different values, say by a user of the maps, calling the put(key, value). Then,
* initially each node will update its local store and each local store will hold a different value,
* but the aim of multi master replication is to provide eventual consistency across the nodes. So,
* with multi master when ever a node is changed it will notify the other nodes of its change. We
* will refer to this notification as an event. The event will hold a timestamp indicating the time
* the change occurred, it will also hold the state transition, in this case it was a put with a key
* and value. Eventual consistency is achieved by looking at the timestamp from the remote node, if
* for a given key, the remote nodes timestamp is newer than the local nodes timestamp, then the
* event from the remote node will be applied to the local node, otherwise the event will be
* ignored.
However there is an edge case that we have to concern ourselves with, If two
* nodes update their map at the same time with different values, we have to deterministically
* resolve which update wins, because of eventual consistency both nodes should end up locally
* holding the same data. Although it is rare two remote nodes could receive an update to their maps
* at exactly the same time for the same key, we have to handle this edge case, its therefore
* important not to rely on timestamps alone to reconcile the updates. Typically the update with the
* newest timestamp should win, but in this example both timestamps are the same, and the decision
* made to one node should be identical to the decision made to the other. We resolve this simple
* dilemma by using a node identifier, each node will have a unique identifier, the update from the
* node with the smallest identifier wins.
*
* @param the entries key type
* @param the entries value type
*/
class ReplicatedChronicleMap,
V, VI, MVI extends MetaBytesInterop>
extends VanillaChronicleMap
implements Replica, Replica.EntryExternalizable, Replica.EntryResolver {
// for file, jdbc and UDP replication
public static final int RESERVED_MOD_ITER = 8;
public static final int ADDITIONAL_ENTRY_BYTES = 10;
private static final long serialVersionUID = 0L;
private static final Logger LOG = LoggerFactory.getLogger(ReplicatedChronicleMap.class);
private static final long LAST_UPDATED_HEADER_SIZE = 128L * 8L;
private final TimeProvider timeProvider;
private final byte localIdentifier;
transient Set closeables;
private transient Bytes identifierUpdatedBytes;
private transient ATSDirectBitSet modIterSet;
private transient AtomicReferenceArray modificationIterators;
private transient long startOfModificationIterators;
private boolean bootstrapOnlyLocalEntries;
public ReplicatedChronicleMap(@NotNull ChronicleMapBuilder builder,
AbstractReplication replication)
throws IOException {
super(builder, true);
this.timeProvider = builder.timeProvider();
this.localIdentifier = replication.identifier();
this.bootstrapOnlyLocalEntries = replication.bootstrapOnlyLocalEntries();
if (localIdentifier == -1) {
throw new IllegalStateException("localIdentifier should not be -1");
}
}
private int assignedModIterBitSetSizeInBytes() {
return (int) CACHE_LINES.align(BYTES.alignAndConvert(127 + RESERVED_MOD_ITER, BITS), BYTES);
}
@Override
public void initTransients() {
super.initTransients();
ownInitTransients();
}
private void ownInitTransients() {
modificationIterators =
new AtomicReferenceArray<>(127 + RESERVED_MOD_ITER);
closeables = new CopyOnWriteArraySet<>();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
ownInitTransients();
}
long modIterBitSetSizeInBytes() {
long bytes = BITS.toBytes(bitsPerSegmentInModIterBitSet() * actualSegments);
return CACHE_LINES.align(bytes, BYTES);
}
private long bitsPerSegmentInModIterBitSet() {
// min 128 * 8 to prevent false sharing on updating bits from different segments
// TODO this doesn't prevent false sharing. There should be GAPS between per-segment bits
return Maths.nextPower2(actualChunksPerSegment, 128L * 8L);
}
@Override
public long mapHeaderInnerSize() {
return super.mapHeaderInnerSize() + LAST_UPDATED_HEADER_SIZE +
(modIterBitSetSizeInBytes() * (128 + RESERVED_MOD_ITER)) +
assignedModIterBitSetSizeInBytes();
}
void setLastModificationTime(byte identifier, long timestamp) {
final long offset = identifier * 8L;
// purposely not volatile as this will impact performance,
// and the worst that will happen is we'll end up loading more data on a bootstrap
if (identifierUpdatedBytes.readLong(offset) < timestamp)
identifierUpdatedBytes.writeLong(offset, timestamp);
}
@Override
public long lastModificationTime(byte remoteIdentifier) {
assert remoteIdentifier != this.identifier();
// purposely not volatile as this will impact performance,
// and the worst that will happen is we'll end up loading more data on a bootstrap
return identifierUpdatedBytes.readLong(remoteIdentifier * 8L);
}
@Override
public void onHeaderCreated() {
long offset = super.mapHeaderInnerSize();
identifierUpdatedBytes = ms.bytes(offset, LAST_UPDATED_HEADER_SIZE).zeroOut();
offset += LAST_UPDATED_HEADER_SIZE;
Bytes modDelBytes = ms.bytes(offset, assignedModIterBitSetSizeInBytes()).zeroOut();
offset += assignedModIterBitSetSizeInBytes();
startOfModificationIterators = offset;
modIterSet = new ATSDirectBitSet(modDelBytes);
}
@Override
public void clear() {
forEachEntry(KeyContext::remove);
}
void addCloseable(Closeable closeable) {
closeables.add(closeable);
}
@Override
public void close() {
for (Closeable closeable : closeables) {
try {
closeable.close();
} catch (IOException e) {
LOG.error("", e);
}
}
super.close();
}
@Override
public byte identifier() {
return localIdentifier;
}
@Override
public Replica.ModificationIterator acquireModificationIterator(
byte remoteIdentifier, @NotNull final ModificationNotifier modificationNotifier) {
ModificationIterator modificationIterator = modificationIterators.get(remoteIdentifier);
if (modificationIterator != null)
return modificationIterator;
synchronized (modificationIterators) {
modificationIterator = modificationIterators.get(remoteIdentifier);
if (modificationIterator != null)
return modificationIterator;
final Bytes bytes = ms.bytes(startOfModificationIterators +
(modIterBitSetSizeInBytes() * remoteIdentifier),
modIterBitSetSizeInBytes());
final ModificationIterator newModificationIterator = new ModificationIterator(
bytes, modificationNotifier);
modificationIterators.set(remoteIdentifier, newModificationIterator);
modIterSet.set(remoteIdentifier);
return newModificationIterator;
}
}
void raiseChange(long segmentIndex, long pos) {
for (long next = modIterSet.nextSetBit(0L); next > 0L;
next = modIterSet.nextSetBit(next + 1L)) {
try {
modificationIterators.get((int) next).raiseChange(segmentIndex, pos);
} catch (Exception e) {
LOG.error("", e);
}
}
}
void dropChange(long segmentIndex, long pos) {
for (long next = modIterSet.nextSetBit(0L); next > 0L;
next = modIterSet.nextSetBit(next + 1L)) {
try {
modificationIterators.get((int) next).dropChange(segmentIndex, pos);
} catch (Exception e) {
LOG.error("", e);
}
}
}
public boolean identifierCheck(@NotNull Bytes entry, int chronicleId) {
long start = entry.position();
try {
final long keySize = keySizeMarshaller.readSize(entry);
entry.skip(keySize + 8); // we skip 8 for the timestamp
final byte identifier = entry.readByte();
return identifier == localIdentifier;
} finally {
entry.position(start);
}
}
static class ReplicatedContext,
V, VI, MVI extends MetaBytesInterop>
extends VanillaContext {
ReplicatedContext() {
super();
}
ReplicatedContext(HashContext contextCache, int indexInContextCache) {
super(contextCache, indexInContextCache);
}
ReplicatedChronicleMap rm() {
return (ReplicatedChronicleMap) m();
}
@Override
public void totalCheckClosed() {
super.totalCheckClosed();
assert !replicationStateInit() : "replication state not closed";
assert !replicationUpdateInit() : "replication update not closed";
}
/////////////////////////////////////////////////
// Replication state
long replicationBytesOffset;
long timestamp;
byte identifier;
@Override
public boolean containsKey0() {
return searchStatePresent() && !isDeleted();
}
@Override
public void keyFound() {
super.keyFound();
initReplicationBytesOffset0();
timestamp = entry.readLong(replicationBytesOffset);
identifier = entry.readByte(replicationBytesOffset + 8L);
}
boolean isDeleted() {
return entry.readBoolean(replicationBytesOffset + 9L);
}
void initReplicationBytesOffset0() {
replicationBytesOffset = keyOffset0() + keySize0();
}
@Override
public void initKeyOffset0() {
super.initKeyOffset0();
initReplicationBytesOffset0();
}
void closeReplicationState() {
if (!replicationStateInit())
return;
closeReplicationState0();
}
boolean replicationStateInit() {
return replicationBytesOffset != 0;
}
void closeReplicationState0() {
replicationBytesOffset = 0L;
timestamp = 0L;
identifier = (byte) 0;
}
@Override
public void closeKeySearchDependants() {
super.closeKeySearchDependants();
closeReplicationState();
}
@Override
void initValueSizeOffset0() {
valueSizeOffset = replicationBytesOffset + ADDITIONAL_ENTRY_BYTES;
}
/////////////////////////////////////////////////
// Replication update
long newTimestamp;
byte newIdentifier;
void initReplicationUpdate() {
if (replicationUpdateInit())
return;
checkHashInit();
initReplicationUpdate0();
}
boolean replicationUpdateInit() {
return newIdentifier != 0;
}
void initReplicationUpdate0() {
newTimestamp = rm().timeProvider.currentTime();
newIdentifier = rm().identifier();
}
boolean remoteUpdate() {
return newIdentifier != rm().identifier();
}
void closeReplicationUpdate() {
if (!replicationUpdateInit())
return;
closeReplicationUpdate0();
}
void closeReplicationUpdate0() {
newTimestamp = 0L;
newIdentifier = (byte) 0;
}
@Override
public void initRemoveDependencies() {
super.initRemoveDependencies();
initReplicationUpdate();
}
void writeReplicationBytes() {
entry.writeLong(replicationBytesOffset, newTimestamp);
entry.writeByte(replicationBytesOffset + 8L, newIdentifier);
}
void writeDeleted() {
entry.writeBoolean(replicationBytesOffset + 9L, true);
}
void writePresent() {
entry.writeBoolean(replicationBytesOffset + 9L, false);
}
@Override
long sizeOfEverythingBeforeValue(long keySize, long valueSize) {
return super.sizeOfEverythingBeforeValue(keySize, valueSize) + ADDITIONAL_ENTRY_BYTES;
}
@Override
boolean put0() {
try {
if (!searchStatePresent()) {
putEntry();
writePresent();
} else {
if (!shouldIgnore()) {
boolean deleted = isDeleted();
initValueBytes();
putValue();
if (deleted) {
deleted(deleted() - 1);
writePresent();
}
} else {
return false;
}
}
return true;
} finally {
closeReplicationUpdate();
}
}
@Override
void beforeRelocation() {
rm().dropChange(segmentIndex, pos);
}
@Override
void initPutDependencies() {
super.initPutDependencies();
initReplicationUpdate();
}
@Override
void writeNewValueAndSwitch() {
super.writeNewValueAndSwitch();
writeReplicationBytes();
updateChange();
timestamp = newTimestamp;
identifier = newIdentifier;
}
class PseudoMetaBytesInterop implements MetaBytesInterop