/* * Copyright (C) 2012, 2016 higherfrequencytrading.com * Copyright (C) 2016 Roman Leventov * * 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 <http://www.gnu.org/licenses/>. */ package net.openhft.chronicle.map.replication; import net.openhft.chronicle.hash.Data; import net.openhft.chronicle.hash.replication.DefaultEventualConsistencyStrategy; import net.openhft.chronicle.hash.replication.ReplicableEntry; import net.openhft.chronicle.map.*; import static net.openhft.chronicle.hash.replication.DefaultEventualConsistencyStrategy.AcceptanceDecision.ACCEPT; import static net.openhft.chronicle.hash.replication.DefaultEventualConsistencyStrategy.AcceptanceDecision.DISCARD; import static net.openhft.chronicle.hash.replication.DefaultEventualConsistencyStrategy.decideOnRemoteModification; /** * SPI strategy of performing remote calls and apply replication events for {@link ChronicleMap}. * * <p>Example: Grow-only set values CRDT: <pre><code> * class GrowOnlySetValuedMapEntryOperations<K, E> * implements MapEntryOperations<K, Set<E>, Void> { * @Override * public Void remove(@NotNull MapEntry<K, Set<E>> entry) { * throw new UnsupportedOperationException("Map with grow-only set values " + * "doesn't support map value removals"); * } * } * * class GrowOnlySetValuedMapRemoteOperations<K, E> * implements MapRemoteOperations<K, Set<E>, Void> { * @Override * public void put(MapRemoteQueryContext<K, Set<E>, Void> q, * Data<Set<E>, ?> newValue) { * MapReplicableEntry<K, Set<E>> entry = q.entry(); * if (entry != null) { * Set<E> merged = new HashSet<>(entry.value().get()); * merged.addAll(newValue.get()); * q.replaceValue(entry, q.wrapValueAsData(merged)); * } else { * q.insert(q.absentEntry(), newValue); * q.entry().updateOrigin(q.remoteIdentifier(), q.remoteTimestamp()); * } * } * * @Override * public void remove(MapRemoteQueryContext<K, Set<E>, Void> q) { * throw new UnsupportedOperationException(); * } * }</code></pre> * * @param <K> the map key type * @param <V> the map value type * @param <R> the return type of {@link MapEntryOperations} specified fro the queried map * @see DefaultEventualConsistencyStrategy * @see ChronicleMapBuilderPrivateAPI#remoteOperations(MapRemoteOperations) */ public interface MapRemoteOperations<K, V, R> { /** * Handle remote {@code remove} call and {@code remove} replication event, i. e. when the entry * with the query key ({@code q.queriedKey()}) was removed on some {@code ChronicleMap} node. * * @param q the remote operation context */ default void remove(MapRemoteQueryContext<K, V, R> q) { MapReplicableEntry<K, V> entry = q.entry(); if (entry != null) { if (decideOnRemoteModification(entry, q) == ACCEPT) { q.remove(entry); ReplicableEntry replicableAbsentEntry = (ReplicableEntry) q.absentEntry(); assert replicableAbsentEntry != null; replicableAbsentEntry.updateOrigin(q.remoteIdentifier(), q.remoteTimestamp()); // (*) if (q.remoteIdentifier() == q.currentNodeIdentifier()) { // The entry with origin (replicatedIdentifier() is remote entry's origin), // equal to the current node, should be lost on on this node (or this node is // a different Chronicle Map instance, because the previous was lost, just using // the same identifier), if it happens we should propagate this recovered // event again, to other nodes replicableAbsentEntry.raiseChangedForAllExcept(q.remoteNodeIdentifier()); replicableAbsentEntry.dropChangedFor(q.remoteNodeIdentifier()); } else { // We accepted a replication event, suppress further event propagation replicableAbsentEntry.dropChanged(); } } } else { MapAbsentEntry<K, V> absentEntry = q.absentEntry(); assert absentEntry != null; ReplicableEntry replicableAbsentEntry; if (!(absentEntry instanceof ReplicableEntry)) { // Note in the two following lines dummy value is inserted and removed using direct // entry.doXxx calls, not q.xxx(entry). The intention is to avoid calling possibly // overridden MapEntryOperations, because this is technical procedure of making // "truly absent" entry "deleted", not actual insertion and removal. absentEntry.doInsert(q.dummyZeroValue()); entry = q.entry(); assert entry != null; entry.doRemove(); replicableAbsentEntry = (ReplicableEntry) q.absentEntry(); assert replicableAbsentEntry != null; } else { replicableAbsentEntry = (ReplicableEntry) absentEntry; if (decideOnRemoteModification(replicableAbsentEntry, q) == DISCARD) return; } replicableAbsentEntry.updateOrigin(q.remoteIdentifier(), q.remoteTimestamp()); // For explanation see similar block above (*) if (q.remoteIdentifier() == q.currentNodeIdentifier()) { replicableAbsentEntry.raiseChangedForAllExcept(q.remoteNodeIdentifier()); replicableAbsentEntry.dropChangedFor(q.remoteNodeIdentifier()); } else { replicableAbsentEntry.dropChanged(); } } } /** * Handle remote {@code put} call or replication event, i. e. when the entry with the queried * key ({@code q.queriedKey()}) was changed on some remote {@code ChronicleMap} node, with the * given {@code newValue}. * * @param q the remote operation context * @param newValue the new value to put */ default void put(MapRemoteQueryContext<K, V, R> q, Data<V> newValue) { MapReplicableEntry<K, V> entry = q.entry(); if (entry != null) { if (decideOnRemoteModification(entry, q) == ACCEPT) { q.replaceValue(entry, newValue); entry.updateOrigin(q.remoteIdentifier(), q.remoteTimestamp()); // For explanation see similar block in remove() method (*) if (q.remoteIdentifier() == q.currentNodeIdentifier()) { // This differs from action under the same conditions in remove() method, // to let the item 10 in (**) work! entry.raiseChanged(); } else { entry.dropChanged(); } } } else { MapAbsentEntry<K, V> absentEntry = q.absentEntry(); assert absentEntry != null; if (!(absentEntry instanceof ReplicableEntry) || decideOnRemoteModification((ReplicableEntry) absentEntry, q) == ACCEPT) { q.insert(absentEntry, newValue); entry = q.entry(); assert entry != null; entry.updateOrigin(q.remoteIdentifier(), q.remoteTimestamp()); // For explanation see similar block in remove() method (*) if (q.remoteIdentifier() == q.currentNodeIdentifier()) { // This differs from action under the same conditions in remove() method, // to let the item 10 in (**) work! entry.raiseChanged(); } else { entry.dropChanged(); } } else { // (**) // In case of old deleted entries cleanup + network disconnections and // bootstrapping, it is possible that the entry ends being removed on the remote // node and present on the origin node: // // 1. Entry is added on node 1 // 2. This change is replicated and arrived to node 2 // 3. Entry is removed on node 1, it is scheduled for replication again // 4. Network issues, disconnection, bootstrapping // 5. Node 2 schedules the subject entry (currently present on node 2) // for bootstrapping to the node 1 // 6. Node 2 sends the replication event to node 1 (in-flight) // 7. The entry removal is replicated from node 1 to node 2, it is accepted, finally // the entry is removed on node 2 // 8. The entry is cleaned up (erased completely) on node 1, because it is already // replicated to everywhere it should be, and timeout (if set very short) // is expired // 9. Replication event from node 2 (with present entry) arrives to node 1, as the // entry is already erased from node 1, this change is accepted, and finally // the entry is present on node 1. // 10. The entry is bootstrapped again on node 1, by (*) blocks // 11. The "present" entry from node 1 is discarded on node 2, because it sees that // this entry was removed later. // // The following block captures the condition from item 11, and sends the event // again back, to give the chance to be finally removed on node 1 (in the above // example). if (((ReplicableEntry) absentEntry).originIdentifier() == q.remoteIdentifier() && q.remoteIdentifier() != q.currentNodeIdentifier()) { // If this change will arrive to the origin node and accepted, it will be // propagated to all other nodes, by (*) blocks ((ReplicableEntry) absentEntry).raiseChangedFor(q.remoteIdentifier()); } } } } }