/*
* 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());
}
}
}
}
}