/* * Copyright (C) 2015 higherfrequencytrading.com * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ package net.openhft.chronicle.map; import net.openhft.chronicle.algo.bitset.*; import net.openhft.chronicle.bytes.Bytes; import net.openhft.chronicle.core.Maths; import net.openhft.chronicle.hash.VanillaGlobalMutableState; import net.openhft.chronicle.hash.impl.TierCountersArea; import net.openhft.chronicle.hash.impl.VanillaChronicleHash; import net.openhft.chronicle.hash.impl.stage.hash.ChainingInterface; 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.map.impl.CompiledReplicatedMapIterationContext; import net.openhft.chronicle.map.impl.CompiledReplicatedMapQueryContext; import net.openhft.chronicle.map.replication.MapRemoteOperations; import net.openhft.chronicle.values.Values; 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.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReferenceArray; import static net.openhft.chronicle.algo.MemoryUnit.*; import static net.openhft.chronicle.algo.bitset.BitSetFrame.NOT_FOUND; import static net.openhft.chronicle.algo.bytes.Access.checkedBytesStoreAccess; import static net.openhft.chronicle.algo.bytes.Access.nativeAccess; /** *

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 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; private static final int SIZE_OF_BOOTSTRAP_TIME_STAMP = 8; public transient TimeProvider timeProvider; /** * Default value is 0, that corresponds to "unset" identifier value (valid ids are positive) */ private transient byte localIdentifier; transient Set closeables; private transient long identifierUpdatedBytesOffset; private transient BitSet modIterSet; private transient AtomicReferenceArray modificationIterators; private transient long startOfModificationIterators; private transient boolean bootstrapOnlyLocalEntries; public transient long cleanupTimeout; public transient TimeUnit cleanupTimeoutUnit; public transient MapRemoteOperations remoteOperations; transient CompiledReplicatedMapQueryContext remoteOpContext; transient CompiledReplicatedMapIterationContext remoteItContext; transient BitSetFrame mainSegmentsModIterFrameForUpdates; transient BitSetFrame mainSegmentsModIterFrameForIteration; transient BitSetFrame tierBulkModIterFrameForUpdates; transient BitSetFrame tierBulkModIterFrameForIteration; public ReplicatedChronicleMap(@NotNull ChronicleMapBuilder builder, AbstractReplication replication) throws IOException { super(builder); initTransientsFromReplication(replication); } @Override protected VanillaGlobalMutableState createGlobalMutableState() { return Values.newNativeReference(ReplicatedGlobalMutableState.class); } @Override protected ReplicatedGlobalMutableState globalMutableState() { return (ReplicatedGlobalMutableState) super.globalMutableState(); } private int assignedModIterBitSetSizeInBytes() { return (int) CACHE_LINES.align(BYTES.alignAndConvert(127 + RESERVED_MOD_ITER, BITS), BYTES); } @Override public void initTransients() { super.initTransients(); initOwnTransients(); } private void initOwnTransients() { modificationIterators = new AtomicReferenceArray<>(127 + RESERVED_MOD_ITER); closeables = new CopyOnWriteArraySet<>(); long mainSegmentsBitSetSize = BYTES.toBits(modIterBitSetSizeInBytes()); mainSegmentsModIterFrameForUpdates = new SingleThreadedFlatBitSetFrame(mainSegmentsBitSetSize); mainSegmentsModIterFrameForIteration = new ConcurrentFlatBitSetFrame(mainSegmentsBitSetSize); long tierBulkBitSetSize = BYTES.toBits(tierBulkModIterBitSetSizeInBytes(tiersInBulk)); tierBulkModIterFrameForUpdates = new SingleThreadedFlatBitSetFrame(tierBulkBitSetSize); tierBulkModIterFrameForIteration = new ConcurrentFlatBitSetFrame(tierBulkBitSetSize); } @Override void initTransientsFromBuilder(ChronicleMapBuilder builder) { super.initTransientsFromBuilder(builder); this.remoteOperations = (MapRemoteOperations) builder.remoteOperations; this.timeProvider = builder.timeProvider(); cleanupTimeout = builder.cleanupTimeout; cleanupTimeoutUnit = builder.cleanupTimeoutUnit; } void initTransientsFromReplication(AbstractReplication replication) { this.localIdentifier = replication.identifier(); this.bootstrapOnlyLocalEntries = replication.bootstrapOnlyLocalEntries(); if (localIdentifier == -1) throw new IllegalStateException("localIdentifier should not be -1"); } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); initOwnTransients(); } long modIterBitSetSizeInBytes() { long bytes = BITS.toBytes(bitsPerSegmentInModIterBitSet() * actualSegments); return CACHE_LINES.align(bytes, BYTES); } @Override protected long computeTierBulkInnerOffsetToTiers(long tiersInBulk) { return super.computeTierBulkInnerOffsetToTiers(tiersInBulk) + tierBulkModIterBitSetSizeInBytes(tiersInBulk) * (128 + RESERVED_MOD_ITER); } private long tierBulkModIterBitSetSizeInBytes(long numberOfTiersInBulk) { long bytes = BITS.toBytes(bitsPerSegmentInModIterBitSet() * numberOfTiersInBulk); 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(actualChunksPerSegmentTier, 128L * 8L); } @Override public long mapHeaderInnerSize() { return super.mapHeaderInnerSize() + LAST_UPDATED_HEADER_SIZE + assignedModIterBitSetSizeInBytes() + (modIterBitSetSizeInBytes() * (128 + RESERVED_MOD_ITER)); } public void setLastModificationTime(byte identifier, long timestamp) { final long offset = identifierUpdatedBytesOffset + 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 (bs.readLong(offset) < timestamp) bs.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 bs.readLong(identifierUpdatedBytesOffset + remoteIdentifier * 8L); } @Override public void onHeaderCreated() { long offset = super.mapHeaderInnerSize(); identifierUpdatedBytesOffset = offset; offset += LAST_UPDATED_HEADER_SIZE; ConcurrentFlatBitSetFrame modIterBitSetFrame = new ConcurrentFlatBitSetFrame(BYTES.toBits(assignedModIterBitSetSizeInBytes())); modIterSet = new ReusableBitSet(modIterBitSetFrame, checkedBytesStoreAccess(), bs, offset); offset += assignedModIterBitSetSizeInBytes(); startOfModificationIterators = offset; } @Override protected void zeroOutNewlyMappedChronicleMapBytes() { super.zeroOutNewlyMappedChronicleMapBytes(); bs.zeroOut(super.mapHeaderInnerSize(), this.mapHeaderInnerSize()); } void addCloseable(Closeable closeable) { closeables.add(closeable); } @Override public synchronized void close() { if (closed) return; for (Closeable closeable : closeables) { try { closeable.close(); } catch (IOException e) { LOG.error("", e); } } super.close(); } @Override public byte identifier() { byte id = localIdentifier; if (id == 0) { throw new IllegalStateException("Replication identifier is not set for this\n" + "replicated Chronicle Map. This should only be possible if persisted\n" + "replicated Chronicle Map access from another process/JVM run/after\n" + "a transfer from another machine, and replication identifier is not\n" + "specified when access is configured, e. g. ChronicleMap.of(...)" + ".createPersistedTo(existingFile).\n" + "In this case, replicated Chronicle Map \"doesn't know\" it's identifier,\n" + "and is able to perform simple _read_ operations like map.get(), which\n" + "doesn't access the identifier. To perform updates, insertions, replication\n" + "tasks, you should configure the current node identifier,\n" + "by `replication(identifier)` method call in ChronicleMapBuilder\n" + "configuration chain."); } assert id > 0; return id; } @Override public ModificationIterator acquireModificationIterator(byte remoteIdentifier) { ModificationIterator modificationIterator = modificationIterators.get(remoteIdentifier); if (modificationIterator != null) return modificationIterator; synchronized (modificationIterators) { modificationIterator = modificationIterators.get(remoteIdentifier); if (modificationIterator != null) return modificationIterator; final ModificationIterator modIter = new ModificationIterator(remoteIdentifier); modificationIterators.set(remoteIdentifier, modIter); modIterSet.set(remoteIdentifier); return modIter; } } public void raiseChange(long tierIndex, long pos, long timestamp) { for (long next = modIterSet.nextSetBit(0L); next > 0L; next = modIterSet.nextSetBit(next + 1L)) { try { acquireModificationIterator((byte) next).raiseChange(tierIndex, pos, timestamp); } catch (Exception e) { LOG.error("", e); } } } public void dropChange(long tierIndex, long pos) { for (long next = modIterSet.nextSetBit(0L); next > 0L; next = modIterSet.nextSetBit(next + 1L)) { try { acquireModificationIterator((byte) next).dropChange(tierIndex, pos); } catch (Exception e) { LOG.error("", e); } } } public void moveChange(long oldTierIndex, long oldPos, long newTierIndex, long newPos, long timestamp) { for (long next = modIterSet.nextSetBit(0L); next > 0L; next = modIterSet.nextSetBit(next + 1L)) { try { ModificationIterator modificationIterator = acquireModificationIterator((byte) next); if (modificationIterator.dropChange(oldTierIndex, oldPos)) modificationIterator.raiseChange(newTierIndex, newPos, timestamp); } catch (Exception e) { LOG.error("", e); } } } public boolean isChanged(long tierIndex, long pos) { for (long next = modIterSet.nextSetBit(0L); next > 0L; next = modIterSet.nextSetBit(next + 1L)) { ModificationIterator modificationIterator = acquireModificationIterator((byte) next); if (modificationIterator.isChanged(tierIndex, pos)) return true; } return false; } @Override public boolean identifierCheck(@NotNull ReplicableEntry entry, int chronicleId) { return entry.originIdentifier() == identifier(); } @Override public int sizeOfEntry(@NotNull Bytes entry, int chronicleId) { long start = entry.readPosition(); try { final long keySize = keySizeMarshaller.readSize(entry); entry.readSkip(keySize + 8); // we skip 8 for the timestamp final byte identifier = entry.readByte(); if (identifier != identifier()) { // although unlikely, this may occur if the entry has been updated return 0; } entry.readSkip(1); // is Deleted long valueSize = valueSizeMarshaller.readSize(entry); long currentPosition = entry.readPosition(); long currentAddr = entry.address(currentPosition); long skip = alignAddr(currentAddr, alignment) - currentAddr; long result = currentPosition + skip + 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 + SIZE_OF_BOOTSTRAP_TIME_STAMP; } finally { entry.readPosition(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, long bootstrapTime) { final long keySize = keySizeMarshaller.readSize(entry); final long keyPosition = entry.readPosition(); entry.readSkip(keySize); final long timeStamp = entry.readLong(); final byte identifier = entry.readByte(); final boolean isDeleted = entry.readBoolean(); long valueSize; if (!isDeleted) { valueSize = valueSizeMarshaller.readSize(entry); } else { valueSize = Math.max(0, valueSizeMarshaller.minStorableSize()); } final long valuePosition = entry.readPosition(); destination.writeLong(bootstrapTime); 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 destination.write(entry, keyPosition, keySize); boolean debugEnabled = LOG.isDebugEnabled(); String message = null; if (debugEnabled) { if (isDeleted) { LOG.debug("WRITING ENTRY TO DEST - into local-id={}, remove(key={})", identifier(), entry.toString().trim()); } else { message = String.format( "WRITING ENTRY TO DEST - into local-id=%d, put(key=%s,", identifier(), entry.toString().trim()); } } if (isDeleted) return; // skipping the alignment, as alignment wont work when we send the data over the wire. long valueAddr = entry.address(valuePosition); long skip = alignAddr(valueAddr, alignment) - valueAddr; // writes the value destination.write(entry, valuePosition + skip, valueSize); if (debugEnabled) { LOG.debug(message + "value=" + entry.toString().trim() + ")"); } } private ChainingInterface q() { ChainingInterface queryContext; queryContext = cxt.get(); if (queryContext == null) { queryContext = new CompiledReplicatedMapQueryContext<>(ReplicatedChronicleMap.this); cxt.set(queryContext); } return queryContext; } @Override public CompiledReplicatedMapQueryContext mapContext() { return q().getContext(CompiledReplicatedMapQueryContext.class, ci -> new CompiledReplicatedMapQueryContext<>(ci, this)); } /** * 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 = (CompiledReplicatedMapQueryContext) q(); } assert !remoteOpContext.usedInit(); remoteOpContext.initUsed(true); return remoteOpContext; } private CompiledReplicatedMapIterationContext remoteItContext() { if (remoteItContext == null) { remoteItContext = (CompiledReplicatedMapIterationContext) 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 ChainingInterface i() { ChainingInterface iterContext; iterContext = cxt.get(); if (iterContext == null) { iterContext = new CompiledReplicatedMapIterationContext<>(ReplicatedChronicleMap.this); cxt.set(iterContext); } return iterContext; } public CompiledReplicatedMapIterationContext iterationContext() { return i().getContext(CompiledReplicatedMapIterationContext.class, ci -> new CompiledReplicatedMapIterationContext<>(ci, this)); } /** *

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 ModificationNotifier modificationNotifier; private final long mainSegmentsChangesBitSetAddr; private final int segmentIndexShift; private final long posMask; private final long offsetToBitSetWithinATierBulk; // when a bootstrap is send this is the time stamp that the client will bootstrap up to // if it is set as ZERO then the onPut() will set it to the current time, once the // consumer has cycled through the bit set the timestamp will be set back to zero. private AtomicLong bootStrapTimeStamp = new AtomicLong(); private long lastBootStrapTimeStamp = timeProvider.currentTime(); // records the current position of the cursor in the bitset // TODO volatile? private volatile long position = NOT_FOUND; // -1 for main segments area, 0-N is tier bulk id private int iterationMainSegmentsAreaOrTierBulk = -1; private long tierBulkBitSetAddr = 0L; public ModificationIterator(int remoteIdentifier) { long bitsPerSegment = bitsPerSegmentInModIterBitSet(); segmentIndexShift = Long.numberOfTrailingZeros(bitsPerSegment); posMask = bitsPerSegment - 1L; mainSegmentsChangesBitSetAddr = bsAddress() + startOfModificationIterators + remoteIdentifier * modIterBitSetSizeInBytes(); nativeAccess().zeroOut(null, mainSegmentsChangesBitSetAddr, modIterBitSetSizeInBytes()); offsetToBitSetWithinATierBulk = remoteIdentifier * tierBulkModIterBitSetSizeInBytes(tiersInBulk); } public ModificationIterator(int remoteIdentifier, ModificationNotifier notifier) { this(remoteIdentifier); setModificationNotifier(notifier); } public void setModificationNotifier(@NotNull ModificationNotifier modificationNotifier) { this.modificationNotifier = modificationNotifier; } /** * used to merge multiple segments and positions into a single index used by the bit map * * @param tierIndex the index of the maps segment * @param pos the position within this {@code tierIndex} * @return and index the has combined the {@code tierIndex} and {@code pos} into a * single value */ private long combine(long tierIndex, long pos) { long tierIndexMinusOne = tierIndex - 1; if (tierIndexMinusOne < actualSegments) return (tierIndexMinusOne << segmentIndexShift) | pos; return combineExtraTier(tierIndexMinusOne, pos); } private long combineExtraTier(long tierIndexMinusOne, long pos) { long extraTierIndex = tierIndexMinusOne - actualSegments; long tierIndexOffsetWithinBulk = extraTierIndex & (tiersInBulk - 1); return (tierIndexOffsetWithinBulk << segmentIndexShift) | pos; } void raiseChange(long tierIndex, long pos, long timestamp) { LOG.debug("raise change: id {}, tierIndex {}, pos {}", localIdentifier, tierIndex, pos); long bitIndex = combine(tierIndex, pos); if (tierIndex <= actualSegments) { mainSegmentsModIterFrameForUpdates.set(nativeAccess(), null, mainSegmentsChangesBitSetAddr, bitIndex); } else { raiseExtraTierChange(tierIndex, bitIndex); } // assert timestamp > timeProvider.currentTime() - TimeUnit.SECONDS.toMillis(1) && // timestamp <= timeProvider.currentTime() : "timeStamp=" + timestamp + ", " + // "currentTime=" + timeProvider.currentTime(); // todo improve this - use the timestamp from the entry its self bootStrapTimeStamp.compareAndSet(0, timestamp); if (modificationNotifier != null) modificationNotifier.onChange(); } private void raiseExtraTierChange(long tierIndex, long bitIndex) { tierIndex = tierIndex - actualSegments - 1; long bulkIndex = tierIndex >> log2TiersInBulk; TierBulkData tierBulkData = tierBulkOffsets.get((int) bulkIndex); long bitSetAddr = bitSetAddr(tierBulkData); tierBulkModIterFrameForUpdates.set(nativeAccess(), null, bitSetAddr, bitIndex); } boolean dropChange(long tierIndex, long pos) { LOG.debug("drop change: id {}, tierIndex {}, pos {}", localIdentifier, tierIndex, pos); long bitIndex = combine(tierIndex, pos); if (tierIndex <= actualSegments) { return mainSegmentsModIterFrameForUpdates.clearIfSet(nativeAccess(), null, mainSegmentsChangesBitSetAddr, bitIndex); } else { return dropExtraTierChange(tierIndex, bitIndex); } } private boolean dropExtraTierChange(long tierIndex, long bitIndex) { tierIndex = tierIndex - actualSegments - 1; long bulkIndex = tierIndex >> log2TiersInBulk; TierBulkData tierBulkData = tierBulkOffsets.get((int) bulkIndex); long bitSetAddr = bitSetAddr(tierBulkData); return tierBulkModIterFrameForUpdates.clearIfSet(nativeAccess(), null, bitSetAddr, bitIndex); } private long bitSetAddr(TierBulkData tierBulkData) { return tierBulkData.bytesStore.address(tierBulkData.offset) + offsetToBitSetWithinATierBulk; } boolean isChanged(long tierIndex, long pos) { long bitIndex = combine(tierIndex, pos); if (tierIndex <= actualSegments) { return mainSegmentsModIterFrameForUpdates.isSet(nativeAccess(), null, mainSegmentsChangesBitSetAddr, bitIndex); } else { return isChangedExtraTier(tierIndex, bitIndex); } } private boolean isChangedExtraTier(long tierIndex, long bitIndex) { long extraTierIndex = tierIndex - actualSegments - 1; long bulkIndex = extraTierIndex >> log2TiersInBulk; TierBulkData tierBulkData = tierBulkOffsets.get((int) bulkIndex); long bitSetAddr = bitSetAddr(tierBulkData); return tierBulkModIterFrameForUpdates.isSet(nativeAccess(), null, bitSetAddr, bitIndex); } /** * 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() { return nextPosition() >= 0; } private long nextPosition() { long nextPos; long position = this.position; boolean allBitSetsScannedFromTheStart = false; // at most 2 iterations while (!allBitSetsScannedFromTheStart) { if (iterationMainSegmentsAreaOrTierBulk == -1) { allBitSetsScannedFromTheStart = position < 0; if ((nextPos = mainSegmentsModIterFrameForIteration.nextSetBit(nativeAccess(), null, mainSegmentsChangesBitSetAddr, position + 1)) != NOT_FOUND) { return nextPos; } else { this.position = position = NOT_FOUND; // go to the first bulk iterationMainSegmentsAreaOrTierBulk = 0; } } // for each allocated tier bulk while (iterationMainSegmentsAreaOrTierBulk < globalMutableState().getAllocatedExtraTierBulks()) { VanillaChronicleHash.TierBulkData tierBulkData = tierBulkOffsets.get(iterationMainSegmentsAreaOrTierBulk); tierBulkBitSetAddr = bitSetAddr(tierBulkData); if ((nextPos = tierBulkModIterFrameForIteration.nextSetBit(nativeAccess(), null, tierBulkBitSetAddr, position + 1)) != NOT_FOUND) { return nextPos; } // go to the next bulk iterationMainSegmentsAreaOrTierBulk++; this.position = position = NOT_FOUND; } this.iterationMainSegmentsAreaOrTierBulk = -1; this.position = position = NOT_FOUND; bootStrapTimeStamp.set(0); } return 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) { while (true) { long position = nextPosition(); if (position == NOT_FOUND) { this.position = NOT_FOUND; return false; } this.position = position; int segmentIndexOrTierIndexOffsetWithinBulk = (int) (position >>> segmentIndexShift); try (CompiledReplicatedMapIterationContext context = iterationContext()) { if (iterationMainSegmentsAreaOrTierBulk < 0) { // if main area // segmentIndexOrTierIndexOffsetWithinBulk is segment index context.initSegmentIndex(segmentIndexOrTierIndexOffsetWithinBulk); } else { // extra tiers // segmentIndexOrTierIndexOffsetWithinBulk is tier index offset within bulk TierBulkData tierBulkData = tierBulkOffsets.get(iterationMainSegmentsAreaOrTierBulk); long tierBaseAddr = tierAddr(tierBulkData, segmentIndexOrTierIndexOffsetWithinBulk); long tierCountersAreaAddr = tierBaseAddr + tierHashLookupOuterSize; context.initSegmentIndex( TierCountersArea.segmentIndex(tierCountersAreaAddr)); int tier = TierCountersArea.tier(tierCountersAreaAddr); long tierIndex = actualSegments + (iterationMainSegmentsAreaOrTierBulk << log2TiersInBulk) + segmentIndexOrTierIndexOffsetWithinBulk + 1; context.initSegmentTier(tier, tierIndex, tierBaseAddr); } context.updateLock().lock(); if (changesForUpdatesGet(position)) { entryCallback.onBeforeEntry(); final long segmentPos = position & posMask; context.readExistingEntry(segmentPos); // 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.segmentBytes() .readLimit(context.valueOffset() + context.valueSize()); context.segmentBytes().readPosition(context.keySizeOffset()); boolean success = entryCallback.onEntry( context.segmentBytes(), chronicleId, bootStrapTimeStamp()); entryCallback.onAfterEntry(); if (success) changesForUpdatesClear(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) } } } private boolean changesForUpdatesGet(long position) { if (iterationMainSegmentsAreaOrTierBulk < 0) { return mainSegmentsModIterFrameForUpdates.get(nativeAccess(), null, mainSegmentsChangesBitSetAddr, position); } else { return tierBulkModIterFrameForUpdates.get(nativeAccess(), null, tierBulkBitSetAddr, position); } } private void changesForUpdatesClear(long position) { if (iterationMainSegmentsAreaOrTierBulk < 0) { mainSegmentsModIterFrameForUpdates.clear(nativeAccess(), null, mainSegmentsChangesBitSetAddr, position); } else { tierBulkModIterFrameForUpdates.clear(nativeAccess(), null, tierBulkBitSetAddr, position); } } /** * @return the timestamp that the remote client should bootstrap from when there has been a * disconnection, this time maybe later than the message time as event are not send in * chronological order from the bit set. */ private long bootStrapTimeStamp() { final long timeStamp = bootStrapTimeStamp.get(); long result = (timeStamp == 0) ? this.lastBootStrapTimeStamp : timeStamp; this.lastBootStrapTimeStamp = result; return result; } @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 boolean debugEnabled = LOG.isDebugEnabled(); for (int segmentIndex = 0; segmentIndex < actualSegments; segmentIndex++) { c.initSegmentIndex(segmentIndex); c.forEachSegmentReplicableEntry(e -> { if (debugEnabled) { LOG.debug("Bootstrap entry: id {}, key {}, value {}", localIdentifier, c.key(), c.value()); } // Bizarrely the next line line cause NPE in JDT compiler //assert re.originTimestamp() > 0L; if (debugEnabled) { LOG.debug("Bootstrap decision: bs ts: {}, entry ts: {}, " + "entry id: {}, local id: {}", fromTimeStamp, e.originTimestamp(), e.originIdentifier(), localIdentifier); } if (e.originTimestamp() >= fromTimeStamp && (!bootstrapOnlyLocalEntries || e.originIdentifier() == localIdentifier)) { raiseChange(c.tierIndex(), c.pos(), c.timestamp()); } }); } } } } }