/* * 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.replication.AbstractReplication; import net.openhft.chronicle.hash.replication.ReplicableEntry; import net.openhft.chronicle.hash.replication.TimeProvider; import net.openhft.chronicle.hash.serialization.internal.MetaBytesInterop; import net.openhft.chronicle.map.impl.*; import net.openhft.chronicle.map.replication.MapRemoteOperations; import net.openhft.chronicle.map.replication.MapReplicableEntry; import net.openhft.lang.Maths; import net.openhft.lang.collection.ATSDirectBitSet; import net.openhft.lang.collection.SingleThreadedDirectBitSet; import net.openhft.lang.io.Bytes; 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 */ public class ReplicatedChronicleMap, V, VI, MVI extends MetaBytesInterop, R> extends VanillaChronicleMap implements Replica, Replica.EntryExternalizable { // 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; public 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; // TODO init in builder public transient MapRemoteOperations remoteOperations = DefaultSpi.mapRemoteOperations(); transient CompiledReplicatedMapQueryContext remoteOpContext; transient CompiledReplicatedMapIterationContext remoteItContext; 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"); } } @Override void initQueryContext() { queryCxt = new ThreadLocal< CompiledReplicatedMapQueryContext>() { @Override protected CompiledReplicatedMapQueryContext initialValue() { return new CompiledReplicatedMapQueryContext<>(ReplicatedChronicleMap.this); } }; } @Override void initIterationContext() { iterCxt = new ThreadLocal< CompiledReplicatedMapIterationContext>() { @Override protected CompiledReplicatedMapIterationContext initialValue() { return new CompiledReplicatedMapIterationContext<>(ReplicatedChronicleMap.this); } }; } 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; } } public 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); } } } public 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); } } } @Override public boolean identifierCheck(@NotNull ReplicableEntry entry, int chronicleId) { return entry.originIdentifier() == localIdentifier; } @Override public int sizeOfEntry(@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(); if (identifier != localIdentifier) { // although unlikely, this may occur if the entry has been updated return 0; } entry.skip(1); // is Deleted long valueSize = valueSizeMarshaller.readSize(entry); alignment.alignPositionAddr(entry); long result = (entry.position() + valueSize - start); // entries can be larger than Integer.MAX_VALUE as we are restricted to the size we can // make a byte buffer assert result < Integer.MAX_VALUE; return (int) result; } finally { entry.position(start); } } /** * This method does not set a segment lock, A segment lock should be obtained before calling * this method, especially when being used in a multi threaded context. */ @Override public void writeExternalEntry(@NotNull Bytes entry, @NotNull Bytes destination, int chronicleId) { final long keySize = keySizeMarshaller.readSize(entry); final long keyPosition = entry.position(); entry.skip(keySize); final long timeStamp = entry.readLong(); final byte identifier = entry.readByte(); if (identifier != localIdentifier) { // although unlikely, this may occur if the entry has been updated return; } final boolean isDeleted = entry.readBoolean(); long valueSize; if (!isDeleted) { valueSize = valueSizeMarshaller.readSize(entry); } else { valueSize = valueSizeMarshaller.minEncodableSize(); } final long valuePosition = entry.position(); keySizeMarshaller.writeSize(destination, keySize); valueSizeMarshaller.writeSize(destination, valueSize); destination.writeStopBit(timeStamp); if (identifier == 0) throw new IllegalStateException("Identifier can't be 0"); destination.writeByte(identifier); destination.writeBoolean(isDeleted); // write the key entry.position(keyPosition); destination.write(entry, entry.position(), keySize); boolean debugEnabled = LOG.isDebugEnabled(); String message = null; if (debugEnabled) { if (isDeleted) { LOG.debug("WRITING ENTRY TO DEST - into local-id={}, remove(key={})", localIdentifier, entry.toString().trim()); } else { message = String.format( "WRITING ENTRY TO DEST - into local-id=%d, put(key=%s,", localIdentifier, entry.toString().trim()); } } if (isDeleted) return; entry.position(valuePosition); // skipping the alignment, as alignment wont work when we send the data over the wire. alignment.alignPositionAddr(entry); // writes the value destination.write(entry, entry.position(), valueSize); if (debugEnabled) { LOG.debug(message + "value=" + entry.toString().trim() + ")"); } } private CompiledReplicatedMapQueryContext q() { //noinspection unchecked return (CompiledReplicatedMapQueryContext) queryCxt.get(); } @Override public CompiledReplicatedMapQueryContext mapContext() { CompiledReplicatedMapQueryContext q = q().getContext(); q.initUsed(true); return q; } /** * Assumed to be called from a single thread - the replication thread. Not to waste time * for going into replication thread's threadLocal map, cache the context in Map's field */ private CompiledReplicatedMapQueryContext remoteOpContext() { if (remoteOpContext == null) { remoteOpContext = q(); } assert !remoteOpContext.usedInit(); remoteOpContext.initUsed(true); return remoteOpContext; } private CompiledReplicatedMapIterationContext remoteItContext() { if (remoteItContext == null) { remoteItContext = i(); } assert !remoteItContext.usedInit(); remoteItContext.initUsed(true); return remoteItContext; } /** * This method does not set a segment lock, A segment lock should be obtained before calling * this method, especially when being used in a multi threaded context. */ @Override public void readExternalEntry(@NotNull Bytes source) { try (CompiledReplicatedMapQueryContext remoteOpContext = mapContext()) { remoteOpContext.initReplicationInput(source); remoteOpContext.processReplicatedEvent(); } } private CompiledReplicatedMapIterationContext i() { //noinspection unchecked return (CompiledReplicatedMapIterationContext) iterCxt.get(); } public CompiledReplicatedMapIterationContext iterationContext() { CompiledReplicatedMapIterationContext c = i().getContext(); c.initUsed(true); return c; } /** *

Once a change occurs to a map, map replication requires that these changes are picked up * by another thread, this class provides an iterator like interface to poll for such changes. *

In most cases the thread that adds data to the node is unlikely to be the same thread * that replicates the data over to the other nodes, so data will have to be marshaled between * the main thread storing data to the map, and the thread running the replication.

One * way to perform this marshalling, would be to pipe the data into a queue. However, This class * takes another approach. It uses a bit set, and marks bits which correspond to the indexes of * the entries that have changed. It then provides an iterator like interface to poll for such * changes.

* * @author Rob Austin. */ class ModificationIterator implements Replica.ModificationIterator { private final ModificationNotifier modificationNotifier; private final SingleThreadedDirectBitSet changesForUpdates; // to getVolatile when reading changes bits, because we iterate when without lock. // hardly this is needed on x86, probably on other architectures too. // Anyway getVolatile is cheap. private final ATSDirectBitSet changesForIteration; private final int segmentIndexShift; private final long posMask; // records the current position of the cursor in the bitset private volatile long position = -1L; /** * @param bytes the back the bitset, used to mark which entries have changed * @param modificationNotifier called when ever there is a change applied */ public ModificationIterator(@NotNull final Bytes bytes, @NotNull final ModificationNotifier modificationNotifier) { this.modificationNotifier = modificationNotifier; long bitsPerSegment = bitsPerSegmentInModIterBitSet(); segmentIndexShift = Long.numberOfTrailingZeros(bitsPerSegment); posMask = bitsPerSegment - 1L; changesForUpdates = new SingleThreadedDirectBitSet(bytes); changesForIteration = new ATSDirectBitSet(bytes); } /** * used to merge multiple segments and positions into a single index used by the bit map * * @param segmentIndex the index of the maps segment * @param pos the position within this {@code segmentIndex} * @return and index the has combined the {@code segmentIndex} and {@code pos} into a * single value */ private long combine(long segmentIndex, long pos) { return (segmentIndex << segmentIndexShift) | pos; } void raiseChange(long segmentIndex, long pos) { LOG.debug("raise change: id {}, segment {}, pos {}", localIdentifier, segmentIndex, pos); changesForUpdates.set(combine(segmentIndex, pos)); modificationNotifier.onChange(); } void dropChange(long segmentIndex, long pos) { LOG.debug("drop change: id {}, segment {}, pos {}", localIdentifier, segmentIndex, pos); changesForUpdates.clear(combine(segmentIndex, pos)); } /** * you can continue to poll hasNext() until data becomes available. If are are in the middle * of processing an entry via {@code nextEntry}, hasNext will return true until the bit is * cleared * * @return true if there is an entry */ @Override public boolean hasNext() { final long position = this.position; return changesForIteration.nextSetBit(position == NOT_FOUND ? 0L : position) != NOT_FOUND || (position > 0L && changesForIteration.nextSetBit(0L) != NOT_FOUND); } /** * @param entryCallback call this to get an entry, this class will take care of the locking * @return true if an entry was processed */ @Override public boolean nextEntry(@NotNull EntryCallback entryCallback, int chronicleId) { long position = this.position; while (true) { long oldPosition = position; position = changesForIteration.nextSetBit(oldPosition + 1L); if (position == NOT_FOUND) { if (oldPosition == NOT_FOUND) { this.position = NOT_FOUND; return false; } continue; } this.position = position; int segmentIndex = (int) (position >>> segmentIndexShift); try (CompiledReplicatedMapIterationContext context = iterationContext()) { context.initTheSegmentIndex(segmentIndex); context.updateLock().lock(); if (changesForUpdates.get(position)) { entryCallback.onBeforeEntry(); final long segmentPos = position & posMask; context.readExistingEntry(segmentPos); // if the entry should be ignored, we'll move the next entry if (entryCallback.shouldBeIgnored(context, chronicleId)) { changesForUpdates.clear(position); continue; } // it may not be successful if the buffer can not be re-sized so we will // process it later, by NOT clearing the changes.clear(position) context.entryBytes().limit(context.valueOffset() + context.valueSize()); context.entryBytes().position(context.keySizeOffset()); boolean success = entryCallback.onEntry(context.entryBytes(), chronicleId); entryCallback.onAfterEntry(); if (success) changesForUpdates.clear(position); return success; } // if the position was already cleared by another thread // while we were trying to obtain segment lock (for example, in relocation()), // go to pick up next (next iteration in while (true) loop) } } } @Override public void dirtyEntries(long fromTimeStamp) { try (CompiledReplicatedMapIterationContext c = iterationContext()) { // iterate over all the segments and mark bit in the modification iterator // that correspond to entries with an older timestamp for (int i = 0; i < actualSegments; i++) { final int segmentIndex = i; c.initTheSegmentIndex(segmentIndex); c.forEachReplicableEntry(entry -> { LOG.debug("Bootstrap entry: id {}, key {}, value {}", localIdentifier, c.key(), c.value()); MapReplicableEntry re = (MapReplicableEntry) entry; assert re.originTimestamp() > 0L; if (re.originIdentifier() >= fromTimeStamp && (!bootstrapOnlyLocalEntries || re.originIdentifier() == localIdentifier)) { raiseChange(segmentIndex, c.pos()); } }); } } } } }