/* * JBoss, Home of Professional Open Source * Copyright 2011 Red Hat Inc. and/or its affiliates and other contributors * as indicated by the @author tags. All rights reserved. * See the copyright.txt in the distribution for a * full listing of individual contributors. * * This copyrighted material is made available to anyone wishing to use, * modify, copy, or redistribute it subject to the terms and conditions * of the GNU Lesser General Public License, v. 2.1. * This program is distributed in the hope that it will be useful, but WITHOUT A * 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, * v.2.1 along with this distribution; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301, USA. */ package org.infinispan.cacheviews; import org.infinispan.remoting.transport.Address; import org.infinispan.util.Immutables; import org.infinispan.util.logging.Log; import org.infinispan.util.logging.LogFactory; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; /** * This class is used on the coordinator to keep track of changes since the last merge. * * When the coordinator changes or in case of a merge, the new coordinator recovers the last committed view * from all the members and rolls back any uncommitted views, then it prepares a new view if necessary. */ public class PendingCacheViewChanges { private static final Log log = LogFactory.getLog(PendingCacheViewChanges.class); private static final int NO_REPLICATION_DEGREE_CHANGE = -1; private final Object lock = new Object(); private final String cacheName; // The last view id generated (or received during a recover operation) private int lastViewId; // The join requests since the last COMMIT_VIEW // These are only used if we are the coordinator. private final Set<Address> joiners; // The leave requests are also used on normal nodes to compute the valid members set private final Set<Address> leavers; // True if there was a merge since the last committed view private Set<Address> recoveredMembers; //The flag to move keys private boolean shouldMoveKey = false; private int pendingReplicationDegree = NO_REPLICATION_DEGREE_CHANGE; private int replicationDegree = NO_REPLICATION_DEGREE_CHANGE; private boolean viewInstallationInProgress; public PendingCacheViewChanges(String cacheName) { this.cacheName = cacheName; this.joiners = new HashSet<Address>(); this.leavers = new HashSet<Address>(); this.recoveredMembers = Collections.emptySet(); } /** * Called on the coordinator to create the view that will be prepared next. * It also sets the pendingView field, so the next call to {@code prepareView} will have no effect. */ public CacheView createPendingView(CacheView committedView) { synchronized (lock) { // TODO Enforce view installation policy here? if(!shouldMoveKey && pendingReplicationDegree == NO_REPLICATION_DEGREE_CHANGE){ if (viewInstallationInProgress) { log.tracef("Cannot create a new view, there is another view installation in progress"); return null; } if (leavers.size() == 0 && joiners.size() == 0 && recoveredMembers == null) { log.tracef("Cannot create a new view, we have no joiners or leavers"); return null; } } shouldMoveKey = false; replicationDegree = pendingReplicationDegree; pendingReplicationDegree = NO_REPLICATION_DEGREE_CHANGE; Collection<Address> baseMembers = recoveredMembers != null ? recoveredMembers : committedView.getMembers(); log.tracef("Previous members are %s, joiners are %s, leavers are %s, recovered after merge = %s", baseMembers, joiners, leavers, recoveredMembers != null); List<Address> members = new ArrayList<Address>(baseMembers); // If a node is both in leavers and in joiners we should install a view without it first // so that other nodes don't consider it an old owner, so we first add it as a joiner // and then we remove it as a leaver. members.addAll(joiners); members.removeAll(leavers); viewInstallationInProgress = true; lastViewId++; CacheView pendingView = new CacheView(lastViewId, members); log.tracef("%s: created new view %s", cacheName, pendingView); return pendingView; } } /** * Called on the coordinator before a rollback to assign a unique view id to the rollback. */ public int getRollbackViewId() { synchronized (lock) { lastViewId++; return lastViewId; } } public boolean hasChanges() { return recoveredMembers != null || !joiners.isEmpty() || !leavers.isEmpty(); } public void resetChanges(CacheView committedView) { synchronized (lock) { // the list of valid members remains the same if (log.isDebugEnabled()) { // if a node was both a joiner and a leaver, the committed view should not contain it List<Address> bothJoinerAndLeavers = new ArrayList<Address>(joiners); bothJoinerAndLeavers.retainAll(leavers); for (Address node : bothJoinerAndLeavers) { if (committedView.contains(node)) { log.debugf("Node %s should not be a member in view %s, left and then joined before the view was installed"); } } } leavers.retainAll(committedView.getMembers()); joiners.removeAll(committedView.getMembers()); recoveredMembers = null; shouldMoveKey = false; log.tracef("Should move key set to false"); pendingReplicationDegree = NO_REPLICATION_DEGREE_CHANGE; replicationDegree = NO_REPLICATION_DEGREE_CHANGE; viewInstallationInProgress = false; if (committedView.getViewId() > lastViewId) { lastViewId = committedView.getViewId(); } } } /** * Signal a join */ public void requestJoin(Address joiner) { synchronized (lock) { log.tracef("%s: Node %s is joining", cacheName, joiner); // if the node wanted to leave earlier, we don't remove it from the list of leavers // since it has already left, it won't have the latest data and so it's not a valid member joiners.add(joiner); } } /** * Signal to move keys */ public void requestMoveKeys(){ shouldMoveKey = true; log.tracef("Should move key set to TRUE"); } /** * Signal a leave. */ public Set<Address> requestLeave(Collection<Address> leavers) { synchronized (lock) { log.tracef("%s: Nodes %s are leaving", cacheName, leavers); // if the node wanted to join earlier, just remove it from the list of joiners Set<Address> leavers2 = new HashSet<Address>(leavers); joiners.removeAll(leavers2); leavers2.removeAll(joiners); log.tracef("%s: After pruning nodes that have joined but have never installed a view, leavers are %s", cacheName, leavers2); this.leavers.addAll(leavers2); return leavers2; } } /** * Signal a merge */ public void recoveredViews(Collection<Address> newMembers, Collection<Address> recoveredJoiners) { synchronized (lock) { log.tracef("%s: Coordinator changed, this node is the current coordinator", cacheName); recoveredMembers = new HashSet<Address>(newMembers); // Apply any changes that we may have received before we realized we're the coordinator recoveredMembers.removeAll(leavers); joiners.addAll(recoveredJoiners); joiners.removeAll(recoveredMembers); log.tracef("%s: Members after coordinator change: %s, joiners: %s, leavers: %s", cacheName, recoveredMembers, joiners, leavers); } } /** * If we recovered a view after a merge or coordinator change we need to make sure the next view id is greater * than any view id that was already committed. */ public void updateLatestViewId(int viewId) { synchronized (lock) { if (viewId > lastViewId) { lastViewId = viewId; } } } /** * @return the nodes that left since the last {@code resetChanges} call */ public Set<Address> getLeavers() { synchronized (lock) { return Immutables.immutableSetCopy(leavers); } } /** * @return The nodes that are in the {@code joiners} collection but not in the {@code newMembers} collection. */ public Set<Address> computeMissingJoiners(Collection<Address> newMembers) { synchronized (lock) { Set<Address> missingJoiners = new HashSet<Address>(joiners); missingJoiners.removeAll(newMembers); return missingJoiners; } } /** * @return true if {@code createPendingView} has been called without a pair {@code resetChanges} */ public boolean isViewInstallationInProgress() { return viewInstallationInProgress; } /** * @return the id of the view we created last (or received via {@code updateLatestViewId}. */ public int getLastViewId() { synchronized (lock) { return lastViewId; } } public final void requestNewReplicationDegree(int replicationDegree) { this.pendingReplicationDegree = replicationDegree; } public final int getReplicationDegree() { return replicationDegree; } }