/* * 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.TimeProvider; import net.openhft.chronicle.map.serialization.BytesReader; import net.openhft.chronicle.map.serialization.MetaBytesInterop; import net.openhft.chronicle.map.serialization.MetaBytesWriter; import net.openhft.chronicle.map.threadlocal.ThreadLocalCopies; import net.openhft.lang.Maths; import net.openhft.lang.collection.ATSDirectBitSet; import net.openhft.lang.io.Bytes; import net.openhft.lang.io.MultiStoreBytes; import net.openhft.lang.io.NativeBytes; import net.openhft.lang.model.Byteable; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.*; import java.util.Iterator; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.atomic.AtomicReferenceArray; import static net.openhft.chronicle.map.serialization.Hasher.hash; 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, VW, MVW extends MetaBytesWriter> extends VanillaChronicleMap implements ChronicleMap, Replica, Replica.EntryExternalizable, Replica.EntryResolver, Closeable { // for file, jdbc and UDP replication public static final int RESERVED_MOD_ITER = 8; static final int MAX_UNSIGNED_SHORT = Character.MAX_VALUE; 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 static final int ADDITIONAL_ENTRY_BYTES = 10; private final TimeProvider timeProvider; private final byte localIdentifier; transient Set closeables; private transient Bytes identifierUpdatedBytes; private transient ModificationDelegator modificationDelegator; private transient long startOfModificationIterators; public ReplicatedChronicleMap(@NotNull ChronicleMapBuilder builder) throws IOException { super(builder); this.timeProvider = builder.timeProvider(); this.localIdentifier = builder.identifier(); if (localIdentifier==-1){ throw new IllegalStateException("localIdentifier should not be -1"); } } private int assignedModIterBitSetSizeInBytes() { return (int) align64((127 + RESERVED_MOD_ITER) / 8); } @Override Segment createSegment(NativeBytes bytes, int index) { return new Segment(bytes, index); } Class segmentType() { return Segment.class; } @Override void initTransients() { super.initTransients(); closeables = new CopyOnWriteArraySet(); } long modIterBitSetSizeInBytes() { return align64(bitsPerSegmentInModIterBitSet() * segments.length / 8); } private long bitsPerSegmentInModIterBitSet() { // min 128 * 8 to prevent false sharing on updating bits from different segments return Maths.nextPower2((long) entriesPerSegment, 128 * 8); } @Override int multiMapsPerSegment() { return 2; } @Override long getHeaderSize() { return super.getHeaderSize() + 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 void onHeaderCreated() { long offset = super.getHeaderSize(); 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; modificationDelegator = new ModificationDelegator(eventListener, modDelBytes); } @Override MapEventListener> eventListener() { return modificationDelegator; } @Override public V put(K key, V value) { return put0(key, value, true, localIdentifier, timeProvider.currentTimeMillis()); } /** * Used in conjunction with map replication, all put events that originate from a remote node will be * processed using this method. * * @param key key with which the specified value is to be associated * @param value value to be associated with the specified key * @param identifier a unique identifier for a replicating node * @param timeStamp timestamp in milliseconds, when the put event originally occurred * @return the previous value * @see #put(Object, Object) */ V put(K key, V value, byte identifier, long timeStamp) { assert identifier > 0; return put0(key, value, true, identifier, timeStamp); } @Override public V putIfAbsent(@net.openhft.lang.model.constraints.NotNull K key, V value) { return put0(key, value, false, localIdentifier, timeProvider.currentTimeMillis()); } /** * @param key key with which the specified value is associated * @param value value expected to be associated with the specified key * @param replaceIfPresent set to false for putIfAbsent() * @param identifier used to identify which replicating node made the change * @param timeStamp the time that that change was made, this is used for replication * @return the value that was replaced */ private V put0(K key, V value, boolean replaceIfPresent, final byte identifier, long timeStamp) { checkKey(key); checkValue(value); ThreadLocalCopies copies = keyInteropProvider.getCopies(null); KI keyWriter = keyInteropProvider.get(copies, originalKeyInterop); copies = metaKeyInteropProvider.getCopies(copies); MKI metaKeyWriter = metaKeyInteropProvider.get(copies, originalMetaKeyInterop, keyWriter, key); long hash = metaKeyWriter.hash(keyWriter, key); int segmentNum = getSegment(hash); int segmentHash = segmentHash(hash); return segment(segmentNum).put(copies, metaKeyWriter, keyWriter, key, value, segmentHash, replaceIfPresent, identifier, timeStamp); } /** * @param segmentNum a unique index of the segment * @return the segment associated with the {@code segmentNum} */ private Segment segment(int segmentNum) { return (Segment) segments[segmentNum]; } @Override V lookupUsing(K key, V value, boolean create) { checkKey(key); ThreadLocalCopies copies = keyInteropProvider.getCopies(null); KI keyWriter = keyInteropProvider.get(copies, originalKeyInterop); copies = metaKeyInteropProvider.getCopies(copies); MKI metaKeyWriter = metaKeyInteropProvider.get(copies, originalMetaKeyInterop, keyWriter, key); long hash = metaKeyWriter.hash(keyWriter, key); int segmentNum = getSegment(hash); int segmentHash = segmentHash(hash); return segment(segmentNum).acquire(copies, metaKeyWriter, keyWriter, key, value, segmentHash, create, timeProvider.currentTimeMillis()); } @Override public V remove(final Object key) { return removeIfValueIs(key, null, localIdentifier, timeProvider.currentTimeMillis()); } /** * Used in conjunction with map replication, all remove events that originate from a remote node will be * processed using this method. * * @param key key with which the specified value is associated * @param value value expected to be associated with the specified key * @param identifier a unique identifier for a replicating node * @param timeStamp timestamp in milliseconds, when the remove event originally occurred * @return {@code true} if the entry was removed * @see #remove(Object, Object) */ V remove(K key, V value, byte identifier, long timeStamp) { assert identifier > 0; return removeIfValueIs(key, value, identifier, timeStamp); } void addCloseable(Closeable closeable) { closeables.add(closeable); } @Override public void close() { for (Closeable closeable : closeables) { try { closeable.close(); } catch (IOException e) { LOG.error("", e); } } try { // give time for stuff to fully close Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } super.close(); } @Override public byte identifier() { return localIdentifier; } @Override public Replica.ModificationIterator acquireModificationIterator (short remoteIdentifier, @NotNull final ModificationNotifier modificationNotifier) { return modificationDelegator .acquireModificationIterator(remoteIdentifier, modificationNotifier); } @Override public boolean remove(@net.openhft.lang.model.constraints.NotNull final Object key, final Object value) { if (value == null) return false; // CHM compatibility; I would throw NPE return removeIfValueIs(key, (V) value, localIdentifier, timeProvider.currentTimeMillis()) != null; } private V removeIfValueIs(final Object k, final V expectedValue, final byte identifier, final long timestamp) { checkKey(k); K key = (K) k; ThreadLocalCopies copies = keyInteropProvider.getCopies(null); KI keyWriter = keyInteropProvider.get(copies, originalKeyInterop); copies = metaKeyInteropProvider.getCopies(copies); MKI metaKeyWriter = metaKeyInteropProvider.get(copies, originalMetaKeyInterop, keyWriter, key); long hash = metaKeyWriter.hash(keyWriter, key); int segmentNum = getSegment(hash); int segmentHash = segmentHash(hash); return segment(segmentNum).remove(copies, metaKeyWriter, keyWriter, key, expectedValue, segmentHash, timestamp, identifier); } @Override V replaceIfValueIs(@net.openhft.lang.model.constraints.NotNull final K key, final V existingValue, final V newValue) { checkKey(key); checkValue(newValue); ThreadLocalCopies copies = keyInteropProvider.getCopies(null); KI keyWriter = keyInteropProvider.get(copies, originalKeyInterop); copies = metaKeyInteropProvider.getCopies(copies); MKI metaKeyWriter = metaKeyInteropProvider.get(copies, originalMetaKeyInterop, keyWriter, key); long hash = metaKeyWriter.hash(keyWriter, key); int segmentNum = getSegment(hash); int segmentHash = segmentHash(hash); return segment(segmentNum).replace(copies, metaKeyWriter, keyWriter, key, existingValue, newValue, segmentHash, timeProvider.currentTimeMillis()); } /** * 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 initialLimit = entry.limit(); final long keySize = keySizeMarshaller.readSize(entry); final long keyPosition = entry.position(); entry.skip(keySize); final long keyLimit = entry.position(); 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 = 0; } final long valuePosition = entry.position(); keySizeMarshaller.writeSize(destination, keySize); valueSizeMarshaller.writeSize(destination, valueSize); destination.writeStopBit(timeStamp); // we store the isDeleted flag in the identifier // ( when the identifier is negative it is deleted ) destination.writeByte(isDeleted ? -identifier : identifier); // write the key entry.position(keyPosition); entry.limit(keyLimit); destination.write(entry); 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.limit(initialLimit); entry.position(valuePosition); // skipping the alignment, as alignment wont work when we send the data over the wire. alignment.alignPositionAddr(entry); // writes the value entry.limit(entry.position() + valueSize); destination.write(entry); if (debugEnabled) { LOG.debug(message + "value=" + entry.toString().trim() + ")"); } } /** * 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) { final long keySize = keySizeMarshaller.readSize(source); final long valueSize = valueSizeMarshaller.readSize(source); final long timeStamp = source.readStopBit(); final byte id = source.readByte(); final byte remoteIdentifier; final boolean isDeleted; if (id < 0) { isDeleted = true; remoteIdentifier = (byte) -id; } else if (id != 0) { isDeleted = false; remoteIdentifier = id; } else { throw new IllegalStateException("identifier can't be 0"); } if (remoteIdentifier == ReplicatedChronicleMap.this.identifier()) { // this may occur when working with UDP, as we will receive our own data return; } final long keyPosition = source.position(); final long keyLimit = keyPosition + keySize; source.limit(keyLimit); long hash = hash(source); int segmentNum = getSegment(hash); int segmentHash = segmentHash(hash); boolean debugEnabled = LOG.isDebugEnabled(); if (isDeleted) { if (debugEnabled) { LOG.debug( "READING FROM SOURCE - into local-id={}, remote={}, remove(key={})", localIdentifier, remoteIdentifier, source.toString().trim() ); } segment(segmentNum).remoteRemove(source, segmentHash, timeStamp, remoteIdentifier); setLastModificationTime(remoteIdentifier, timeStamp); return; } String message = null; if (debugEnabled) { message = String.format( "READING FROM SOURCE - into local-id=%d, remote-id=%d, put(key=%s,", localIdentifier, remoteIdentifier, source.toString().trim()); } final long valuePosition = keyLimit; final long valueLimit = valuePosition + valueSize; segment(segmentNum).remotePut(source, segmentHash, remoteIdentifier, timeStamp, valuePosition, valueLimit); setLastModificationTime(remoteIdentifier, timeStamp); if (debugEnabled) { source.limit(valueLimit); source.position(valuePosition); LOG.debug(message + "value=" + source.toString().trim() + ")"); } } @NotNull @Override public Set> entrySet() { return new EntrySet(); } @Override public K key(@NotNull Bytes entry, K usingKey) { final long start = entry.position(); try { long keySize = keySizeMarshaller.readSize(entry); ThreadLocalCopies copies = keyReaderProvider.getCopies(null); return keyReaderProvider.get(copies, originalKeyReader).read(entry, keySize); } finally { entry.position(start); } } @Override public V value(@NotNull Bytes entry, V usingValue) { final long start = entry.position(); try { entry.skip(keySizeMarshaller.readSize(entry)); //timeStamp entry.readLong(); final byte identifier = entry.readByte(); if (identifier != localIdentifier) { return null; } final boolean isDeleted = entry.readBoolean(); long valueSize; if (!isDeleted) { valueSize = valueSizeMarshaller.readSize(entry); assert valueSize > 0; } else { return null; } alignment.alignPositionAddr(entry); ThreadLocalCopies copies = valueReaderProvider.getCopies(null); BytesReader valueReader = valueReaderProvider.get(copies, originalValueReader); return valueReader.read(entry, valueSize, usingValue); } finally { entry.position(start); } } @Override public boolean wasRemoved(@NotNull Bytes entry) { final long start = entry.position(); try { return entry.readBoolean(keySizeMarshaller.readSize(entry) + 9); } finally { entry.position(start); } } class Segment extends VanillaChronicleMap.Segment { private volatile IntIntMultiMap hashLookupLiveAndDeleted; private volatile IntIntMultiMap hashLookupLiveOnly; Segment(NativeBytes bytes, int index) { super(bytes, index); } @Override void createHashLookups(long start) { hashLookupLiveAndDeleted = createMultiMap(start); start += align64(sizeOfMultiMap() + sizeOfMultiMapBitSet()); hashLookupLiveOnly = createMultiMap(start); } public IntIntMultiMap getHashLookup() { return hashLookupLiveOnly; } private long entrySize(long keySize, long valueSize) { long result = alignment.alignAddr(metaDataBytes + keySizeMarshaller.sizeEncodingSize(keySize) + keySize + ADDITIONAL_ENTRY_BYTES + valueSizeMarshaller.sizeEncodingSize(valueSize)) + valueSize; // replication enforces that the entry size will never be larger than an unsigned short if (result > MAX_UNSIGNED_SHORT) throw new IllegalStateException("ENTRY WRITE_BUFFER_SIZE TOO LARGE : Replicated " + "ChronicleMap's" + " are restricted to an " + "entry size of " + MAX_UNSIGNED_SHORT + ", " + "your entry size=" + result); return result; } /** * @see VanillaChronicleMap.Segment#acquire */ V acquire(ThreadLocalCopies copies, MKI metaKeyWriter, KI keyWriter, K key, V usingValue, int hash2, boolean create, long timestamp) { lock(); try { long keySize = metaKeyWriter.size(keyWriter, key); MultiStoreBytes entry = tmpBytes; long offset = searchKey(keyWriter, metaKeyWriter, key, keySize, hash2, entry, hashLookupLiveOnly); if (offset >= 0) { // skip the timestamp, identifier and is deleted flag entry.skip(ADDITIONAL_ENTRY_BYTES); return onKeyPresentOnAcquire(copies, key, usingValue, offset, entry); } else { boolean usingValuePassed = usingValue != null; usingValue = tryObtainUsingValueOnAcquire(key, usingValue, create); if (usingValue != null) { // see VanillaChronicleMap.Segment.acquire() for explanation // why `usingValue` is `create`. offset = putEntryOnAcquire(copies, metaKeyWriter, keyWriter, key, keySize, hash2, usingValue, create, timestamp); incrementSize(); if (usingValuePassed || !create) notifyPut(offset, true, key, usingValue, posFromOffset(offset)); return usingValue; } else { return null; } } } finally { unlock(); } } private long putEntryOnAcquire(ThreadLocalCopies copies, MKI metaKeyWriter, KI keyWriter, K key, long keySize, int hash2, V value, boolean usingValue, long timestamp) { return putEntry(copies, metaKeyWriter, keyWriter, key, keySize, hash2, value, usingValue, localIdentifier, timestamp, hashLookupLiveOnly); } /** * called from a remote node as part of replication */ private void remoteRemove(Bytes keyBytes, int hash2, final long timestamp, final byte identifier) { lock(); try { long keySize = keyBytes.remaining(); hashLookupLiveAndDeleted.startSearch(hash2); for (int pos; (pos = hashLookupLiveAndDeleted.nextPos()) >= 0; ) { long offset = offsetFromPos(pos); NativeBytes entry = entry(offset); if (!remoteKeyEquals(keyBytes, keySize, entry)) continue; // key is found entry.skip(keySize); long timeStampPos = entry.position(); if (shouldIgnore(entry, timestamp, identifier)) { return; } // skip the is deleted flag boolean wasDeleted = entry.readBoolean(); if (!wasDeleted) { hashLookupLiveOnly.remove(hash2, pos); decrementSize(); } entry.position(timeStampPos); entry.writeLong(timestamp); assert identifier > 0; entry.writeByte(identifier); // was deleted entry.writeBoolean(true); } // key is not found if (LOG.isDebugEnabled()) LOG.debug("Segment.remoteRemove() : key=" + keyBytes.toString().trim() + " was not " + "found"); } finally { unlock(); } } /** * called from a remote node when it wishes to propagate a remove event */ private void remotePut(@NotNull final Bytes inBytes, int hash2, final byte identifier, final long timestamp, long valuePos, long valueLimit) { lock(); try { // inBytes position and limit correspond to the key final long keySize = inBytes.remaining(); hashLookupLiveAndDeleted.startSearch(hash2); for (int pos; (pos = hashLookupLiveAndDeleted.nextPos()) >= 0; ) { long offset = offsetFromPos(pos); NativeBytes entry = entry(offset); if (!remoteKeyEquals(inBytes, keySize, entry)) continue; // key is found entry.skip(keySize); final long timeStampPos = entry.positionAddr(); if (shouldIgnore(entry, timestamp, identifier)) { return; } boolean wasDeleted = entry.readBoolean(); entry.positionAddr(timeStampPos); entry.writeLong(timestamp); assert identifier > 0; entry.writeByte(identifier); // deleted flag entry.writeBoolean(false); long valueSizePos = entry.position(); long valueSize = readValueSize(entry); long entryEndAddr = entry.positionAddr() + valueSize; // write the value inBytes.limit(valueLimit); inBytes.position(valuePos); putValue(pos, offset, entry, valueSizePos, entryEndAddr, null, null, inBytes, hashLookupLiveAndDeleted); if (wasDeleted) { // remove() would have got rid of this so we have to add it back in hashLookupLiveOnly.put(hash2, pos); incrementSize(); } return; } // key is not found long valueSize = valueLimit - valuePos; int pos = alloc(inBlocks(entrySize(keySize, valueSize))); long offset = offsetFromPos(pos); clearMetaData(offset); NativeBytes entry = entry(offset); keySizeMarshaller.writeSize(entry, keySize); // write the key entry.write(inBytes); entry.writeLong(timestamp); entry.writeByte(identifier); entry.writeBoolean(false); valueSizeMarshaller.writeSize(entry, valueSize); alignment.alignPositionAddr(entry); // write the value inBytes.limit(valueLimit); inBytes.position(valuePos); entry.write(inBytes); hashLookupLiveAndDeleted.putAfterFailedSearch(pos); hashLookupLiveOnly.put(hash2, pos); incrementSize(); } finally { unlock(); } } private boolean remoteKeyEquals(Bytes keyBytes, long keySize, Bytes entry) { return keySize == keySizeMarshaller.readSize(entry) && entry.startsWith(keyBytes); } V put(ThreadLocalCopies copies, MKI metaKeyWriter, KI keyWriter, K key, V value, int hash2, boolean replaceIfPresent, final byte identifier, final long timestamp) { lock(); try { IntIntMultiMap hashLookup = hashLookupLiveAndDeleted; long keySize = metaKeyWriter.size(keyWriter, key); hashLookup.startSearch(hash2); int pos; while ((pos = hashLookup.nextPos()) >= 0) { long offset = offsetFromPos(pos); NativeBytes entry = entry(offset); if (!keyEquals(keyWriter, metaKeyWriter, key, keySize, entry)) continue; // key is found entry.skip(keySize); final long timeStampPos = entry.positionAddr(); if (shouldIgnore(entry, timestamp, identifier)) return null; boolean wasDeleted = entry.readBoolean(); // if wasDeleted==true then we even if replaceIfPresent==false we can treat it // the same way as replaceIfPresent true if (replaceIfPresent || wasDeleted) { entry.positionAddr(timeStampPos); entry.writeLong(timestamp); entry.writeByte(identifier); // deleted flag entry.writeBoolean(false); final V prevValue = replaceValueOnPut(copies, key, value, entry, pos, offset, !wasDeleted && !putReturnsNull, hashLookup); if (wasDeleted) { // remove() would have got rid of this so we have to add it back in hashLookupLiveOnly.put(hash2, pos); incrementSize(); return null; } else { return prevValue; } } else { // we wont replaceIfPresent and the entry is not deleted return putReturnsNull ? null : readValue(copies, entry, null); } } // key is not found long offset = putEntry(copies, metaKeyWriter, keyWriter, key, keySize, hash2, value, false, identifier, timestamp, hashLookup); incrementSize(); notifyPut(offset, true, key, value, posFromOffset(offset)); return null; } finally { unlock(); } } /** * Used only with replication, its sometimes possible to receive an old ( or stale update ) from a * remote map. This method is used to determine if we should ignore such updates.

We can * reject put() and removes() when comparing times stamps with remote systems * * @param entry the maps entry * @param timestamp the time the entry was created or updated * @param identifier the unique identifier relating to this map * @return true if the entry should not be processed */ private boolean shouldIgnore(@NotNull final NativeBytes entry, final long timestamp, final byte identifier) { final long lastModifiedTimeStamp = entry.readLong(); // if the readTimeStamp is newer then we'll reject this put() // or they are the same and have a larger id if (lastModifiedTimeStamp < timestamp) { entry.skip(1); // skip the byte used for the identifier return false; } if (lastModifiedTimeStamp > timestamp) return true; // check the identifier return entry.readByte() > identifier; } /** * Puts entry. If {@code value} implements {@link Byteable} interface and {@code usingValue} is {@code * true}, the value is backed with the bytes of this entry. * * @param hash2 a hash of the {@code keyBytes}. Caller was searching for the key in the * {@code searchedHashLookup} using this hash. * @param value the value to put * @param usingValue {@code true} if the value should be backed with the bytes of the entry, * if it implements {@link Byteable} interface, {@code false} if it should * put itself * @param identifier the identifier of the outer CHM node * @param timestamp the timestamp when the entry was put (this could be later if it was a * remote put) this method is called only from usual put or acquire * @param searchedHashLookup the hash lookup that used to find the entry based on the key * @return offset of the written entry in the Segment bytes * @see VanillaChronicleMap.Segment#putEntry */ private long putEntry(ThreadLocalCopies copies, MKI metaKeyWriter, KI keyWriter, K key, long keySize, int hash2, V value, boolean usingValue, final int identifier, final long timestamp, IntIntMultiMap searchedHashLookup) { // "if-else polymorphism" is not very beautiful, but allows to // reuse the rest code of this method and doesn't hurt performance. boolean byteableValue = usingValue && value instanceof Byteable; long valueSize; MetaBytesWriter metaValueWriter = null; VW valueWriter = null; Byteable valueAsByteable = null; if (!byteableValue) { copies = valueWriterProvider.getCopies(copies); valueWriter = valueWriterProvider.get(copies, originalValueWriter); copies = metaValueWriterProvider.getCopies(copies); metaValueWriter = metaValueWriterProvider.get( copies, originalMetaValueWriter, valueWriter, value); valueSize = metaValueWriter.size(valueWriter, value); } else { valueAsByteable = (Byteable) value; valueSize = valueAsByteable.maxSize(); } long entrySize = entrySize(keySize, valueSize); int pos = alloc(inBlocks(entrySize)); long offset = offsetFromPos(pos); clearMetaData(offset); NativeBytes entry = entry(offset); keySizeMarshaller.writeSize(entry, keySize); metaKeyWriter.write(keyWriter, entry, key); entry.writeLong(timestamp); entry.writeByte(identifier); entry.writeBoolean(false); writeValueOnPutEntry(valueSize, metaValueWriter, valueWriter, value, valueAsByteable, entry); // we have to add it both to the live if (searchedHashLookup == hashLookupLiveAndDeleted) { hashLookupLiveAndDeleted.putAfterFailedSearch(pos); hashLookupLiveOnly.put(hash2, pos); } else { hashLookupLiveOnly.putAfterFailedSearch(pos); hashLookupLiveAndDeleted.put(hash2, pos); } return offset; } /** * @see VanillaChronicleMap.Segment#remove */ V remove(ThreadLocalCopies copies, MKI metaKeyWriter, KI keyWriter, K key, V expectedValue, int hash2, final long timestamp, final byte identifier) { assert identifier > 0; lock(); try { long keySize = metaKeyWriter.size(keyWriter, key); final IntIntMultiMap multiMap = hashLookupLiveAndDeleted; multiMap.startSearch(hash2); for (int pos; (pos = multiMap.nextPos()) >= 0; ) { long offset = offsetFromPos(pos); NativeBytes entry = entry(offset); if (!keyEquals(keyWriter, metaKeyWriter, key, keySize, entry)) continue; // key is found entry.skip(keySize); long timeStampPos = entry.position(); if (shouldIgnore(entry, timestamp, identifier)) { return null; } // skip the is deleted flag final boolean wasDeleted = entry.readBoolean(); if (wasDeleted) { // this caters for the case when the entry in not in our hashLookupLiveOnly // map but maybe in our hashLookupLiveAndDeleted, // so we have to send the deleted notification entry.position(timeStampPos); entry.writeLong(timestamp); entry.writeByte(identifier); // was deleted is already true entry.skip(1); notifyRemoved(offset, key, null, pos); return null; } long valueSize = readValueSize(entry); V valueRemoved = expectedValue != null || !removeReturnsNull ? readValue(copies, entry, null, valueSize) : null; if (expectedValue != null && !expectedValue.equals(valueRemoved)) return null; hashLookupLiveOnly.remove(hash2, pos); decrementSize(); entry.position(timeStampPos); entry.writeLong(timestamp); entry.writeByte(identifier); entry.writeBoolean(true); notifyRemoved(offset, key, valueRemoved, pos); return valueRemoved; } // key is not found return null; } finally { unlock(); } } @Override IntIntMultiMap containsKeyHashLookup() { return hashLookupLiveOnly; } /** * @see VanillaChronicleMap.Segment#replace */ V replace(ThreadLocalCopies copies, MKI metaKeyWriter, KI keyWriter, K key, V expectedValue, V newValue, int hash2, long timestamp) { lock(); try { long keySize = metaKeyWriter.size(keyWriter, key); hashLookupLiveOnly.startSearch(hash2); for (int pos; (pos = hashLookupLiveOnly.nextPos()) >= 0; ) { long offset = offsetFromPos(pos); NativeBytes entry = entry(offset); if (!keyEquals(keyWriter, metaKeyWriter, key, keySize, entry)) continue; // key is found entry.skip(keySize); if (shouldIgnore(entry, timestamp, localIdentifier)) return null; // skip the is deleted flag entry.skip(1); return onKeyPresentOnReplace(copies, key, expectedValue, newValue, pos, offset, entry, hashLookupLiveOnly); } // key is not found return null; } finally { unlock(); } } @Override void replacePosInHashLookupOnRelocation(IntIntMultiMap searchedHashLookup, int prevPos, int pos) { searchedHashLookup.replacePrevPos(pos); int hash = searchedHashLookup.getSearchHash(); IntIntMultiMap anotherLookup = searchedHashLookup == hashLookupLiveAndDeleted ? hashLookupLiveOnly : hashLookupLiveAndDeleted; anotherLookup.replace(hash, prevPos, pos); } public void dirtyEntries(final long timeStamp, final ModificationIterator.EntryModifiableCallback callback) { this.lock(); try { final int index = Segment.this.getIndex(); hashLookupLiveAndDeleted.forEach(new IntIntMultiMap.EntryConsumer() { @Override public void accept(int hash, int pos) { final NativeBytes entry = entry(offsetFromPos(pos)); long keySize = keySizeMarshaller.readSize(entry); entry.skip(keySize); final long entryTimestamp = entry.readLong(); if (entryTimestamp >= timeStamp && entry.readByte() == ReplicatedChronicleMap.this.identifier()) callback.set(index, pos); } }); } finally { unlock(); } } @Override public Entry getEntry(long pos) { bytes.position(offsetFromPos(pos) + metaDataBytes); long keySize = keySizeMarshaller.readSize(bytes); ThreadLocalCopies copies = keyReaderProvider.getCopies(null); K key = keyReaderProvider.get(copies, originalKeyReader).read(bytes, keySize); long timestamp = bytes.readLong(); bytes.skip(2L); // identifier and isDeleted flag long valueSize = valueSizeMarshaller.readSize(bytes); alignment.alignPositionAddr(bytes); copies = valueReaderProvider.getCopies(copies); V value = valueReaderProvider.get(copies, originalValueReader).read(bytes, valueSize); return new TimestampTrackingEntry(key, value, timestamp); } @Override void clear() { // we have to make sure that every calls notifies on remove, // so that the replicators can pick it up for (K k : keySet()) { ReplicatedChronicleMap.this.remove(k); } } } class TimestampTrackingEntry extends SimpleEntry { private static final long serialVersionUID = 0L; transient long timestamp; public TimestampTrackingEntry(K key, V value, long timestamp) { super(key, value); this.timestamp = timestamp; } @Override public V setValue(V value) { long newTimestamp = timestamp = timeProvider.currentTimeMillis(); put(getKey(), value, localIdentifier, newTimestamp); return super.setValue(value); } } class EntryIterator extends VanillaChronicleMap.EntryIterator { @Override void removePresent(VanillaChronicleMap.Segment seg, int pos) { @SuppressWarnings("unchecked") Segment segment = (Segment) seg; final long offset = segment.offsetFromPos(pos); final NativeBytes entry = segment.entry(offset); final long limit = entry.limit(); final long keySize = keySizeMarshaller.readSize(entry); long keyPosition = entry.position(); entry.skip(keySize); long timestamp = entry.readLong(); entry.position(keyPosition); if (timestamp > ((TimestampTrackingEntry) returnedEntry).timestamp) { // The entry was updated after being returned from iterator.next() // Check that it is still the entry with the same key K key = returnedEntry.getKey(); ThreadLocalCopies copies = keyInteropProvider.getCopies(null); KI keyWriter = keyInteropProvider.get(copies, originalKeyInterop); copies = metaKeyInteropProvider.getCopies(copies); MKI metaKeyWriter = metaKeyInteropProvider.get(copies, originalMetaKeyInterop, keyWriter, key); long returnedKeySize = metaKeyWriter.size(keyWriter, key); if (returnedKeySize != keySize || !metaKeyWriter.startsWith(keyWriter, entry, key)) { // The case: // 1. iterator.next() - thread 1 // 2. map.put() which cause relocation of the key, returned above - thread 2 // OR map.remove() which remove this key - thread 2 // 3. map.put() which place a new key on the `pos` in current segment - thread 3 // 4. iterator.remove() - thread 1 ReplicatedChronicleMap.this.remove(key); return; } } final int segmentHash = segmentHash(hash(entry, keyPosition, keyPosition + keySize)); segment.hashLookupLiveOnly.remove(segmentHash, pos); segment.decrementSize(); entry.skip(keySize); entry.writeLong(timeProvider.currentTimeMillis()); entry.writeByte(localIdentifier); entry.writeBoolean(true); segment.notifyRemoved(offset, returnedEntry.getKey(), returnedEntry.getValue(), pos); } } class EntrySet extends VanillaChronicleMap.EntrySet { @NotNull @Override public Iterator> iterator() { return new EntryIterator(); } } /** * receive an update from the map, via the MapEventListener and delegates the changes to the currently * active modification iterators */ class ModificationDelegator extends MapEventListener> { private static final long serialVersionUID = 0L; // the assigned modification iterators private final ATSDirectBitSet modIterSet; private final MapEventListener> nextListener; private final AtomicReferenceArray modificationIterators = new AtomicReferenceArray(127 + RESERVED_MOD_ITER); public ModificationDelegator(@NotNull final MapEventListener> nextListener, final Bytes bytes) { this.nextListener = nextListener; modIterSet = new ATSDirectBitSet(bytes); } public ModificationIterator acquireModificationIterator( final short remoteIdentifier, @NotNull final ModificationNotifier modificationNotifier) { final ModificationIterator modificationIterator = modificationIterators.get(remoteIdentifier); if (modificationIterator != null) return modificationIterator; synchronized (modificationIterators) { final ModificationIterator modificationIterator0 = modificationIterators.get(remoteIdentifier); if (modificationIterator0 != null) return modificationIterator0; 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; } } @Override public void onPut(ChronicleMap map, Bytes entry, int metaDataBytes, boolean added, K key, V value, long pos, SharedSegment segment) { assert ReplicatedChronicleMap.this == map : "ModificationIterator.onPut() is called from outside of the parent map"; try { nextListener.onPut(map, entry, metaDataBytes, added, key, value, pos, segment); } catch (Exception e) { LOG.error("", e); } for (long next = modIterSet.nextSetBit(0); next > 0; next = modIterSet.nextSetBit(next + 1)) { try { final ModificationIterator modificationIterator = modificationIterators.get((int) next); modificationIterator.onPut(map, entry, metaDataBytes, added, key, value, pos, segment); } catch (Exception e) { LOG.error("", e); } } } @Override public void onRemove(ChronicleMap map, Bytes entry, int metaDataBytes, K key, V value, int pos, SharedSegment segment) { assert ReplicatedChronicleMap.this == map : "ModificationIterator.onRemove() is called from outside of the parent map"; try { nextListener.onRemove(map, entry, metaDataBytes, key, value, pos, segment); } catch (Exception e) { LOG.error("", e); } for (long next = modIterSet.nextSetBit(0); next > 0; next = modIterSet.nextSetBit(next + 1)) { try { final ModificationIterator modificationIterator = modificationIterators.get((int) next); modificationIterator.onRemove(map, entry, metaDataBytes, key, value, pos, segment); } catch (Exception e) { LOG.error("", e); } } } @Override void onRelocation(int pos, SharedSegment segment) { for (long next = modIterSet.nextSetBit(0); next > 0; next = modIterSet.nextSetBit(next + 1)) { try { final ModificationIterator modificationIterator = modificationIterators.get((int) next); modificationIterator.onRelocation(pos, segment); } catch (Exception e) { LOG.error("", e); } } } @Override public void onGetFound(ChronicleMap map, Bytes entry, int metaDataBytes, K key, V value) { nextListener.onGetFound(map, entry, metaDataBytes, key, value); } /** * Always throws {@code NotSerializableException} since instances of this class are not intended to be * serializable. */ private void writeObject(ObjectOutputStream out) throws IOException { throw new NotSerializableException(getClass().getCanonicalName()); } /** * Always throws {@code NotSerializableException} since instances of this class are not intended to be * serializable. */ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { throw new NotSerializableException(getClass().getCanonicalName()); } } /** * 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 extends MapEventListener> implements Replica.ModificationIterator { private static final long serialVersionUID = 0L; private final ModificationNotifier modificationNotifier; private final ATSDirectBitSet changes; private final int segmentIndexShift; private final long posMask; private final EntryModifiableCallback entryModifiableCallback = new EntryModifiableCallback(); // records the current position of the cursor in the bitset private long position = -1; /** * @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 - 1; changes = 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(int segmentIndex, long pos) { return (((long) segmentIndex) << segmentIndexShift) | pos; } @Override public void onPut(ChronicleMap map, Bytes entry, int metaDataBytes, boolean added, K key, V value, long pos, SharedSegment segment) { assert ReplicatedChronicleMap.this == map : "ModificationIterator.onPut() is called from outside of the parent map"; changes.set(combine(segment.getIndex(), pos)); modificationNotifier.onChange(); } @Override public void onRemove(ChronicleMap map, Bytes entry, int metaDataBytes, K key, V value, int pos, SharedSegment segment) { assert ReplicatedChronicleMap.this == map : "ModificationIterator.onRemove() is called from outside of the parent map"; changes.set(combine(segment.getIndex(), pos)); modificationNotifier.onChange(); } /** * Ensures that garbage in the old entry's location won't be broadcast as changed entry. */ @Override void onRelocation(int pos, SharedSegment segment) { changes.clear(combine(segment.getIndex(), pos)); // don't call nextListener.onRelocation(), // because no one event listener else overrides this method. } /** * 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 */ public boolean hasNext() { final long position = this.position; return changes.nextSetBit(position == NOT_FOUND ? 0 : position) != NOT_FOUND || (position > 0 && changes.nextSetBit(0) != 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 */ public boolean nextEntry(@NotNull final EntryCallback entryCallback, final int chronicleId) { long position = this.position; while (true) { long oldPosition = position; position = changes.nextSetBit(oldPosition + 1); if (position == NOT_FOUND) { if (oldPosition == NOT_FOUND) { this.position = NOT_FOUND; return false; } continue; } this.position = position; final SharedSegment segment = segment((int) (position >>> segmentIndexShift)); segment.lock(); try { if (changes.clearIfSet(position)) { entryCallback.onBeforeEntry(); final long segmentPos = position & posMask; final NativeBytes entry = segment.entry(segment.offsetFromPos(segmentPos)); // if the entry should be ignored, we'll move the next entry final boolean success = entryCallback.onEntry(entry, chronicleId); entryCallback.onAfterEntry(); if (success) { return true; } } // if the position was already cleared by another thread // while we were trying to obtain segment lock (for example, in onReplication()), // go to pick up next (next iteration in while (true) loop) } finally { segment.unlock(); } } } @Override public void dirtyEntries(long fromTimeStamp) { // iterate over all the segments and mark bit in the modification iterator // that correspond to entries with an older timestamp for (final Segment segment : (Segment[]) segments) { segment.dirtyEntries(fromTimeStamp, entryModifiableCallback); } } /** * Always throws {@code NotSerializableException} since instances of this class are not intended to be * serializable. */ private void writeObject(ObjectOutputStream out) throws IOException { throw new NotSerializableException(getClass().getCanonicalName()); } /** * Always throws {@code NotSerializableException} since instances of this class are not intended to be * serializable. */ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { throw new NotSerializableException(getClass().getCanonicalName()); } /** * details about when a modification to an entry was made */ class EntryModifiableCallback { /** * set the bit related to {@code segment} and {@code pos} * * @param segmentIndex the segment relating to the bit to set * @param pos the position relating to the bit to set */ public void set(int segmentIndex, int pos) { final long combine = combine(segmentIndex, pos); changes.set(combine); } } } }