/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional information regarding
* copyright ownership. The ASF licenses this file to You 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 org.apache.geode.cache.partition;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.apache.geode.cache.Cache;
import org.apache.geode.cache.DataPolicy;
import org.apache.geode.cache.PartitionAttributes;
import org.apache.geode.cache.PartitionAttributesFactory;
import org.apache.geode.cache.PartitionResolver;
import org.apache.geode.cache.Region;
import org.apache.geode.cache.control.RebalanceResults;
import org.apache.geode.cache.control.ResourceManager;
import org.apache.geode.cache.execute.Function;
import org.apache.geode.cache.execute.FunctionService;
import org.apache.geode.cache.execute.RegionFunctionContext;
import org.apache.geode.distributed.DistributedMember;
import org.apache.geode.internal.cache.ColocationHelper;
import org.apache.geode.internal.cache.FixedPartitionAttributesImpl;
import org.apache.geode.internal.cache.GemFireCacheImpl;
import org.apache.geode.internal.cache.LocalDataSet;
import org.apache.geode.internal.cache.PartitionedRegion;
import org.apache.geode.internal.cache.PartitionedRegion.RecoveryLock;
import org.apache.geode.internal.cache.PartitionedRegionHelper;
import org.apache.geode.internal.cache.control.RebalanceResultsImpl;
import org.apache.geode.internal.cache.execute.InternalRegionFunctionContext;
import org.apache.geode.internal.cache.partitioned.PartitionedRegionRebalanceOp;
import org.apache.geode.internal.cache.partitioned.rebalance.ExplicitMoveDirector;
import org.apache.geode.internal.cache.partitioned.rebalance.PercentageMoveDirector;
import org.apache.geode.internal.i18n.LocalizedStrings;
/**
* Utility methods for handling partitioned Regions, for example during execution of {@link Function
* Functions} on a Partitioned Region.
*
* <p>
* Example of a Function using utility methods:
*
* <pre>
* public Serializable execute(FunctionContext context) {
* if (context instanceof RegionFunctionContext) {
* RegionFunctionContext rc = (RegionFunctionContext) context;
* if (PartitionRegionHelper.isPartitionedRegion(rc.getDataSet())) {
* Region efficientReader =
* PartitionRegionHelper.getLocalDataForContext(rc);
* efficientReader.get("someKey");
* // ...
* }
* }
* // ...
* </pre>
*
*
* @since GemFire 6.0
* @see FunctionService#onRegion(Region)
*/
public final class PartitionRegionHelper {
private PartitionRegionHelper() {}
/**
* Given a partitioned Region, return a map of
* {@linkplain PartitionAttributesFactory#setColocatedWith(String) colocated Regions}. Given a
* local data reference to a partitioned region, return a map of local
* {@linkplain PartitionAttributesFactory#setColocatedWith(String) colocated Regions}. If there
* are no colocated regions, return an empty map.
*
* @param r a partitioned Region
* @throws IllegalStateException if the Region is not a {@linkplain DataPolicy#PARTITION
* partitioned Region}
* @return an unmodifiable map of {@linkplain Region#getFullPath() region name} to {@link Region}
* @since GemFire 6.0
*/
public static Map<String, Region<?, ?>> getColocatedRegions(final Region<?, ?> r) {
Map ret;
if (isPartitionedRegion(r)) {
final PartitionedRegion pr = (PartitionedRegion) r;
ret = ColocationHelper.getAllColocationRegions(pr);
if (ret.isEmpty()) {
ret = Collections.emptyMap();
}
} else if (r instanceof LocalDataSet) {
LocalDataSet lds = (LocalDataSet) r;
InternalRegionFunctionContext fc = lds.getFunctionContext();
if (fc != null) {
ret = ColocationHelper.getAllColocatedLocalDataSets(lds.getProxy(), fc);
if (ret.isEmpty()) {
ret = Collections.emptyMap();
}
} else {
ret = ColocationHelper.getColocatedLocalDataSetsForBuckets(lds.getProxy(),
lds.getBucketSet());
}
} else {
throw new IllegalArgumentException(
LocalizedStrings.PartitionManager_REGION_0_IS_NOT_A_PARTITIONED_REGION
.toLocalizedString(r.getFullPath()));
}
return Collections.unmodifiableMap(ret);
}
/**
* Test a Region to see if it is a partitioned Region
*
* @param r
* @return true if it is a partitioned Region
* @since GemFire 6.0
*/
public static boolean isPartitionedRegion(final Region<?, ?> r) {
if (r == null) {
throw new IllegalArgumentException(
LocalizedStrings.PartitionRegionHelper_ARGUMENT_REGION_IS_NULL.toString());
}
return r instanceof PartitionedRegion ? true : false;
}
/**
* Test a Region to see if it is a partitioned Region
*
* @param r
* @throws IllegalStateException
* @return PartitionedRegion if it is a partitioned Region
* @since GemFire 6.0
*/
private static PartitionedRegion isPartitionedCheck(final Region<?, ?> r) {
if (!isPartitionedRegion(r)) {
throw new IllegalArgumentException(
LocalizedStrings.PartitionManager_REGION_0_IS_NOT_A_PARTITIONED_REGION
.toLocalizedString(r.getFullPath()));
}
return (PartitionedRegion) r;
}
/**
* Gathers a set of details about all partitioned regions in the local Cache. If there are no
* partitioned regions then an empty set will be returned.
*
* @param cache the cache which has the regions
* @return set of details about all locally defined partitioned regions
* @since GemFire 6.0
*/
public static Set<PartitionRegionInfo> getPartitionRegionInfo(final Cache cache) {
Set<PartitionRegionInfo> prDetailsSet = new TreeSet<PartitionRegionInfo>();
fillInPartitionedRegionInfo((GemFireCacheImpl) cache, prDetailsSet, false);
return prDetailsSet;
}
/**
* Gathers details about the specified partitioned region. Returns null if the partitioned region
* is not locally defined.
*
* @param region the region to get info about
* @return details about the specified partitioned region
* @since GemFire 6.0
*/
public static PartitionRegionInfo getPartitionRegionInfo(final Region<?, ?> region) {
try {
PartitionedRegion pr = isPartitionedCheck(region);
GemFireCacheImpl cache = (GemFireCacheImpl) region.getCache();
return pr.getRedundancyProvider().buildPartitionedRegionInfo(false,
cache.getResourceManager().getLoadProbe()); // may return null
} catch (ClassCastException e) {
// not a PR so return null
}
return null;
}
private static void fillInPartitionedRegionInfo(GemFireCacheImpl cache, final Set prDetailsSet,
final boolean internal) {
// TODO: optimize by fetching all PR details from each member at once
Set<PartitionedRegion> prSet = cache.getPartitionedRegions();
if (prSet.isEmpty()) {
return;
}
for (Iterator<PartitionedRegion> iter = prSet.iterator(); iter.hasNext();) {
PartitionedRegion pr = iter.next();
PartitionRegionInfo prDetails = pr.getRedundancyProvider()
.buildPartitionedRegionInfo(internal, cache.getResourceManager().getLoadProbe());
if (prDetails != null) {
prDetailsSet.add(prDetails);
}
}
}
/**
* Decide which partitions will host which buckets. Gemfire normally assigns buckets to partitions
* as needed when data is added to a partitioned region. This method provides way to assign all of
* the buckets without putting any data in partition region. This method should not be called
* until all of the partitions are running because it will divide the buckets between the running
* partitions. If the buckets are already assigned this method will have no effect.
*
* This method will block until all buckets are assigned.
*
* @param region The region which should have it's buckets assigned.
* @throws IllegalStateException if the provided region is something other than a
* {@linkplain DataPolicy#PARTITION partitioned Region}
* @since GemFire 6.0
*/
public static void assignBucketsToPartitions(Region<?, ?> region) {
PartitionedRegion pr = isPartitionedCheck(region);
RecoveryLock lock = null;
try {
lock = pr.getRecoveryLock();
lock.lock();
for (int i = 0; i < getNumberOfBuckets(pr); i++) {
// This method will return quickly if the bucket already exists
pr.createBucket(i, 0, null);
}
} finally {
if (lock != null) {
lock.unlock();
}
}
}
private static int getNumberOfBuckets(PartitionedRegion pr) {
if (pr.isFixedPartitionedRegion()) {
int numBuckets = 0;
Set<FixedPartitionAttributesImpl> fpaSet = new HashSet<FixedPartitionAttributesImpl>(
pr.getRegionAdvisor().adviseAllFixedPartitionAttributes());
if (pr.getFixedPartitionAttributesImpl() != null) {
fpaSet.addAll(pr.getFixedPartitionAttributesImpl());
}
for (FixedPartitionAttributesImpl fpa : fpaSet) {
numBuckets = numBuckets + fpa.getNumBuckets();
}
return numBuckets;
}
return pr.getTotalNumberOfBuckets();
}
/**
* Get the current primary owner for a key. Upon return there is no guarantee that primary owner
* remains the primary owner, or that the member is still alive.
* <p>
* This method is not a substitute for {@link Region#containsKey(Object)}.
* </p>
*
* @param r a PartitionedRegion
* @param key the key to evaluate
* @throws IllegalStateException if the provided region is something other than a
* {@linkplain DataPolicy#PARTITION partitioned Region}
* @return the primary member for the key, possibly null if a primary is not yet determined
* @since GemFire 6.0
*/
public static <K, V> DistributedMember getPrimaryMemberForKey(final Region<K, V> r, final K key) {
PartitionedRegion pr = isPartitionedCheck(r);
int bucketId = PartitionedRegionHelper.getHashKey(pr, null, key, null, null);
return pr.getBucketPrimary(bucketId);
}
/**
* Get all potential redundant owners for a key. If the key exists in the Region, upon return
* there is no guarantee that key has not been moved or that the members are still alive.
*
* <p>
* This method is not a substitute for {@link Region#containsKey(Object)}.
* </p>
* <p>
* This method is equivalent to: <code>
* DistributedMember primary = getPrimaryMemberForKey(r, key);
* Set<? extends DistributedMember> allMembers = getAllMembersForKey(r, key);
* allMembers.remove(primary);
* </code>
* </p>
*
* @param r a PartitionedRegion
* @param key the key to evaluate
* @throws IllegalStateException if the provided region is something other than a
* {@linkplain DataPolicy#PARTITION partitioned Region}
* @return an unmodifiable set of members minus the primary
* @since GemFire 6.0
*/
public static <K, V> Set<DistributedMember> getRedundantMembersForKey(final Region<K, V> r,
final K key) {
DistributedMember primary = getPrimaryMemberForKey(r, key);
Set<? extends DistributedMember> owners = getAllForKey(r, key);
if (primary != null) {
owners.remove(primary);
}
return Collections.unmodifiableSet(owners);
}
/**
* Get all potential owners for a key. If the key exists in the Region, upon return there is no
* guarantee that it has not moved nor does it guarantee all members are still alive.
* <p>
* This method is not a substitute for {@link Region#containsKey(Object)}.
*
* @param r PartitionedRegion
* @param key the key to evaluate
* @throws IllegalStateException if the provided region is something other than a
* {@linkplain DataPolicy#PARTITION partitioned Region}
* @return an unmodifiable set of all members
* @since GemFire 6.0
*/
public static <K, V> Set<DistributedMember> getAllMembersForKey(final Region<K, V> r,
final K key) {
return Collections.unmodifiableSet(getAllForKey(r, key));
}
private static <K, V> Set<? extends DistributedMember> getAllForKey(final Region<K, V> r,
final K key) {
PartitionedRegion pr = isPartitionedCheck(r);
int bucketId = PartitionedRegionHelper.getHashKey(pr, null, key, null, null);
return pr.getRegionAdvisor().getBucketOwners(bucketId);
}
/**
* Given a RegionFunctionContext {@linkplain RegionFunctionContext#getDataSet() for a partitioned
* Region}, return a map of {@linkplain PartitionAttributesFactory#setColocatedWith(String)
* colocated Regions} with read access limited to the context of the function.
* <p>
* Writes using these Region have no constraints and behave the same as a partitioned Region.
* <p>
* If there are no colocated regions, return an empty map.
*
* @param c the region function context
* @throws IllegalStateException if the Region is not a {@linkplain DataPolicy#PARTITION
* partitioned Region}
* @return an unmodifiable map of {@linkplain Region#getFullPath() region name} to {@link Region}
* @since GemFire 6.0
*/
public static Map<String, Region<?, ?>> getLocalColocatedRegions(final RegionFunctionContext c) {
final Region r = c.getDataSet();
isPartitionedCheck(r);
final InternalRegionFunctionContext rfci = (InternalRegionFunctionContext) c;
Map ret = rfci.getColocatedLocalDataSets();
return ret;
}
/**
* Given a RegionFunctionContext {@linkplain RegionFunctionContext#getDataSet() for a partitioned
* Region}, return a Region providing read access limited to the function context.<br>
* Returned Region provides only one copy of the data although
* {@link PartitionAttributes#getRedundantCopies() redundantCopies} configured is more than 0. If
* the invoking Function is configured to have {@link Function#optimizeForWrite()
* optimizeForWrite} as true,the returned Region will only contain primary copy of the data.
* <p>
* Writes using this Region have no constraints and behave the same as a partitioned Region.
*
* @param c a functions context
* @throws IllegalStateException if {@link RegionFunctionContext#getDataSet()} returns something
* other than a {@linkplain DataPolicy#PARTITION partitioned Region}
* @return a Region for efficient reads
* @since GemFire 6.0
*/
public static <K, V> Region<K, V> getLocalDataForContext(final RegionFunctionContext c) {
final Region r = c.getDataSet();
isPartitionedCheck(r);
InternalRegionFunctionContext rfci = (InternalRegionFunctionContext) c;
return rfci.getLocalDataSet(r);
}
/**
* Given a partitioned Region return a Region providing read access limited to the local heap,
* writes using this Region have no constraints and behave the same as a partitioned Region.<br>
*
* @param r a partitioned region
* @throws IllegalStateException if the provided region is something other than a
* {@linkplain DataPolicy#PARTITION partitioned Region}
* @return a Region for efficient reads
* @since GemFire 6.0
*/
public static <K, V> Region<K, V> getLocalData(final Region<K, V> r) {
if (isPartitionedRegion(r)) {
PartitionedRegion pr = (PartitionedRegion) r;
final Set<Integer> buckets;
if (pr.getDataStore() != null) {
buckets = pr.getDataStore().getAllLocalBucketIds();
} else {
buckets = Collections.emptySet();
}
return new LocalDataSet(pr, buckets);
} else if (r instanceof LocalDataSet) {
return r;
} else {
throw new IllegalArgumentException(
LocalizedStrings.PartitionManager_REGION_0_IS_NOT_A_PARTITIONED_REGION
.toLocalizedString(r.getFullPath()));
}
}
/**
* Given a partitioned Region return a Region providing read access to primary copy of the data
* which is limited to the local heap, writes using this Region have no constraints and behave the
* same as a partitioned Region.<br>
*
* @param r a partitioned region
* @throws IllegalStateException if the provided region is something other than a
* {@linkplain DataPolicy#PARTITION partitioned Region}
* @return a Region for efficient reads
* @since GemFire 6.5
*/
public static <K, V> Region<K, V> getLocalPrimaryData(final Region<K, V> r) {
if (isPartitionedRegion(r)) {
PartitionedRegion pr = (PartitionedRegion) r;
final Set<Integer> buckets;
if (pr.getDataStore() != null) {
buckets = pr.getDataStore().getAllLocalPrimaryBucketIds();
} else {
buckets = Collections.emptySet();
}
return new LocalDataSet(pr, buckets);
} else if (r instanceof LocalDataSet) {
return r;
} else {
throw new IllegalArgumentException(
LocalizedStrings.PartitionManager_REGION_0_IS_NOT_A_PARTITIONED_REGION
.toLocalizedString(r.getFullPath()));
}
}
/**
* Moves the bucket which contains the given key from the source member to the destination member.
* The bucket will be fully transferred once this method is complete, if the method does not throw
* an exception.
* <p>
* All keys which exist in the same bucket will also be moved to the new node.
* <p>
* Any data in colocated regions that are colocated with this key will also be moved.
* <p>
* This method allows direct control of what data to move. To automatically balance buckets, see
* {@link ResourceManager#createRebalanceFactory()}
*
* @param region The region in which to move the bucket. Data in regions colocated with this
* region will also be moved.
* @param source A member that is currently hosting this bucket. The bucket is moved off of this
* member.
* @param destination A member that is not currently hosting this bucket, but has the partitioned
* region defined. The bucket is moved to this member.
* @param key A key which maps to the bucket to move. This key does not actually need to exist in
* the region, but if using a {@link PartitionResolver} the resolver should be able to get
* the routing object from this key to determine the bucket to move.
*
* @throws IllegalStateException if the bucket is not present on the source, if the source or
* destination are not valid members of the system, if the destination already hosts a
* copy of the bucket, or if the bucket does not exist.
*
* @since GemFire 7.1
*/
public static <K> void moveBucketByKey(Region<K, ?> region, DistributedMember source,
DistributedMember destination, K key) {
PartitionedRegion pr = isPartitionedCheck(region);
if (pr.isFixedPartitionedRegion()) {
throw new IllegalStateException("Cannot move data in a fixed partitioned region");
}
int bucketId = pr.getKeyInfo(key).getBucketId();
ExplicitMoveDirector director = new ExplicitMoveDirector(key, bucketId, source, destination,
region.getCache().getDistributedSystem());
PartitionedRegionRebalanceOp rebalance =
new PartitionedRegionRebalanceOp(pr, false, director, true, true);
rebalance.execute();
}
/**
* Moves data from the source member to the destination member, up to the given percentage of data
* (measured in bytes). The data will be fully transferred once this method is complete, if the
* method does not throw an exception. The percentage is a percentage of the amount of data in
* bytes on the source member for this region.
* <p>
*
* If this region has colocated regions, the colocated data will also be moved. The total amount
* of data in all colocated regions will be taken into consideration when determining what
* percentage of data will be moved.
* <p>
* It may not be possible to move data to the destination member, if the destination member has no
* available space, no bucket smaller than the given percentage exists, or if moving data would
* violate redundancy constraints. If data cannot be moved, this method will return a
* RebalanceResult object with 0 total bucket transfers.
* <p>
* This method allows direct control of what data to move. To automatically balance buckets, see
* {@link ResourceManager#createRebalanceFactory()}
*
* @param region The region in which to move data. Data in regions colocated with this region will
* also be moved.
* @param source A member that is currently hosting data. The bucket is moved off of this member.
* @param destination A member that that has the partitioned region defined. Data is moved to this
* member.
* @param percentage the maximum amount of data to move, as a percentage from 0 to 100.
*
* @throws IllegalStateException if the source or destination are not valid members of the system.
* @throws IllegalArgumentException if the percentage is not between 0 to 100.
*
* @return A RebalanceResult object that contains information about what what data was actually
* moved.
*
* @since GemFire 7.1
*/
public static RebalanceResults moveData(Region<?, ?> region, DistributedMember source,
DistributedMember destination, float percentage) {
PartitionedRegion pr = isPartitionedCheck(region);
if (pr.isFixedPartitionedRegion()) {
throw new IllegalStateException("Cannot move data in a fixed partitioned region");
}
if (percentage <= 0 || percentage > 100.0) {
throw new IllegalArgumentException("Percentage must be between 0 and 100");
}
PercentageMoveDirector director = new PercentageMoveDirector(source, destination, percentage);
PartitionedRegionRebalanceOp rebalance =
new PartitionedRegionRebalanceOp(pr, false, director, true, true);
Set<PartitionRebalanceInfo> results = rebalance.execute();
RebalanceResultsImpl rebalanceResults = new RebalanceResultsImpl();
for (PartitionRebalanceInfo details : results) {
rebalanceResults.addDetails(details);
}
return rebalanceResults;
}
}