diff --git a/bin/hbase b/bin/hbase index 559a02e9f8c0..59a3b6915321 100755 --- a/bin/hbase +++ b/bin/hbase @@ -92,7 +92,8 @@ if [ $# = 0 ]; then echo "Commands:" echo "Some commands take arguments. Pass no args or -h for usage." echo " shell Run the HBase shell" - echo " hbck Run the hbase 'fsck' tool" + echo " hbck Run the HBase 'fsck' tool. Defaults read-only hbck1." + echo " Pass '-j /path/to/HBCK2.jar' to run hbase-2.x HBCK2." echo " snapshot Tool for managing snapshots" if [ "${in_omnibus_tarball}" = "true" ]; then echo " wal Write-ahead-log analyzer" @@ -482,7 +483,25 @@ if [ "$COMMAND" = "shell" ] ; then HBASE_OPTS="$HBASE_OPTS $HBASE_SHELL_OPTS" CLASS="org.jruby.Main -X+O ${JRUBY_OPTS} ${HBASE_HOME}/bin/hirb.rb" elif [ "$COMMAND" = "hbck" ] ; then - CLASS='org.apache.hadoop.hbase.util.HBaseFsck' + # Look for the -j /path/to/HBCK2.jar parameter. Else pass through to hbck. + case "${1}" in + -j) + # Found -j parameter. Add arg to CLASSPATH and set CLASS to HBCK2. + shift + JAR="${1}" + if [ ! -f "${JAR}" ]; then + echo "${JAR} file not found!" + echo "Usage: hbase [] hbck -jar /path/to/HBCK2.jar []" + exit 1 + fi + CLASSPATH="${JAR}:${CLASSPATH}"; + CLASS="org.apache.hbase.HBCK2" + shift # past argument=value + ;; + *) + CLASS='org.apache.hadoop.hbase.util.HBaseFsck' + ;; + esac elif [ "$COMMAND" = "wal" ] ; then CLASS='org.apache.hadoop.hbase.wal.WALPrettyPrinter' elif [ "$COMMAND" = "hfile" ] ; then diff --git a/dev-support/Jenkinsfile b/dev-support/Jenkinsfile index bbff87cc0c91..b333afbd7f28 100644 --- a/dev-support/Jenkinsfile +++ b/dev-support/Jenkinsfile @@ -80,7 +80,8 @@ pipeline { if [[ true != "${USE_YETUS_PRERELEASE}" ]]; then YETUS_DIR="${WORKSPACE}/yetus-${YETUS_RELEASE}" echo "Checking for Yetus ${YETUS_RELEASE} in '${YETUS_DIR}'" - if [ ! -d "${YETUS_DIR}" ]; then + if ! "${YETUS_DIR}/bin/test-patch" --version >/dev/null 2>&1 ; then + rm -rf "${YETUS_DIR}" "${WORKSPACE}/component/dev-support/jenkins-scripts/cache-apache-project-artifact.sh" \ --working-dir "${WORKSPACE}/downloads-yetus" \ --keys 'https://www.apache.org/dist/yetus/KEYS' \ diff --git a/dev-support/submit-patch.py b/dev-support/submit-patch.py index f5d772fb3b6b..56955852f0e3 100755 --- a/dev-support/submit-patch.py +++ b/dev-support/submit-patch.py @@ -171,17 +171,16 @@ def validate_patch_dir(patch_dir): # - current branch is same as base branch # - current branch is ahead of base_branch by more than 1 commits def check_diff_between_branches(base_branch): - only_in_base_branch = git.log("HEAD.." + base_branch, oneline = True) - only_in_active_branch = git.log(base_branch + "..HEAD", oneline = True) + only_in_base_branch = list(repo.iter_commits("HEAD.." + base_branch)) + only_in_active_branch = list(repo.iter_commits(base_branch + "..HEAD")) if len(only_in_base_branch) != 0: log_fatal_and_exit(" '%s' is ahead of current branch by %s commits. Rebase " - "and try again.", base_branch, len(only_in_base_branch.split("\n"))) + "and try again.", base_branch, len(only_in_base_branch)) if len(only_in_active_branch) == 0: log_fatal_and_exit(" Current branch is same as '%s'. Exiting...", base_branch) - if len(only_in_active_branch.split("\n")) > 1: + if len(only_in_active_branch) > 1: log_fatal_and_exit(" Current branch is ahead of '%s' by %s commits. Squash into single " - "commit and try again.", - base_branch, len(only_in_active_branch.split("\n"))) + "commit and try again.", base_branch, len(only_in_active_branch)) # If ~/.apache-creds is present, load credentials from it otherwise prompt user. @@ -277,7 +276,7 @@ def get_review_board_id_if_present(issue_url, rb_link_title): # Use jira summary as review's summary too. summary = get_jira_summary(issue_url) # Use commit message as description. - description = git.log("-1", pretty="%B") + description = repo.head.commit.message update_draft_data = {"bugs_closed" : [args.jira_id.upper()], "target_groups" : "hbase", "target_people" : args.reviewers, "summary" : summary, "description" : description } diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/impl/IncrementalBackupManager.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/impl/IncrementalBackupManager.java index 3eebf4299c36..853f458456d0 100644 --- a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/impl/IncrementalBackupManager.java +++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/impl/IncrementalBackupManager.java @@ -296,9 +296,20 @@ private List getLogFilesForNewBackup(HashMap olderTimestam currentLogFile = log.getPath().toString(); resultLogFiles.add(currentLogFile); currentLogTS = BackupUtils.getCreationTime(log.getPath()); - // newestTimestamps is up-to-date with the current list of hosts - // so newestTimestamps.get(host) will not be null. - if (currentLogTS > newestTimestamps.get(host)) { + + // If newestTimestamps.get(host) is null, means that + // either RS (host) has been restarted recently with different port number + // or RS is down (was decommisioned). In any case, we treat this + // log file as eligible for inclusion into incremental backup log list + Long ts = newestTimestamps.get(host); + if (ts == null) { + LOG.warn("ORPHAN log found: " + log + " host=" + host); + LOG.debug("Known hosts (from newestTimestamps):"); + for (String s: newestTimestamps.keySet()) { + LOG.debug(s); + } + } + if (ts == null || currentLogTS > ts) { newestLogs.add(currentLogFile); } } @@ -343,7 +354,7 @@ private List getLogFilesForNewBackup(HashMap olderTimestam // Even if these logs belong to a obsolete region server, we still need // to include they to avoid loss of edits for backup. Long newTimestamp = newestTimestamps.get(host); - if (newTimestamp != null && currentLogTS > newTimestamp) { + if (newTimestamp == null || currentLogTS > newTimestamp) { newestLogs.add(currentLogFile); } } diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/mapreduce/MapReduceHFileSplitterJob.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/mapreduce/MapReduceHFileSplitterJob.java index 1a3c46528a48..b8d520c530c7 100644 --- a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/mapreduce/MapReduceHFileSplitterJob.java +++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/mapreduce/MapReduceHFileSplitterJob.java @@ -164,7 +164,7 @@ public static void main(String[] args) throws Exception { public int run(String[] args) throws Exception { if (args.length < 2) { usage("Wrong number of arguments: " + args.length); - System.exit(-1); + return -1; } Job job = createSubmittableJob(args); int result = job.waitForCompletion(true) ? 0 : 1; diff --git a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/util/BackupUtils.java b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/util/BackupUtils.java index 12f2f2f846bc..da11756fe024 100644 --- a/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/util/BackupUtils.java +++ b/hbase-backup/src/main/java/org/apache/hadoop/hbase/backup/util/BackupUtils.java @@ -327,7 +327,7 @@ public static void checkTargetDir(String backupRootPath, Configuration conf) thr if (expMsg.contains("No FileSystem for scheme")) { newMsg = "Unsupported filesystem scheme found in the backup target url. Error Message: " - + newMsg; + + expMsg; LOG.error(newMsg); throw new IOException(newMsg); } else { diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/Admin.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/Admin.java index a43a0b2dc0fa..08b44c930152 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/Admin.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/Admin.java @@ -1737,11 +1737,14 @@ HTableDescriptor[] getTableDescriptors(List names) /** * Abort a procedure. + * Do not use. Usually it is ignored but if not, it can do more damage than good. See hbck2. * @param procId ID of the procedure to abort * @param mayInterruptIfRunning if the proc completed at least one step, should it be aborted? * @return true if aborted, false if procedure already completed or does not exist * @throws IOException + * @deprecated Since 2.1.1 -- to be removed. */ + @Deprecated boolean abortProcedure( long procId, boolean mayInterruptIfRunning) throws IOException; @@ -1752,12 +1755,15 @@ boolean abortProcedure( * It may throw ExecutionException if there was an error while executing the operation * or TimeoutException in case the wait timeout was not long enough to allow the * operation to complete. + * Do not use. Usually it is ignored but if not, it can do more damage than good. See hbck2. * * @param procId ID of the procedure to abort * @param mayInterruptIfRunning if the proc completed at least one step, should it be aborted? * @return true if aborted, false if procedure already completed or does not exist * @throws IOException + * @deprecated Since 2.1.1 -- to be removed. */ + @Deprecated Future abortProcedureAsync( long procId, boolean mayInterruptIfRunning) throws IOException; diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncAdmin.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncAdmin.java index 739c78a46ddc..6bb253a20abd 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncAdmin.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncAdmin.java @@ -877,12 +877,15 @@ CompletableFuture isProcedureFinished(String signature, String instance Map props); /** - * abort a procedure + * Abort a procedure + * Do not use. Usually it is ignored but if not, it can do more damage than good. See hbck2. * @param procId ID of the procedure to abort * @param mayInterruptIfRunning if the proc completed at least one step, should it be aborted? * @return true if aborted, false if procedure already completed or does not exist. the value is * wrapped by {@link CompletableFuture} + * @deprecated Since 2.1.1 -- to be removed. */ + @Deprecated CompletableFuture abortProcedure(long procId, boolean mayInterruptIfRunning); /** diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncRequestFutureImpl.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncRequestFutureImpl.java index 80535a1691e4..b75b0c60bf06 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncRequestFutureImpl.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/AsyncRequestFutureImpl.java @@ -826,7 +826,9 @@ private void receiveMultiAction(MultiAction multiAction, byte[] regionName = regionEntry.getKey(); Throwable regionException = responses.getExceptions().get(regionName); - cleanServerCache(server, regionException); + if (regionException != null) { + cleanServerCache(server, regionException); + } Map regionResults = results.containsKey(regionName) ? results.get(regionName).result : Collections.emptyMap(); diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HBaseAdmin.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HBaseAdmin.java index c54ff1750b17..45961ff4886e 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HBaseAdmin.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HBaseAdmin.java @@ -25,6 +25,7 @@ import java.io.InterruptedIOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.Iterator; @@ -230,7 +231,6 @@ * @see Admin */ @InterfaceAudience.Private -@InterfaceStability.Evolving public class HBaseAdmin implements Admin { private static final Logger LOG = LoggerFactory.getLogger(HBaseAdmin.class); @@ -4314,15 +4314,13 @@ public Void call() throws Exception { } @Override - public List clearDeadServers(final List servers) throws IOException { - if (servers == null || servers.size() == 0) { - throw new IllegalArgumentException("servers cannot be null or empty"); - } + public List clearDeadServers(List servers) throws IOException { return executeCallable(new MasterCallable>(getConnection(), getRpcControllerFactory()) { @Override protected List rpcCall() throws Exception { - ClearDeadServersRequest req = RequestConverter.buildClearDeadServersRequest(servers); + ClearDeadServersRequest req = RequestConverter. + buildClearDeadServersRequest(servers == null? Collections.EMPTY_LIST: servers); return ProtobufUtil.toServerNameList( master.clearDeadServers(getRpcController(), req).getServerNameList()); } diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HBaseHbck.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HBaseHbck.java index 03a6f69c2218..2d088253a4bc 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HBaseHbck.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HBaseHbck.java @@ -18,25 +18,38 @@ package org.apache.hadoop.hbase.client; import java.io.IOException; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; + import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hbase.ipc.RpcControllerFactory; -import org.apache.yetus.audience.InterfaceAudience; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.apache.hbase.thirdparty.com.google.protobuf.ServiceException; +import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil; import org.apache.hadoop.hbase.shaded.protobuf.RequestConverter; +import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos; import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.GetTableStateResponse; import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.HbckService.BlockingInterface; +import org.apache.hbase.thirdparty.com.google.protobuf.ServiceException; + +import org.apache.yetus.audience.InterfaceAudience; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Use {@link ClusterConnection#getHbck()} to obtain an instance of {@link Hbck} instead of - * constructing - * an HBaseHbck directly. This will be mostly used by hbck tool. + * constructing an HBaseHbck directly. * *

Connection should be an unmanaged connection obtained via * {@link ConnectionFactory#createConnection(Configuration)}.

* + *

NOTE: The methods in here can do damage to a cluster if applied in the wrong sequence or at + * the wrong time. Use with caution. For experts only. These methods are only for the + * extreme case where the cluster has been damaged or has achieved an inconsistent state because + * of some unforeseen circumstance or bug and requires manual intervention. + * *

An instance of this class is lightweight and not-thread safe. A new instance should be created * by each thread. Pooling or caching of the instance is not recommended.

* @@ -75,10 +88,6 @@ public boolean isAborted() { return this.aborted; } - /** - * NOTE: This is a dangerous action, as existing running procedures for the table or regions - * which belong to the table may get confused. - */ @Override public TableState setTableStateInMeta(TableState state) throws IOException { try { @@ -87,9 +96,62 @@ public TableState setTableStateInMeta(TableState state) throws IOException { RequestConverter.buildSetTableStateInMetaRequest(state)); return TableState.convert(state.getTableName(), response.getTableState()); } catch (ServiceException se) { - LOG.debug("ServiceException while updating table state in meta. table={}, state={}", - state.getTableName(), state.getState()); + LOG.debug("table={}, state={}", state.getTableName(), state.getState(), se); + throw new IOException(se); + } + } + + @Override + public List assigns(List encodedRegionNames, boolean override) + throws IOException { + try { + MasterProtos.AssignsResponse response = + this.hbck.assigns(rpcControllerFactory.newController(), + RequestConverter.toAssignRegionsRequest(encodedRegionNames, override)); + return response.getPidList(); + } catch (ServiceException se) { + LOG.debug(toCommaDelimitedString(encodedRegionNames), se); + throw new IOException(se); + } + } + + @Override + public List unassigns(List encodedRegionNames, boolean override) + throws IOException { + try { + MasterProtos.UnassignsResponse response = + this.hbck.unassigns(rpcControllerFactory.newController(), + RequestConverter.toUnassignRegionsRequest(encodedRegionNames, override)); + return response.getPidList(); + } catch (ServiceException se) { + LOG.debug(toCommaDelimitedString(encodedRegionNames), se); throw new IOException(se); } } + + private static String toCommaDelimitedString(List list) { + return list.stream().collect(Collectors.joining(", ")); + } + + @Override + public List bypassProcedure(List pids, long waitTime, boolean override, + boolean recursive) + throws IOException { + MasterProtos.BypassProcedureResponse response = ProtobufUtil.call( + new Callable() { + @Override + public MasterProtos.BypassProcedureResponse call() throws Exception { + try { + return hbck.bypassProcedure(rpcControllerFactory.newController(), + MasterProtos.BypassProcedureRequest.newBuilder().addAllProcId(pids). + setWaitTime(waitTime).setOverride(override).setRecursive(recursive).build()); + } catch (Throwable t) { + LOG.error(pids.stream().map(i -> i.toString()). + collect(Collectors.joining(", ")), t); + throw t; + } + } + }); + return response.getBypassedList(); + } } diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HTable.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HTable.java index 69ec3661c5a6..fb69a2530bf5 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HTable.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/HTable.java @@ -487,35 +487,20 @@ public static void doBatchWithCallback(List actions, Object[] } @Override - public void delete(final Delete delete) - throws IOException { - CancellableRegionServerCallable callable = - new CancellableRegionServerCallable( - connection, getName(), delete.getRow(), this.rpcControllerFactory.newController(), - writeRpcTimeoutMs, new RetryingTimeTracker().start(), delete.getPriority()) { + public void delete(final Delete delete) throws IOException { + ClientServiceCallable callable = + new ClientServiceCallable(this.connection, getName(), delete.getRow(), + this.rpcControllerFactory.newController(), delete.getPriority()) { @Override - protected SingleResponse rpcCall() throws Exception { - MutateRequest request = RequestConverter.buildMutateRequest( - getLocation().getRegionInfo().getRegionName(), delete); - MutateResponse response = doMutate(request); - return ResponseConverter.getResult(request, response, getRpcControllerCellScanner()); + protected Void rpcCall() throws Exception { + MutateRequest request = RequestConverter + .buildMutateRequest(getLocation().getRegionInfo().getRegionName(), delete); + doMutate(request); + return null; } }; - List rows = Collections.singletonList(delete); - AsyncProcessTask task = AsyncProcessTask.newBuilder() - .setPool(pool) - .setTableName(tableName) - .setRowAccess(rows) - .setCallable(callable) - .setRpcTimeout(writeRpcTimeoutMs) - .setOperationTimeout(operationTimeoutMs) - .setSubmittedRows(AsyncProcessTask.SubmittedRows.ALL) - .build(); - AsyncRequestFuture ars = multiAp.submit(task); - ars.waitUntilDone(); - if (ars.hasError()) { - throw ars.getErrors(); - } + rpcCallerFactory.newCaller(this.writeRpcTimeoutMs) + .callWithRetries(callable, this.operationTimeoutMs); } @Override diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/Hbck.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/Hbck.java index a216cdbc6b7d..5c97d97daabd 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/client/Hbck.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/client/Hbck.java @@ -19,32 +19,86 @@ import java.io.Closeable; import java.io.IOException; +import java.util.List; + import org.apache.hadoop.hbase.Abortable; import org.apache.hadoop.hbase.HBaseInterfaceAudience; import org.apache.yetus.audience.InterfaceAudience; /** - * Hbck APIs for HBase. Obtain an instance from {@link ClusterConnection#getHbck()} and call + * Hbck fixup tool APIs. Obtain an instance from {@link ClusterConnection#getHbck()} and call * {@link #close()} when done. - *

Hbck client APIs will be mostly used by hbck tool which in turn can be used by operators to - * fix HBase and bringging it to consistent state.

+ *

WARNING: the below methods can damage the cluster. It may leave the cluster in an + * indeterminate state, e.g. region not assigned, or some hdfs files left behind. After running + * any of the below, operators may have to do some clean up on hdfs or schedule some assign + * procedures to get regions back online. DO AT YOUR OWN RISK. For experienced users only. * * @see ConnectionFactory * @see ClusterConnection - * @since 2.2.0 + * @since 2.0.2, 2.1.1 */ @InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.HBCK) public interface Hbck extends Abortable, Closeable { /** - * Update table state in Meta only. No procedures are submitted to open/ assign or close/ - * unassign regions of the table. This is useful only when some procedures/ actions are stuck - * beause of inconsistency between region and table states. - * - * NOTE: This is a dangerous action, as existing running procedures for the table or regions - * which belong to the table may get confused. - * + * Update table state in Meta only. No procedures are submitted to open/assign or + * close/unassign regions of the table. * @param state table state * @return previous state of the table in Meta */ TableState setTableStateInMeta(TableState state) throws IOException; + + /** + * Like {@link Admin#assign(byte[])} but 'raw' in that it can do more than one Region at a time + * -- good if many Regions to online -- and it will schedule the assigns even in the case where + * Master is initializing (as long as the ProcedureExecutor is up). Does NOT call Coprocessor + * hooks. + * @param override You need to add the override for case where a region has previously been + * bypassed. When a Procedure has been bypassed, a Procedure will have completed + * but no other Procedure will be able to make progress on the target entity + * (intentionally). This override flag will override this fencing mechanism. + * @param encodedRegionNames Region encoded names; e.g. 1588230740 is the hard-coded encoding + * for hbase:meta region and de00010733901a05f5a2a3a382e27dd4 is an + * example of what a random user-space encoded Region name looks like. + */ + List assigns(List encodedRegionNames, boolean override) throws IOException; + + default List assigns(List encodedRegionNames) throws IOException { + return assigns(encodedRegionNames, false); + } + + /** + * Like {@link Admin#unassign(byte[], boolean)} but 'raw' in that it can do more than one Region + * at a time -- good if many Regions to offline -- and it will schedule the assigns even in the + * case where Master is initializing (as long as the ProcedureExecutor is up). Does NOT call + * Coprocessor hooks. + * @param override You need to add the override for case where a region has previously been + * bypassed. When a Procedure has been bypassed, a Procedure will have completed + * but no other Procedure will be able to make progress on the target entity + * (intentionally). This override flag will override this fencing mechanism. + * @param encodedRegionNames Region encoded names; e.g. 1588230740 is the hard-coded encoding + * for hbase:meta region and de00010733901a05f5a2a3a382e27dd4 is an + * example of what a random user-space encoded Region name looks like. + */ + List unassigns(List encodedRegionNames, boolean override) throws IOException; + + default List unassigns(List encodedRegionNames) throws IOException { + return unassigns(encodedRegionNames, false); + } + + /** + * Bypass specified procedure and move it to completion. Procedure is marked completed but + * no actual work is done from the current state/step onwards. Parents of the procedure are + * also marked for bypass. + * + * @param pids of procedures to complete. + * @param waitTime wait time in ms for acquiring lock for a procedure + * @param override if override set to true, we will bypass the procedure even if it is executing. + * This is for procedures which can't break out during execution (bugs?). + * @param recursive If set, if a parent procedure, we will find and bypass children and then + * the parent procedure (Dangerous but useful in case where child procedure has been 'lost'). + * Does not always work. Experimental. + * @return true if procedure is marked for bypass successfully, false otherwise + */ + List bypassProcedure(List pids, long waitTime, boolean override, boolean recursive) + throws IOException; } diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/filter/FuzzyRowFilter.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/filter/FuzzyRowFilter.java index d1acbba9811a..abfa7e1d6e8c 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/filter/FuzzyRowFilter.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/filter/FuzzyRowFilter.java @@ -35,7 +35,6 @@ import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.BytesBytesPair; import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.util.Pair; -import org.apache.hadoop.hbase.util.UnsafeAccess; import org.apache.hadoop.hbase.util.UnsafeAvailChecker; import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting; @@ -352,9 +351,9 @@ static SatisfiesCode satisfies(boolean reverse, byte[] row, int offset, int leng int j = numWords << 3; // numWords * SIZEOF_LONG; for (int i = 0; i < j; i += Bytes.SIZEOF_LONG) { - long fuzzyBytes = UnsafeAccess.toLong(fuzzyKeyBytes, i); - long fuzzyMeta = UnsafeAccess.toLong(fuzzyKeyMeta, i); - long rowValue = UnsafeAccess.toLong(row, offset + i); + long fuzzyBytes = Bytes.toLong(fuzzyKeyBytes, i); + long fuzzyMeta = Bytes.toLong(fuzzyKeyMeta, i); + long rowValue = Bytes.toLong(row, offset + i); if ((rowValue & fuzzyMeta) != (fuzzyBytes)) { // We always return NEXT_EXISTS return SatisfiesCode.NEXT_EXISTS; @@ -364,9 +363,9 @@ static SatisfiesCode satisfies(boolean reverse, byte[] row, int offset, int leng int off = j; if (length - off >= Bytes.SIZEOF_INT) { - int fuzzyBytes = UnsafeAccess.toInt(fuzzyKeyBytes, off); - int fuzzyMeta = UnsafeAccess.toInt(fuzzyKeyMeta, off); - int rowValue = UnsafeAccess.toInt(row, offset + off); + int fuzzyBytes = Bytes.toInt(fuzzyKeyBytes, off); + int fuzzyMeta = Bytes.toInt(fuzzyKeyMeta, off); + int rowValue = Bytes.toInt(row, offset + off); if ((rowValue & fuzzyMeta) != (fuzzyBytes)) { // We always return NEXT_EXISTS return SatisfiesCode.NEXT_EXISTS; @@ -375,9 +374,9 @@ static SatisfiesCode satisfies(boolean reverse, byte[] row, int offset, int leng } if (length - off >= Bytes.SIZEOF_SHORT) { - short fuzzyBytes = UnsafeAccess.toShort(fuzzyKeyBytes, off); - short fuzzyMeta = UnsafeAccess.toShort(fuzzyKeyMeta, off); - short rowValue = UnsafeAccess.toShort(row, offset + off); + short fuzzyBytes = Bytes.toShort(fuzzyKeyBytes, off); + short fuzzyMeta = Bytes.toShort(fuzzyKeyMeta, off); + short rowValue = Bytes.toShort(row, offset + off); if ((rowValue & fuzzyMeta) != (fuzzyBytes)) { // We always return NEXT_EXISTS // even if it does not (in this case getNextForFuzzyRule diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/regionserver/BloomType.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/regionserver/BloomType.java index 778784232598..c0b2dd204126 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/regionserver/BloomType.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/regionserver/BloomType.java @@ -34,5 +34,13 @@ public enum BloomType { /** * Bloom enabled with Table row & column (family+qualifier) as Key */ - ROWCOL + ROWCOL, + /** + * Bloom enabled with Table row prefix as Key, specify the length of the prefix + */ + ROWPREFIX_FIXED_LENGTH, + /** + * Bloom enabled with Table row prefix as Key, specify the delimiter of the prefix + */ + ROWPREFIX_DELIMITED } diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/regionserver/InvalidMutationDurabilityException.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/regionserver/InvalidMutationDurabilityException.java new file mode 100644 index 000000000000..d694f3774f34 --- /dev/null +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/regionserver/InvalidMutationDurabilityException.java @@ -0,0 +1,44 @@ +/** + * + * 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.hadoop.hbase.regionserver; + +import org.apache.hadoop.hbase.DoNotRetryIOException; +import org.apache.hadoop.hbase.client.Durability; +import org.apache.yetus.audience.InterfaceAudience; + +/** + * Thrown if Mutation's {@link Durability} is skip wal but table need replication. + */ +@InterfaceAudience.Public +public class InvalidMutationDurabilityException extends DoNotRetryIOException { + + /** + * default constructor + */ + public InvalidMutationDurabilityException() { + super(); + } + + /** + * @param message exception message + */ + public InvalidMutationDurabilityException(String message) { + super(message); + } +} diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/SecurityInfo.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/SecurityInfo.java index 8fca997fdc83..eb9d209be2fa 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/security/SecurityInfo.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/security/SecurityInfo.java @@ -20,12 +20,13 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import org.apache.yetus.audience.InterfaceAudience; -import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos; import org.apache.hadoop.hbase.protobuf.generated.AuthenticationProtos.TokenIdentifier.Kind; +import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos; +import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos; import org.apache.hadoop.hbase.shaded.protobuf.generated.ClientProtos; import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.MasterService; import org.apache.hadoop.hbase.shaded.protobuf.generated.RegionServerStatusProtos; +import org.apache.yetus.audience.InterfaceAudience; /** * Maps RPC protocol interfaces to required configuration @@ -37,13 +38,19 @@ public class SecurityInfo { // populate info for known services static { infos.put(AdminProtos.AdminService.getDescriptor().getName(), - new SecurityInfo("hbase.regionserver.kerberos.principal", Kind.HBASE_AUTH_TOKEN)); + new SecurityInfo("hbase.regionserver.kerberos.principal", + Kind.HBASE_AUTH_TOKEN)); infos.put(ClientProtos.ClientService.getDescriptor().getName(), - new SecurityInfo("hbase.regionserver.kerberos.principal", Kind.HBASE_AUTH_TOKEN)); + new SecurityInfo("hbase.regionserver.kerberos.principal", + Kind.HBASE_AUTH_TOKEN)); infos.put(MasterService.getDescriptor().getName(), new SecurityInfo("hbase.master.kerberos.principal", Kind.HBASE_AUTH_TOKEN)); infos.put(RegionServerStatusProtos.RegionServerStatusService.getDescriptor().getName(), new SecurityInfo("hbase.master.kerberos.principal", Kind.HBASE_AUTH_TOKEN)); + infos.put(MasterProtos.HbckService.getDescriptor().getName(), + new SecurityInfo("hbase.master.kerberos.principal", Kind.HBASE_AUTH_TOKEN)); + // NOTE: IF ADDING A NEW SERVICE, BE SURE TO UPDATE HBasePolicyProvider ALSO ELSE + // new Service will not be found when all is Kerberized!!!! } /** diff --git a/hbase-client/src/main/java/org/apache/hadoop/hbase/shaded/protobuf/RequestConverter.java b/hbase-client/src/main/java/org/apache/hadoop/hbase/shaded/protobuf/RequestConverter.java index 41abf97696f5..1253974e0c42 100644 --- a/hbase-client/src/main/java/org/apache/hadoop/hbase/shaded/protobuf/RequestConverter.java +++ b/hbase-client/src/main/java/org/apache/hadoop/hbase/shaded/protobuf/RequestConverter.java @@ -25,6 +25,7 @@ import java.util.Map; import java.util.Set; import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.apache.hadoop.hbase.CellScannable; import org.apache.hadoop.hbase.ClusterMetrics.Option; @@ -1888,4 +1889,27 @@ private static List toProtoServerNames(List return TransitReplicationPeerSyncReplicationStateRequest.newBuilder().setPeerId(peerId) .setSyncReplicationState(ReplicationPeerConfigUtil.toSyncReplicationState(state)).build(); } + + // HBCK2 + public static MasterProtos.AssignsRequest toAssignRegionsRequest( + List encodedRegionNames, boolean override) { + MasterProtos.AssignsRequest.Builder b = MasterProtos.AssignsRequest.newBuilder(); + return b.addAllRegion(toEncodedRegionNameRegionSpecifiers(encodedRegionNames)). + setOverride(override).build(); + } + + public static MasterProtos.UnassignsRequest toUnassignRegionsRequest( + List encodedRegionNames, boolean override) { + MasterProtos.UnassignsRequest.Builder b = + MasterProtos.UnassignsRequest.newBuilder(); + return b.addAllRegion(toEncodedRegionNameRegionSpecifiers(encodedRegionNames)). + setOverride(override).build(); + } + + private static List toEncodedRegionNameRegionSpecifiers( + List encodedRegionNames) { + return encodedRegionNames.stream(). + map(r -> buildRegionSpecifier(RegionSpecifierType.ENCODED_REGION_NAME, Bytes.toBytes(r))). + collect(Collectors.toList()); + } } diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/HConstants.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/HConstants.java index beb65faa7214..fbfab4b60bcf 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/HConstants.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/HConstants.java @@ -1325,6 +1325,7 @@ public enum OperationStatusCode { "hbase.regionserver.region.split.threads.max"; /** Canary config keys */ + // TODO: Move these defines to Canary Class public static final String HBASE_CANARY_WRITE_DATA_TTL_KEY = "hbase.canary.write.data.ttl"; public static final String HBASE_CANARY_WRITE_PERSERVER_REGIONS_LOWERLIMIT_KEY = diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/ByteBufferUtils.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/ByteBufferUtils.java index 11745a4d96d3..deb667b6727d 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/ByteBufferUtils.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/ByteBufferUtils.java @@ -55,7 +55,267 @@ public final class ByteBufferUtils { private ByteBufferUtils() { } - /** + + static abstract class Comparer { + abstract int compareTo(byte [] buf1, int o1, int l1, ByteBuffer buf2, int o2, int l2); + abstract int compareTo(ByteBuffer buf1, int o1, int l1, ByteBuffer buf2, int o2, int l2); + } + + static abstract class Converter { + abstract short toShort(ByteBuffer buffer, int offset); + abstract int toInt(ByteBuffer buffer); + abstract int toInt(ByteBuffer buffer, int offset); + abstract long toLong(ByteBuffer buffer, int offset); + abstract void putInt(ByteBuffer buffer, int val); + abstract int putInt(ByteBuffer buffer, int index, int val); + abstract void putShort(ByteBuffer buffer, short val); + abstract int putShort(ByteBuffer buffer, int index, short val); + abstract void putLong(ByteBuffer buffer, long val); + abstract int putLong(ByteBuffer buffer, int index, long val); + } + + static class ComparerHolder { + static final String UNSAFE_COMPARER_NAME = ComparerHolder.class.getName() + "$UnsafeComparer"; + + static final Comparer BEST_COMPARER = getBestComparer(); + + static Comparer getBestComparer() { + try { + Class theClass = Class.forName(UNSAFE_COMPARER_NAME); + + @SuppressWarnings("unchecked") + Comparer comparer = (Comparer) theClass.getConstructor().newInstance(); + return comparer; + } catch (Throwable t) { // ensure we really catch *everything* + return PureJavaComparer.INSTANCE; + } + } + + static final class PureJavaComparer extends Comparer { + static final PureJavaComparer INSTANCE = new PureJavaComparer(); + + private PureJavaComparer() {} + + @Override + public int compareTo(byte [] buf1, int o1, int l1, ByteBuffer buf2, int o2, int l2) { + int end1 = o1 + l1; + int end2 = o2 + l2; + for (int i = o1, j = o2; i < end1 && j < end2; i++, j++) { + int a = buf1[i] & 0xFF; + int b = buf2.get(j) & 0xFF; + if (a != b) { + return a - b; + } + } + return l1 - l2; + } + + @Override + public int compareTo(ByteBuffer buf1, int o1, int l1, ByteBuffer buf2, int o2, int l2) { + int end1 = o1 + l1; + int end2 = o2 + l2; + for (int i = o1, j = o2; i < end1 && j < end2; i++, j++) { + int a = buf1.get(i) & 0xFF; + int b = buf2.get(j) & 0xFF; + if (a != b) { + return a - b; + } + } + return l1 - l2; + } + } + + static final class UnsafeComparer extends Comparer { + + public UnsafeComparer() {} + + static { + if(!UNSAFE_UNALIGNED) { + throw new Error(); + } + } + + @Override + public int compareTo(byte[] buf1, int o1, int l1, ByteBuffer buf2, int o2, int l2) { + long offset2Adj; + Object refObj2 = null; + if (buf2.isDirect()) { + offset2Adj = o2 + ((DirectBuffer)buf2).address(); + } else { + offset2Adj = o2 + buf2.arrayOffset() + UnsafeAccess.BYTE_ARRAY_BASE_OFFSET; + refObj2 = buf2.array(); + } + return compareToUnsafe(buf1, o1 + UnsafeAccess.BYTE_ARRAY_BASE_OFFSET, l1, + refObj2, offset2Adj, l2); + } + + @Override + public int compareTo(ByteBuffer buf1, int o1, int l1, ByteBuffer buf2, int o2, int l2) { + long offset1Adj, offset2Adj; + Object refObj1 = null, refObj2 = null; + if (buf1.isDirect()) { + offset1Adj = o1 + ((DirectBuffer) buf1).address(); + } else { + offset1Adj = o1 + buf1.arrayOffset() + UnsafeAccess.BYTE_ARRAY_BASE_OFFSET; + refObj1 = buf1.array(); + } + if (buf2.isDirect()) { + offset2Adj = o2 + ((DirectBuffer) buf2).address(); + } else { + offset2Adj = o2 + buf2.arrayOffset() + UnsafeAccess.BYTE_ARRAY_BASE_OFFSET; + refObj2 = buf2.array(); + } + return compareToUnsafe(refObj1, offset1Adj, l1, refObj2, offset2Adj, l2); + } + } + } + + + static class ConverterHolder { + static final String UNSAFE_CONVERTER_NAME = + ConverterHolder.class.getName() + "$UnsafeConverter"; + static final Converter BEST_CONVERTER = getBestConverter(); + + static Converter getBestConverter() { + try { + Class theClass = Class.forName(UNSAFE_CONVERTER_NAME); + + // yes, UnsafeComparer does implement Comparer + @SuppressWarnings("unchecked") + Converter converter = (Converter) theClass.getConstructor().newInstance(); + return converter; + } catch (Throwable t) { // ensure we really catch *everything* + return PureJavaConverter.INSTANCE; + } + } + + static final class PureJavaConverter extends Converter { + static final PureJavaConverter INSTANCE = new PureJavaConverter(); + + private PureJavaConverter() {} + + @Override + short toShort(ByteBuffer buffer, int offset) { + return buffer.getShort(offset); + } + + @Override + int toInt(ByteBuffer buffer) { + return buffer.getInt(); + } + + @Override + int toInt(ByteBuffer buffer, int offset) { + return buffer.getInt(offset); + } + + @Override + long toLong(ByteBuffer buffer, int offset) { + return buffer.getLong(offset); + } + + @Override + void putInt(ByteBuffer buffer, int val) { + buffer.putInt(val); + } + + @Override + int putInt(ByteBuffer buffer, int index, int val) { + buffer.putInt(index, val); + return index + Bytes.SIZEOF_INT; + } + + @Override + void putShort(ByteBuffer buffer, short val) { + buffer.putShort(val); + } + + @Override + int putShort(ByteBuffer buffer, int index, short val) { + buffer.putShort(index, val); + return index + Bytes.SIZEOF_SHORT; + } + + @Override + void putLong(ByteBuffer buffer, long val) { + buffer.putLong(val); + } + + @Override + int putLong(ByteBuffer buffer, int index, long val) { + buffer.putLong(index, val); + return index + Bytes.SIZEOF_LONG; + } + } + + static final class UnsafeConverter extends Converter { + + public UnsafeConverter() {} + + static { + if(!UNSAFE_UNALIGNED) { + throw new Error(); + } + } + + @Override + short toShort(ByteBuffer buffer, int offset) { + return UnsafeAccess.toShort(buffer, offset); + } + + @Override + int toInt(ByteBuffer buffer) { + int i = UnsafeAccess.toInt(buffer, buffer.position()); + buffer.position(buffer.position() + Bytes.SIZEOF_INT); + return i; + } + + @Override + int toInt(ByteBuffer buffer, int offset) { + return UnsafeAccess.toInt(buffer, offset); + } + + @Override + long toLong(ByteBuffer buffer, int offset) { + return UnsafeAccess.toLong(buffer, offset); + } + + @Override + void putInt(ByteBuffer buffer, int val) { + int newPos = UnsafeAccess.putInt(buffer, buffer.position(), val); + buffer.position(newPos); + } + + @Override + int putInt(ByteBuffer buffer, int index, int val) { + return UnsafeAccess.putInt(buffer, index, val); + } + + @Override + void putShort(ByteBuffer buffer, short val) { + int newPos = UnsafeAccess.putShort(buffer, buffer.position(), val); + buffer.position(newPos); + } + + @Override + int putShort(ByteBuffer buffer, int index, short val) { + return UnsafeAccess.putShort(buffer, index, val); + } + + @Override + void putLong(ByteBuffer buffer, long val) { + int newPos = UnsafeAccess.putLong(buffer, buffer.position(), val); + buffer.position(newPos); + } + + @Override + int putLong(ByteBuffer buffer, int index, long val) { + return UnsafeAccess.putLong(buffer, index, val); + } + } + } + + /** * Similar to {@link WritableUtils#writeVLong(java.io.DataOutput, long)}, * but writes to a {@link ByteBuffer}. */ @@ -629,35 +889,7 @@ public static int hashCode(ByteBuffer buf, int offset, int length) { } public static int compareTo(ByteBuffer buf1, int o1, int l1, ByteBuffer buf2, int o2, int l2) { - // NOTE: This method is copied over in BBKVComparator!!!!! For perf reasons. If you make - // changes here, make them there too!!!! - if (UNSAFE_UNALIGNED) { - long offset1Adj, offset2Adj; - Object refObj1 = null, refObj2 = null; - if (buf1.isDirect()) { - offset1Adj = o1 + ((DirectBuffer) buf1).address(); - } else { - offset1Adj = o1 + buf1.arrayOffset() + UnsafeAccess.BYTE_ARRAY_BASE_OFFSET; - refObj1 = buf1.array(); - } - if (buf2.isDirect()) { - offset2Adj = o2 + ((DirectBuffer) buf2).address(); - } else { - offset2Adj = o2 + buf2.arrayOffset() + UnsafeAccess.BYTE_ARRAY_BASE_OFFSET; - refObj2 = buf2.array(); - } - return compareToUnsafe(refObj1, offset1Adj, l1, refObj2, offset2Adj, l2); - } - int end1 = o1 + l1; - int end2 = o2 + l2; - for (int i = o1, j = o2; i < end1 && j < end2; i++, j++) { - int a = buf1.get(i) & 0xFF; - int b = buf2.get(j) & 0xFF; - if (a != b) { - return a - b; - } - } - return l1 - l2; + return ComparerHolder.BEST_COMPARER.compareTo(buf1, o1, l1, buf2, o2, l2); } public static boolean equals(ByteBuffer buf1, int o1, int l1, byte[] buf2, int o2, int l2) { @@ -678,53 +910,11 @@ public static boolean equals(ByteBuffer buf1, int o1, int l1, byte[] buf2, int o // of compiled code via jitwatch). public static int compareTo(byte [] buf1, int o1, int l1, ByteBuffer buf2, int o2, int l2) { - if (UNSAFE_UNALIGNED) { - long offset2Adj; - Object refObj2 = null; - if (buf2.isDirect()) { - offset2Adj = o2 + ((DirectBuffer)buf2).address(); - } else { - offset2Adj = o2 + buf2.arrayOffset() + UnsafeAccess.BYTE_ARRAY_BASE_OFFSET; - refObj2 = buf2.array(); - } - return compareToUnsafe(buf1, o1 + UnsafeAccess.BYTE_ARRAY_BASE_OFFSET, l1, - refObj2, offset2Adj, l2); - } - int end1 = o1 + l1; - int end2 = o2 + l2; - for (int i = o1, j = o2; i < end1 && j < end2; i++, j++) { - int a = buf1[i] & 0xFF; - int b = buf2.get(j) & 0xFF; - if (a != b) { - return a - b; - } - } - return l1 - l2; + return ComparerHolder.BEST_COMPARER.compareTo(buf1, o1, l1, buf2, o2, l2); } public static int compareTo(ByteBuffer buf1, int o1, int l1, byte[] buf2, int o2, int l2) { - if (UNSAFE_UNALIGNED) { - long offset1Adj; - Object refObj1 = null; - if (buf1.isDirect()) { - offset1Adj = o1 + ((DirectBuffer) buf1).address(); - } else { - offset1Adj = o1 + buf1.arrayOffset() + UnsafeAccess.BYTE_ARRAY_BASE_OFFSET; - refObj1 = buf1.array(); - } - return compareToUnsafe(refObj1, offset1Adj, l1, - buf2, o2 + UnsafeAccess.BYTE_ARRAY_BASE_OFFSET, l2); - } - int end1 = o1 + l1; - int end2 = o2 + l2; - for (int i = o1, j = o2; i < end1 && j < end2; i++, j++) { - int a = buf1.get(i) & 0xFF; - int b = buf2[j] & 0xFF; - if (a != b) { - return a - b; - } - } - return l1 - l2; + return compareTo(buf2, o2, l2, buf1, o1, l1)*-1; } static int compareToUnsafe(Object obj1, long o1, int l1, Object obj2, long o2, int l2) { @@ -777,24 +967,14 @@ static int compareToUnsafe(Object obj1, long o1, int l1, Object obj2, long o2, i * @return short value at offset */ public static short toShort(ByteBuffer buffer, int offset) { - if (UNSAFE_UNALIGNED) { - return UnsafeAccess.toShort(buffer, offset); - } else { - return buffer.getShort(offset); - } + return ConverterHolder.BEST_CONVERTER.toShort(buffer, offset); } /** * Reads an int value at the given buffer's current position. Also advances the buffer's position */ public static int toInt(ByteBuffer buffer) { - if (UNSAFE_UNALIGNED) { - int i = UnsafeAccess.toInt(buffer, buffer.position()); - buffer.position(buffer.position() + Bytes.SIZEOF_INT); - return i; - } else { - return buffer.getInt(); - } + return ConverterHolder.BEST_CONVERTER.toInt(buffer); } /** @@ -804,11 +984,7 @@ public static int toInt(ByteBuffer buffer) { * @return int value at offset */ public static int toInt(ByteBuffer buffer, int offset) { - if (UNSAFE_UNALIGNED) { - return UnsafeAccess.toInt(buffer, offset); - } else { - return buffer.getInt(offset); - } + return ConverterHolder.BEST_CONVERTER.toInt(buffer, offset); } /** @@ -841,11 +1017,7 @@ public static int readAsInt(ByteBuffer buf, int offset, final int length) { * @return long value at offset */ public static long toLong(ByteBuffer buffer, int offset) { - if (UNSAFE_UNALIGNED) { - return UnsafeAccess.toLong(buffer, offset); - } else { - return buffer.getLong(offset); - } + return ConverterHolder.BEST_CONVERTER.toLong(buffer, offset); } /** @@ -855,20 +1027,11 @@ public static long toLong(ByteBuffer buffer, int offset) { * @param val int to write out */ public static void putInt(ByteBuffer buffer, int val) { - if (UNSAFE_UNALIGNED) { - int newPos = UnsafeAccess.putInt(buffer, buffer.position(), val); - buffer.position(newPos); - } else { - buffer.putInt(val); - } + ConverterHolder.BEST_CONVERTER.putInt(buffer, val); } public static int putInt(ByteBuffer buffer, int index, int val) { - if (UNSAFE_UNALIGNED) { - return UnsafeAccess.putInt(buffer, index, val); - } - buffer.putInt(index, val); - return index + Bytes.SIZEOF_INT; + return ConverterHolder.BEST_CONVERTER.putInt(buffer, index, val); } /** @@ -906,20 +1069,11 @@ public static BigDecimal toBigDecimal(ByteBuffer buffer, int offset, int length) * @param val short to write out */ public static void putShort(ByteBuffer buffer, short val) { - if (UNSAFE_UNALIGNED) { - int newPos = UnsafeAccess.putShort(buffer, buffer.position(), val); - buffer.position(newPos); - } else { - buffer.putShort(val); - } + ConverterHolder.BEST_CONVERTER.putShort(buffer, val); } public static int putShort(ByteBuffer buffer, int index, short val) { - if (UNSAFE_UNALIGNED) { - return UnsafeAccess.putShort(buffer, index, val); - } - buffer.putShort(index, val); - return index + Bytes.SIZEOF_SHORT; + return ConverterHolder.BEST_CONVERTER.putShort(buffer, index, val); } public static int putAsShort(ByteBuffer buf, int index, int val) { @@ -936,20 +1090,11 @@ public static int putAsShort(ByteBuffer buf, int index, int val) { * @param val long to write out */ public static void putLong(ByteBuffer buffer, long val) { - if (UNSAFE_UNALIGNED) { - int newPos = UnsafeAccess.putLong(buffer, buffer.position(), val); - buffer.position(newPos); - } else { - buffer.putLong(val); - } + ConverterHolder.BEST_CONVERTER.putLong(buffer, val); } public static int putLong(ByteBuffer buffer, int index, long val) { - if (UNSAFE_UNALIGNED) { - return UnsafeAccess.putLong(buffer, index, val); - } - buffer.putLong(index, val); - return index + Bytes.SIZEOF_LONG; + return ConverterHolder.BEST_CONVERTER.putLong(buffer, index, val); } /** diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/Bytes.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/Bytes.java index e63c0db5b2d6..dcc1266df92c 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/Bytes.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/Bytes.java @@ -130,7 +130,8 @@ public class Bytes implements Comparable { // SizeOf which uses java.lang.instrument says 24 bytes. (3 longs?) public static final int ESTIMATED_HEAP_TAX = 16; - private static final boolean UNSAFE_UNALIGNED = UnsafeAvailChecker.unaligned(); + @VisibleForTesting + static final boolean UNSAFE_UNALIGNED = UnsafeAvailChecker.unaligned(); /** * Returns length of the byte array, returning 0 if the array is null. @@ -811,16 +812,7 @@ public static long toLong(byte[] bytes, int offset, final int length) { if (length != SIZEOF_LONG || offset + length > bytes.length) { throw explainWrongLengthOrOffset(bytes, offset, length, SIZEOF_LONG); } - if (UNSAFE_UNALIGNED) { - return UnsafeAccess.toLong(bytes, offset); - } else { - long l = 0; - for(int i = offset; i < offset + length; i++) { - l <<= 8; - l ^= bytes[i] & 0xFF; - } - return l; - } + return ConverterHolder.BEST_CONVERTER.toLong(bytes, offset, length); } private static IllegalArgumentException @@ -852,16 +844,7 @@ public static int putLong(byte[] bytes, int offset, long val) { throw new IllegalArgumentException("Not enough room to put a long at" + " offset " + offset + " in a " + bytes.length + " byte array"); } - if (UNSAFE_UNALIGNED) { - return UnsafeAccess.putLong(bytes, offset, val); - } else { - for(int i = offset + 7; i > offset; i--) { - bytes[i] = (byte) val; - val >>>= 8; - } - bytes[offset] = (byte) val; - return offset + SIZEOF_LONG; - } + return ConverterHolder.BEST_CONVERTER.putLong(bytes, offset, val); } /** @@ -1003,16 +986,7 @@ public static int toInt(byte[] bytes, int offset, final int length) { if (length != SIZEOF_INT || offset + length > bytes.length) { throw explainWrongLengthOrOffset(bytes, offset, length, SIZEOF_INT); } - if (UNSAFE_UNALIGNED) { - return UnsafeAccess.toInt(bytes, offset); - } else { - int n = 0; - for(int i = offset; i < (offset + length); i++) { - n <<= 8; - n ^= bytes[i] & 0xFF; - } - return n; - } + return ConverterHolder.BEST_CONVERTER.toInt(bytes, offset, length); } /** @@ -1087,16 +1061,7 @@ public static int putInt(byte[] bytes, int offset, int val) { throw new IllegalArgumentException("Not enough room to put an int at" + " offset " + offset + " in a " + bytes.length + " byte array"); } - if (UNSAFE_UNALIGNED) { - return UnsafeAccess.putInt(bytes, offset, val); - } else { - for(int i= offset + 3; i > offset; i--) { - bytes[i] = (byte) val; - val >>>= 8; - } - bytes[offset] = (byte) val; - return offset + SIZEOF_INT; - } + return ConverterHolder.BEST_CONVERTER.putInt(bytes, offset, val); } /** @@ -1157,15 +1122,7 @@ public static short toShort(byte[] bytes, int offset, final int length) { if (length != SIZEOF_SHORT || offset + length > bytes.length) { throw explainWrongLengthOrOffset(bytes, offset, length, SIZEOF_SHORT); } - if (UNSAFE_UNALIGNED) { - return UnsafeAccess.toShort(bytes, offset); - } else { - short n = 0; - n = (short) ((n ^ bytes[offset]) & 0xFF); - n = (short) (n << 8); - n = (short) ((n ^ bytes[offset+1]) & 0xFF); - return n; - } + return ConverterHolder.BEST_CONVERTER.toShort(bytes, offset, length); } /** @@ -1195,14 +1152,7 @@ public static int putShort(byte[] bytes, int offset, short val) { throw new IllegalArgumentException("Not enough room to put a short at" + " offset " + offset + " in a " + bytes.length + " byte array"); } - if (UNSAFE_UNALIGNED) { - return UnsafeAccess.putShort(bytes, offset, val); - } else { - bytes[offset+1] = (byte) val; - val >>= 8; - bytes[offset] = (byte) val; - return offset + SIZEOF_SHORT; - } + return ConverterHolder.BEST_CONVERTER.putShort(bytes, offset, val); } /** @@ -1431,11 +1381,161 @@ int compareTo( ); } + static abstract class Converter { + abstract long toLong(byte[] bytes, int offset, int length); + abstract int putLong(byte[] bytes, int offset, long val); + + abstract int toInt(byte[] bytes, int offset, final int length); + abstract int putInt(byte[] bytes, int offset, int val); + + abstract short toShort(byte[] bytes, int offset, final int length); + abstract int putShort(byte[] bytes, int offset, short val); + + } + @VisibleForTesting static Comparer lexicographicalComparerJavaImpl() { return LexicographicalComparerHolder.PureJavaComparer.INSTANCE; } + static class ConverterHolder { + static final String UNSAFE_CONVERTER_NAME = + ConverterHolder.class.getName() + "$UnsafeConverter"; + + static final Converter BEST_CONVERTER = getBestConverter(); + /** + * Returns the Unsafe-using Converter, or falls back to the pure-Java + * implementation if unable to do so. + */ + static Converter getBestConverter() { + try { + Class theClass = Class.forName(UNSAFE_CONVERTER_NAME); + + // yes, UnsafeComparer does implement Comparer + @SuppressWarnings("unchecked") + Converter converter = (Converter) theClass.getConstructor().newInstance(); + return converter; + } catch (Throwable t) { // ensure we really catch *everything* + return PureJavaConverter.INSTANCE; + } + } + + protected static final class PureJavaConverter extends Converter { + static final PureJavaConverter INSTANCE = new PureJavaConverter(); + + private PureJavaConverter() {} + + @Override + long toLong(byte[] bytes, int offset, int length) { + long l = 0; + for(int i = offset; i < offset + length; i++) { + l <<= 8; + l ^= bytes[i] & 0xFF; + } + return l; + } + + @Override + int putLong(byte[] bytes, int offset, long val) { + for(int i = offset + 7; i > offset; i--) { + bytes[i] = (byte) val; + val >>>= 8; + } + bytes[offset] = (byte) val; + return offset + SIZEOF_LONG; + } + + @Override + int toInt(byte[] bytes, int offset, int length) { + int n = 0; + for(int i = offset; i < (offset + length); i++) { + n <<= 8; + n ^= bytes[i] & 0xFF; + } + return n; + } + + @Override + int putInt(byte[] bytes, int offset, int val) { + for(int i= offset + 3; i > offset; i--) { + bytes[i] = (byte) val; + val >>>= 8; + } + bytes[offset] = (byte) val; + return offset + SIZEOF_INT; + } + + @Override + short toShort(byte[] bytes, int offset, int length) { + short n = 0; + n = (short) ((n ^ bytes[offset]) & 0xFF); + n = (short) (n << 8); + n ^= (short) (bytes[offset+1] & 0xFF); + return n; + } + + @Override + int putShort(byte[] bytes, int offset, short val) { + bytes[offset+1] = (byte) val; + val >>= 8; + bytes[offset] = (byte) val; + return offset + SIZEOF_SHORT; + } + } + + protected static final class UnsafeConverter extends Converter { + + static final Unsafe theUnsafe; + + public UnsafeConverter() {} + + static { + if (UNSAFE_UNALIGNED) { + theUnsafe = UnsafeAccess.theUnsafe; + } else { + // It doesn't matter what we throw; + // it's swallowed in getBestComparer(). + throw new Error(); + } + + // sanity check - this should never fail + if (theUnsafe.arrayIndexScale(byte[].class) != 1) { + throw new AssertionError(); + } + } + + @Override + long toLong(byte[] bytes, int offset, int length) { + return UnsafeAccess.toLong(bytes, offset); + } + + @Override + int putLong(byte[] bytes, int offset, long val) { + return UnsafeAccess.putLong(bytes, offset, val); + } + + @Override + int toInt(byte[] bytes, int offset, int length) { + return UnsafeAccess.toInt(bytes, offset); + } + + @Override + int putInt(byte[] bytes, int offset, int val) { + return UnsafeAccess.putInt(bytes, offset, val); + } + + @Override + short toShort(byte[] bytes, int offset, int length) { + return UnsafeAccess.toShort(bytes, offset); + } + + @Override + int putShort(byte[] bytes, int offset, short val) { + return UnsafeAccess.putShort(bytes, offset, val); + } + } + } + /** * Provides a lexicographical comparer implementation; either a Java * implementation or a faster implementation based on {@link Unsafe}. diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/CommonFSUtils.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/CommonFSUtils.java index 8924098bec24..899c63328381 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/CommonFSUtils.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/CommonFSUtils.java @@ -420,6 +420,34 @@ private static boolean isValidWALRootDir(Path walDir, final Configuration c) thr return true; } + /** + * Returns the WAL region directory based on the given table name and region name + * @param conf configuration to determine WALRootDir + * @param tableName Table that the region is under + * @param encodedRegionName Region name used for creating the final region directory + * @return the region directory used to store WALs under the WALRootDir + * @throws IOException if there is an exception determining the WALRootDir + */ + public static Path getWALRegionDir(final Configuration conf, + final TableName tableName, final String encodedRegionName) + throws IOException { + return new Path(getWALTableDir(conf, tableName), + encodedRegionName); + } + + /** + * Returns the Table directory under the WALRootDir for the specified table name + * @param conf configuration used to get the WALRootDir + * @param tableName Table to get the directory for + * @return a path to the WAL table directory for the specified table + * @throws IOException if there is an exception determining the WALRootDir + */ + public static Path getWALTableDir(final Configuration conf, final TableName tableName) + throws IOException { + return new Path(new Path(getWALRootDir(conf), tableName.getNamespaceAsString()), + tableName.getQualifierAsString()); + } + /** * Returns the {@link org.apache.hadoop.fs.Path} object representing the table directory under * path rootdir diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/IdLock.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/IdLock.java index 414cc66fd8fc..c4adfbff154c 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/IdLock.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/IdLock.java @@ -81,6 +81,17 @@ public Entry getLockEntry(long id) throws IOException { existing.wait(); } catch (InterruptedException e) { --existing.numWaiters; // Remove ourselves from waiters. + // HBASE-21292 + // There is a rare case that interrupting and the lock owner thread call + // releaseLockEntry at the same time. Since the owner thread found there + // still one waiting, it won't remove the entry from the map. If the interrupted + // thread is the last one waiting on the lock, and since an exception is thrown, + // the 'existing' entry will stay in the map forever. Later threads which try to + // get this lock will stuck in a infinite loop because + // existing = map.putIfAbsent(entry.id, entry)) != null and existing.locked=false. + if (!existing.locked && existing.numWaiters == 0) { + map.remove(existing.id); + } throw new InterruptedIOException( "Interrupted waiting to acquire sparse lock"); } @@ -135,6 +146,12 @@ public Entry tryLockEntry(long id, long time) throws IOException { } } catch (InterruptedException e) { + // HBASE-21292 + // Please refer to the comments in getLockEntry() + // the difference here is that we decrease numWaiters in finally block + if (!existing.locked && existing.numWaiters == 1) { + map.remove(existing.id); + } throw new InterruptedIOException( "Interrupted waiting to acquire sparse lock"); } finally { diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/Random64.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/Random64.java new file mode 100644 index 000000000000..f337b5f745dc --- /dev/null +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/Random64.java @@ -0,0 +1,149 @@ +/** + * + * 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.hadoop.hbase.util; + +import java.util.HashSet; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.yetus.audience.InterfaceAudience; +import org.apache.hbase.thirdparty.com.google.common.base.Preconditions; + +/** + * + * An instance of this class is used to generate a stream of + * pseudorandom numbers. The class uses a 64-bit seed, which is + * modified using a linear congruential formula. + * + * see https://en.wikipedia.org/wiki/Linear_congruential_generator + */ +@InterfaceAudience.Private +public class Random64 { + + private static final long multiplier = 6364136223846793005L; + private static final long addend = 1442695040888963407L; + + private static final AtomicLong seedUniquifier + = new AtomicLong(8682522807148012L); + + private long seed; + + /** + * Copy from {@link Random#seedUniquifier()} + */ + private static long seedUniquifier() { + for (; ; ) { + long current = seedUniquifier.get(); + long next = current * 181783497276652981L; + if (seedUniquifier.compareAndSet(current, next)) { + return next; + } + } + } + + public Random64() { + this(seedUniquifier() ^ System.nanoTime()); + } + + public Random64(long seed) { + this.seed = seed; + } + + public long nextLong() { + return next64(64); + } + + public void nextBytes(byte[] bytes) { + for (int i = 0, len = bytes.length; i < len;) { + // We regard seed as unsigned long, therefore used '>>>' instead of '>>'. + for (long rnd = nextLong(), n = Math.min(len - i, Long.SIZE / Byte.SIZE); + n-- > 0; rnd >>>= Byte.SIZE) { + bytes[i++] = (byte) rnd; + } + } + } + + private long next64(int bits) { + seed = seed * multiplier + addend; + return seed >>> (64 - bits); + } + + + /** + * Random64 is a pseudorandom algorithm(LCG). Therefore, we will get same sequence + * if seeds are the same. This main will test how many calls nextLong() it will + * get the same seed. + * + * We do not need to save all numbers (that is too large). We could save + * once every 100000 calls nextLong(). If it get a same seed, we can + * detect this by calling nextLong() 100000 times continuously. + * + */ + public static void main(String[] args) { + long defaultTotalTestCnt = 1000000000000L; // 1 trillion + + if (args.length == 1) { + defaultTotalTestCnt = Long.parseLong(args[0]); + } + + Preconditions.checkArgument(defaultTotalTestCnt > 0, "totalTestCnt <= 0"); + + final int precision = 100000; + final long totalTestCnt = defaultTotalTestCnt + precision; + final int reportPeriod = 100 * precision; + final long startTime = System.currentTimeMillis(); + + System.out.println("Do collision test, totalTestCnt=" + totalTestCnt); + + Random64 rand = new Random64(); + Set longSet = new HashSet<>(); + + for (long cnt = 1; cnt <= totalTestCnt; cnt++) { + final long randLong = rand.nextLong(); + + if (longSet.contains(randLong)) { + System.err.println("Conflict! count=" + cnt); + System.exit(1); + } + + if (cnt % precision == 0) { + if (!longSet.add(randLong)) { + System.err.println("Conflict! count=" + cnt); + System.exit(1); + } + + if (cnt % reportPeriod == 0) { + long cost = System.currentTimeMillis() - startTime; + long remainingMs = (long) (1.0 * (totalTestCnt - cnt) * cost / cnt); + System.out.println( + String.format( + "Progress: %.3f%%, remaining %d minutes", + 100.0 * cnt / totalTestCnt, remainingMs / 60000 + ) + ); + } + } + + } + + System.out.println("No collision!"); + } + +} diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/RetryCounter.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/RetryCounter.java index 4c163fd1c308..a2f09d08170d 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/RetryCounter.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/RetryCounter.java @@ -18,12 +18,15 @@ */ package org.apache.hadoop.hbase.util; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.TimeUnit; import org.apache.yetus.audience.InterfaceAudience; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.hbase.thirdparty.com.google.common.base.Preconditions; + /** * Operation retry accounting. * Use to calculate wait period, {@link #getBackoffTimeAndIncrementAttempts()}}, or for performing @@ -44,6 +47,7 @@ public static class RetryConfig { private long maxSleepTime; private TimeUnit timeUnit; private BackoffPolicy backoffPolicy; + private float jitter; private static final BackoffPolicy DEFAULT_BACKOFF_POLICY = new ExponentialBackoffPolicy(); @@ -53,6 +57,7 @@ public RetryConfig() { maxSleepTime = -1; timeUnit = TimeUnit.MILLISECONDS; backoffPolicy = DEFAULT_BACKOFF_POLICY; + jitter = 0.0f; } public RetryConfig(int maxAttempts, long sleepInterval, long maxSleepTime, @@ -89,6 +94,13 @@ public RetryConfig setTimeUnit(TimeUnit timeUnit) { return this; } + public RetryConfig setJitter(float jitter) { + Preconditions.checkArgument(jitter >= 0.0f && jitter < 1.0f, + "Invalid jitter: %s, should be in range [0.0, 1.0)", jitter); + this.jitter = jitter; + return this; + } + public int getMaxAttempts() { return maxAttempts; } @@ -105,17 +117,26 @@ public TimeUnit getTimeUnit() { return timeUnit; } + public float getJitter() { + return jitter; + } + public BackoffPolicy getBackoffPolicy() { return backoffPolicy; } } + private static long addJitter(long interval, float jitter) { + long jitterInterval = (long) (interval * ThreadLocalRandom.current().nextFloat() * jitter); + return interval + jitterInterval; + } + /** * Policy for calculating sleeping intervals between retry attempts */ public static class BackoffPolicy { public long getBackoffTime(RetryConfig config, int attempts) { - return config.getSleepInterval(); + return addJitter(config.getSleepInterval(), config.getJitter()); } } @@ -123,7 +144,7 @@ public static class ExponentialBackoffPolicy extends BackoffPolicy { @Override public long getBackoffTime(RetryConfig config, int attempts) { long backoffTime = (long) (config.getSleepInterval() * Math.pow(2, attempts)); - return backoffTime; + return addJitter(backoffTime, config.getJitter()); } } @@ -155,7 +176,6 @@ public int getMaxAttempts() { /** * Sleep for a back off time as supplied by the backoff policy, and increases the attempts - * @throws InterruptedException */ public void sleepUntilNextRetry() throws InterruptedException { int attempts = getAttemptTimes(); diff --git a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/Sleeper.java b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/Sleeper.java index 7d4d692e1a11..93ef08cc605e 100644 --- a/hbase-common/src/main/java/org/apache/hadoop/hbase/util/Sleeper.java +++ b/hbase-common/src/main/java/org/apache/hadoop/hbase/util/Sleeper.java @@ -49,13 +49,6 @@ public Sleeper(final int sleep, final Stoppable stopper) { this.stopper = stopper; } - /** - * Sleep for period. - */ - public void sleep() { - sleep(System.currentTimeMillis()); - } - /** * If currently asleep, stops sleeping; if not asleep, will skip the next * sleep cycle. @@ -68,28 +61,24 @@ public void skipSleepCycle() { } /** - * Sleep for period adjusted by passed startTime - * @param startTime Time some task started previous to now. Time to sleep - * will be docked current time minus passed startTime. + * Sleep for period. */ - public void sleep(final long startTime) { + public void sleep() { + sleep(this.period); + } + + public void sleep(long sleepTime) { if (this.stopper.isStopped()) { return; } long now = System.currentTimeMillis(); - long waitTime = this.period - (now - startTime); - if (waitTime > this.period) { - LOG.warn("Calculated wait time > " + this.period + - "; setting to this.period: " + System.currentTimeMillis() + ", " + - startTime); - waitTime = this.period; - } - while (waitTime > 0) { + long currentSleepTime = sleepTime; + while (currentSleepTime > 0) { long woke = -1; try { synchronized (sleepLock) { if (triggerWake) break; - sleepLock.wait(waitTime); + sleepLock.wait(currentSleepTime); } woke = System.currentTimeMillis(); long slept = woke - now; @@ -108,7 +97,7 @@ public void sleep(final long startTime) { } // Recalculate waitTime. woke = (woke == -1)? System.currentTimeMillis(): woke; - waitTime = this.period - (woke - startTime); + currentSleepTime = this.period - (woke - now); } synchronized(sleepLock) { triggerWake = false; diff --git a/hbase-common/src/saveVersion.sh b/hbase-common/src/saveVersion.sh index abb8c5bcff8b..730224f0d653 100644 --- a/hbase-common/src/saveVersion.sh +++ b/hbase-common/src/saveVersion.sh @@ -38,7 +38,7 @@ if [ -d .svn ]; then revision=`svn info | sed -n -e 's/Last Changed Rev: \(.*\)/\1/p'` url=`svn info | sed -n -e 's/^URL: \(.*\)/\1/p'` elif [ -d .git ]; then - revision=`git log -1 --pretty=format:"%H"` + revision=`git log -1 --no-show-signature --pretty=format:"%H"` hostname=`hostname` url="git://${hostname}${cwd}" else diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/ResourceCheckerJUnitListener.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/ResourceCheckerJUnitListener.java index d8df1379252e..225d94f42b45 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/ResourceCheckerJUnitListener.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/ResourceCheckerJUnitListener.java @@ -18,15 +18,12 @@ package org.apache.hadoop.hbase; -import java.lang.management.ManagementFactory; -import java.lang.management.MemoryUsage; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; import org.apache.hadoop.hbase.ResourceChecker.Phase; import org.apache.hadoop.hbase.util.JVM; import org.junit.runner.notification.RunListener; @@ -142,41 +139,6 @@ public int getVal(Phase phase) { } } - static class MaxHeapMemoryMBResourceAnalyzer extends ResourceChecker.ResourceAnalyzer { - - @Override - public int getVal(Phase phase) { - MemoryUsage usage = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage(); - return (int) (usage.getMax() / (1024 * 1024)); - } - } - - static class UsedHeapMemoryMBResourceAnalyzer extends ResourceChecker.ResourceAnalyzer { - - @Override - public int getVal(Phase phase) { - MemoryUsage usage = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage(); - return (int) (usage.getUsed() / (1024 * 1024)); - } - } - - static class GCCountResourceAnalyzer extends ResourceChecker.ResourceAnalyzer { - - @Override - public int getVal(Phase phase) { - return Math.toIntExact(ManagementFactory.getGarbageCollectorMXBeans().stream() - .mapToLong(b -> b.getCollectionCount()).sum()); - } - } - - static class GCTimeSecondResourceAnalyzer extends ResourceChecker.ResourceAnalyzer { - - @Override - public int getVal(Phase phase) { - return Math.toIntExact(TimeUnit.MILLISECONDS.toSeconds(ManagementFactory - .getGarbageCollectorMXBeans().stream().mapToLong(b -> b.getCollectionTime()).sum())); - } - } /** * To be implemented by sub classes if they want to add specific ResourceAnalyzer. @@ -193,10 +155,6 @@ private void start(String testName) { rc.addResourceAnalyzer(new SystemLoadAverageResourceAnalyzer()); rc.addResourceAnalyzer(new ProcessCountResourceAnalyzer()); rc.addResourceAnalyzer(new AvailableMemoryMBResourceAnalyzer()); - rc.addResourceAnalyzer(new MaxHeapMemoryMBResourceAnalyzer()); - rc.addResourceAnalyzer(new UsedHeapMemoryMBResourceAnalyzer()); - rc.addResourceAnalyzer(new GCCountResourceAnalyzer()); - rc.addResourceAnalyzer(new GCTimeSecondResourceAnalyzer()); addResourceAnalyzer(rc); diff --git a/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestBytes.java b/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestBytes.java index cea615e081a8..8b206e2b7418 100644 --- a/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestBytes.java +++ b/hbase-common/src/test/java/org/apache/hadoop/hbase/util/TestBytes.java @@ -22,6 +22,8 @@ import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.math.BigDecimal; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -44,6 +46,48 @@ public class TestBytes extends TestCase { public static final HBaseClassTestRule CLASS_RULE = HBaseClassTestRule.forClass(TestBytes.class); + private static void setUnsafe(boolean value) throws Exception { + Field field = Bytes.class.getDeclaredField("UNSAFE_UNALIGNED"); + field.setAccessible(true); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + int oldModifiers = field.getModifiers(); + modifiersField.setInt(field, oldModifiers & ~Modifier.FINAL); + try { + field.set(null, value); + } finally { + modifiersField.setInt(field, oldModifiers); + } + assertEquals(Bytes.UNSAFE_UNALIGNED, value); + } + + public void testShort() throws Exception { + testShort(false); + } + + public void testShortUnsafe() throws Exception { + testShort(true); + } + + private static void testShort(boolean unsafe) throws Exception { + setUnsafe(unsafe); + try { + for (short n : Arrays.asList( + Short.MIN_VALUE, + (short) -100, + (short) -1, + (short) 0, + (short) 1, + (short) 300, + Short.MAX_VALUE)) { + byte[] bytes = Bytes.toBytes(n); + assertEquals(Bytes.toShort(bytes, 0, bytes.length), n); + } + } finally { + setUnsafe(UnsafeAvailChecker.unaligned()); + } + } + public void testNullHashCode() { byte [] b = null; Exception ee = null; diff --git a/hbase-endpoint/pom.xml b/hbase-endpoint/pom.xml index 8950de17980e..d0b542554e04 100644 --- a/hbase-endpoint/pom.xml +++ b/hbase-endpoint/pom.xml @@ -177,6 +177,11 @@ test-jar test + + org.bouncycastle + bcprov-jdk15on + test + org.apache.hbase hbase-server @@ -229,12 +234,6 @@ mockito-core test - - - org.bouncycastle - bcprov-jdk16 - test - diff --git a/hbase-examples/src/main/java/org/apache/hadoop/hbase/client/example/RefreshHFilesClient.java b/hbase-examples/src/main/java/org/apache/hadoop/hbase/client/example/RefreshHFilesClient.java index ead0af0e74f0..06ad195c62ea 100644 --- a/hbase-examples/src/main/java/org/apache/hadoop/hbase/client/example/RefreshHFilesClient.java +++ b/hbase-examples/src/main/java/org/apache/hadoop/hbase/client/example/RefreshHFilesClient.java @@ -22,6 +22,8 @@ import java.io.Closeable; import java.io.IOException; import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.conf.Configured; +import org.apache.hadoop.hbase.HBaseConfiguration; import org.apache.hadoop.hbase.HConstants; import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.client.Connection; @@ -31,6 +33,8 @@ import org.apache.hadoop.hbase.ipc.CoprocessorRpcUtils.BlockingRpcCallback; import org.apache.hadoop.hbase.ipc.ServerRpcController; import org.apache.hadoop.hbase.protobuf.generated.RefreshHFilesProtos; +import org.apache.hadoop.util.Tool; +import org.apache.hadoop.util.ToolRunner; import org.apache.yetus.audience.InterfaceAudience; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,7 +44,7 @@ * Region Server side via the RefreshHFilesService. */ @InterfaceAudience.Private -public class RefreshHFilesClient implements Closeable { +public class RefreshHFilesClient extends Configured implements Tool, Closeable { private static final Logger LOG = LoggerFactory.getLogger(RefreshHFilesClient.class); private final Connection connection; @@ -93,4 +97,28 @@ public RefreshHFilesProtos.RefreshHFilesResponse call( }); LOG.debug("Done refreshing HFiles"); } + + @Override + public int run(String[] args) throws Exception { + if (args.length != 1) { + String message = "When there are multiple HBase clusters are sharing a common root dir, " + + "especially for read replica cluster (see detail in HBASE-18477), please consider to " + + "use this tool manually sync the flushed HFiles from the source cluster."; + message += "\nUsage: " + this.getClass().getName() + " tableName"; + System.out.println(message); + return -1; + } + final TableName tableName = TableName.valueOf(args[0]); + try { + refreshHFiles(tableName); + } catch (Throwable t) { + LOG.error("Refresh HFiles from table " + tableName.getNameAsString() + " failed: ", t); + return -1; + } + return 0; + } + + public static void main(String[] args) throws Exception { + ToolRunner.run(new RefreshHFilesClient(HBaseConfiguration.create()), args); + } } diff --git a/hbase-examples/src/test/java/org/apache/hadoop/hbase/client/example/TestRefreshHFilesClient.java b/hbase-examples/src/test/java/org/apache/hadoop/hbase/client/example/TestRefreshHFilesClient.java new file mode 100644 index 000000000000..b168db44b546 --- /dev/null +++ b/hbase-examples/src/test/java/org/apache/hadoop/hbase/client/example/TestRefreshHFilesClient.java @@ -0,0 +1,52 @@ +/** + * 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.hadoop.hbase.client.example; + +import static org.junit.Assert.assertEquals; + +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.coprocessor.example.TestRefreshHFilesBase; +import org.apache.hadoop.hbase.regionserver.HRegion; +import org.apache.hadoop.hbase.testclassification.ClientTests; +import org.apache.hadoop.hbase.testclassification.MediumTests; +import org.apache.hadoop.util.ToolRunner; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +@Category({ ClientTests.class, MediumTests.class }) +public class TestRefreshHFilesClient extends TestRefreshHFilesBase { + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestRefreshHFilesClient.class); + + @BeforeClass + public static void setUp() { + setUp(HRegion.class.getName()); + } + + @Test + public void testRefreshHFilesClient() throws Exception { + addHFilesToRegions(); + assertEquals(2, HTU.getNumHFiles(TABLE_NAME, FAMILY)); + RefreshHFilesClient tool = new RefreshHFilesClient(HTU.getConfiguration()); + assertEquals(0, ToolRunner.run(tool, new String[] { TABLE_NAME.getNameAsString() })); + assertEquals(4, HTU.getNumHFiles(TABLE_NAME, FAMILY)); + } +} diff --git a/hbase-examples/src/test/java/org/apache/hadoop/hbase/coprocessor/example/TestRefreshHFilesBase.java b/hbase-examples/src/test/java/org/apache/hadoop/hbase/coprocessor/example/TestRefreshHFilesBase.java new file mode 100644 index 000000000000..b948b62c600c --- /dev/null +++ b/hbase-examples/src/test/java/org/apache/hadoop/hbase/coprocessor/example/TestRefreshHFilesBase.java @@ -0,0 +1,88 @@ +/** + * 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.hadoop.hbase.coprocessor.example; + +import java.io.IOException; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hbase.HBaseTestingUtility; +import org.apache.hadoop.hbase.HConstants; +import org.apache.hadoop.hbase.MiniHBaseCluster; +import org.apache.hadoop.hbase.TableName; +import org.apache.hadoop.hbase.client.Table; +import org.apache.hadoop.hbase.coprocessor.CoprocessorHost; +import org.apache.hadoop.hbase.master.MasterFileSystem; +import org.apache.hadoop.hbase.regionserver.Region; +import org.apache.hadoop.hbase.util.Bytes; +import org.apache.hadoop.hbase.util.FSUtils; +import org.apache.hadoop.hbase.util.HFileTestUtil; +import org.junit.After; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TestRefreshHFilesBase { + protected static final Logger LOG = LoggerFactory.getLogger(TestRefreshHFilesBase.class); + protected static final HBaseTestingUtility HTU = new HBaseTestingUtility(); + protected static final int NUM_RS = 2; + protected static final TableName TABLE_NAME = TableName.valueOf("testRefreshRegionHFilesEP"); + protected static final byte[] FAMILY = Bytes.toBytes("family"); + protected static final byte[] QUALIFIER = Bytes.toBytes("qualifier"); + protected static final byte[][] SPLIT_KEY = new byte[][] { Bytes.toBytes("30") }; + protected static final int NUM_ROWS = 5; + protected static final String HFILE_NAME = "123abcdef"; + + protected static Configuration CONF = HTU.getConfiguration(); + protected static MiniHBaseCluster cluster; + protected static Table table; + + public static void setUp(String regionImpl) { + try { + CONF.set(HConstants.REGION_IMPL, regionImpl); + CONF.setInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER, 2); + + CONF.setStrings(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, RefreshHFilesEndpoint.class.getName()); + cluster = HTU.startMiniCluster(NUM_RS); + + // Create table + table = HTU.createTable(TABLE_NAME, FAMILY, SPLIT_KEY); + + // this will create 2 regions spread across slaves + HTU.loadNumericRows(table, FAMILY, 1, 20); + HTU.flush(TABLE_NAME); + } catch (Exception ex) { + LOG.error("Couldn't finish setup", ex); + } + } + + @After + public void tearDown() throws Exception { + HTU.shutdownMiniCluster(); + } + + protected void addHFilesToRegions() throws IOException { + MasterFileSystem mfs = HTU.getMiniHBaseCluster().getMaster().getMasterFileSystem(); + Path tableDir = FSUtils.getTableDir(mfs.getRootDir(), TABLE_NAME); + for (Region region : cluster.getRegions(TABLE_NAME)) { + Path regionDir = new Path(tableDir, region.getRegionInfo().getEncodedName()); + Path familyDir = new Path(regionDir, Bytes.toString(FAMILY)); + HFileTestUtil + .createHFile(HTU.getConfiguration(), HTU.getTestFileSystem(), new Path(familyDir, HFILE_NAME), FAMILY, + QUALIFIER, Bytes.toBytes("50"), Bytes.toBytes("60"), NUM_ROWS); + } + } +} diff --git a/hbase-examples/src/test/java/org/apache/hadoop/hbase/coprocessor/example/TestRefreshHFilesEndpoint.java b/hbase-examples/src/test/java/org/apache/hadoop/hbase/coprocessor/example/TestRefreshHFilesEndpoint.java index d94210496f35..1a7c990fdd98 100644 --- a/hbase-examples/src/test/java/org/apache/hadoop/hbase/coprocessor/example/TestRefreshHFilesEndpoint.java +++ b/hbase-examples/src/test/java/org/apache/hadoop/hbase/coprocessor/example/TestRefreshHFilesEndpoint.java @@ -27,92 +27,32 @@ import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.hbase.HBaseClassTestRule; -import org.apache.hadoop.hbase.HBaseTestingUtility; -import org.apache.hadoop.hbase.HConstants; -import org.apache.hadoop.hbase.MiniHBaseCluster; -import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.client.ColumnFamilyDescriptor; import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; import org.apache.hadoop.hbase.client.RegionInfo; import org.apache.hadoop.hbase.client.RetriesExhaustedException; -import org.apache.hadoop.hbase.client.Table; import org.apache.hadoop.hbase.client.TableDescriptor; import org.apache.hadoop.hbase.client.example.RefreshHFilesClient; -import org.apache.hadoop.hbase.coprocessor.CoprocessorHost; -import org.apache.hadoop.hbase.master.MasterFileSystem; import org.apache.hadoop.hbase.regionserver.HRegion; import org.apache.hadoop.hbase.regionserver.HStore; -import org.apache.hadoop.hbase.regionserver.Region; import org.apache.hadoop.hbase.regionserver.RegionServerServices; import org.apache.hadoop.hbase.testclassification.MediumTests; -import org.apache.hadoop.hbase.util.Bytes; -import org.apache.hadoop.hbase.util.FSUtils; -import org.apache.hadoop.hbase.util.HFileTestUtil; import org.apache.hadoop.hbase.wal.WAL; -import org.junit.After; import org.junit.ClassRule; import org.junit.Test; import org.junit.experimental.categories.Category; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; @Category(MediumTests.class) -public class TestRefreshHFilesEndpoint { +public class TestRefreshHFilesEndpoint extends TestRefreshHFilesBase { @ClassRule public static final HBaseClassTestRule CLASS_RULE = HBaseClassTestRule.forClass(TestRefreshHFilesEndpoint.class); - private static final Logger LOG = LoggerFactory.getLogger(TestRefreshHFilesEndpoint.class); - private static final HBaseTestingUtility HTU = new HBaseTestingUtility(); - private static final int NUM_RS = 2; - private static final TableName TABLE_NAME = TableName.valueOf("testRefreshRegionHFilesEP"); - private static final byte[] FAMILY = Bytes.toBytes("family"); - private static final byte[] QUALIFIER = Bytes.toBytes("qualifier"); - private static final byte[][] SPLIT_KEY = new byte[][] { Bytes.toBytes("30") }; - private static final int NUM_ROWS = 5; - private static final String HFILE_NAME = "123abcdef"; - - private static Configuration CONF = HTU.getConfiguration(); - private static MiniHBaseCluster cluster; - private static Table table; - - public static void setUp(String regionImpl) { - try { - CONF.set(HConstants.REGION_IMPL, regionImpl); - CONF.setInt(HConstants.HBASE_CLIENT_RETRIES_NUMBER, 2); - - CONF.setStrings(CoprocessorHost.REGION_COPROCESSOR_CONF_KEY, RefreshHFilesEndpoint.class.getName()); - cluster = HTU.startMiniCluster(NUM_RS); - - // Create table - table = HTU.createTable(TABLE_NAME, FAMILY, SPLIT_KEY); - - // this will create 2 regions spread across slaves - HTU.loadNumericRows(table, FAMILY, 1, 20); - HTU.flush(TABLE_NAME); - } catch (Exception ex) { - LOG.error("Couldn't finish setup", ex); - } - } - - @After - public void tearDown() throws Exception { - HTU.shutdownMiniCluster(); - } - @Test public void testRefreshRegionHFilesEndpoint() throws Exception { setUp(HRegion.class.getName()); - MasterFileSystem mfs = HTU.getMiniHBaseCluster().getMaster().getMasterFileSystem(); - Path tableDir = FSUtils.getTableDir(mfs.getRootDir(), TABLE_NAME); - for (Region region : cluster.getRegions(TABLE_NAME)) { - Path regionDir = new Path(tableDir, region.getRegionInfo().getEncodedName()); - Path familyDir = new Path(regionDir, Bytes.toString(FAMILY)); - HFileTestUtil - .createHFile(HTU.getConfiguration(), HTU.getTestFileSystem(), new Path(familyDir, HFILE_NAME), FAMILY, - QUALIFIER, Bytes.toBytes("50"), Bytes.toBytes("60"), NUM_ROWS); - } + addHFilesToRegions(); assertEquals(2, HTU.getNumHFiles(TABLE_NAME, FAMILY)); callRefreshRegionHFilesEndPoint(); assertEquals(4, HTU.getNumHFiles(TABLE_NAME, FAMILY)); diff --git a/hbase-hadoop-compat/src/main/java/org/apache/hadoop/hbase/master/balancer/MetricsBalancerSource.java b/hbase-hadoop-compat/src/main/java/org/apache/hadoop/hbase/master/balancer/MetricsBalancerSource.java index 5add25f4c048..6b8c40ba5127 100644 --- a/hbase-hadoop-compat/src/main/java/org/apache/hadoop/hbase/master/balancer/MetricsBalancerSource.java +++ b/hbase-hadoop-compat/src/main/java/org/apache/hadoop/hbase/master/balancer/MetricsBalancerSource.java @@ -41,6 +41,7 @@ public interface MetricsBalancerSource extends BaseSource { String BALANCE_CLUSTER = "balancerCluster"; String MISC_INVOATION_COUNT = "miscInvocationCount"; + String BALANCER_STATUS = "isBalancerActive"; /** * Description @@ -50,4 +51,6 @@ public interface MetricsBalancerSource extends BaseSource { void updateBalanceCluster(long time); void incrMiscInvocations(); + + void updateBalancerStatus(boolean status); } diff --git a/hbase-hadoop-compat/src/main/java/org/apache/hadoop/hbase/replication/regionserver/MetricsReplicationSourceSource.java b/hbase-hadoop-compat/src/main/java/org/apache/hadoop/hbase/replication/regionserver/MetricsReplicationSourceSource.java index 10451138b4c3..61e94311563f 100644 --- a/hbase-hadoop-compat/src/main/java/org/apache/hadoop/hbase/replication/regionserver/MetricsReplicationSourceSource.java +++ b/hbase-hadoop-compat/src/main/java/org/apache/hadoop/hbase/replication/regionserver/MetricsReplicationSourceSource.java @@ -53,6 +53,7 @@ public interface MetricsReplicationSourceSource extends BaseSource { public static final String SOURCE_REPEATED_LOG_FILE_BYTES = "source.repeatedLogFileBytes"; public static final String SOURCE_COMPLETED_LOGS = "source.completedLogs"; public static final String SOURCE_COMPLETED_RECOVERY_QUEUES = "source.completedRecoverQueues"; + public static final String SOURCE_FAILED_RECOVERY_QUEUES = "source.failedRecoverQueues"; void setLastShippedAge(long age); void incrSizeOfLogQueue(int size); @@ -76,4 +77,5 @@ public interface MetricsReplicationSourceSource extends BaseSource { void incrRepeatedFileBytes(final long bytes); void incrCompletedWAL(); void incrCompletedRecoveryQueue(); + void incrFailedRecoveryQueue(); } diff --git a/hbase-hadoop2-compat/src/main/java/org/apache/hadoop/hbase/master/balancer/MetricsBalancerSourceImpl.java b/hbase-hadoop2-compat/src/main/java/org/apache/hadoop/hbase/master/balancer/MetricsBalancerSourceImpl.java index a10b3d71ebdd..7bccbb70d584 100644 --- a/hbase-hadoop2-compat/src/main/java/org/apache/hadoop/hbase/master/balancer/MetricsBalancerSourceImpl.java +++ b/hbase-hadoop2-compat/src/main/java/org/apache/hadoop/hbase/master/balancer/MetricsBalancerSourceImpl.java @@ -36,6 +36,7 @@ public MetricsBalancerSourceImpl(String metricsName, String metricsDescription, String metricsContext, String metricsJmxContext) { super(metricsName, metricsDescription, metricsContext, metricsJmxContext); + updateBalancerStatus(true); } @Override @@ -53,4 +54,9 @@ public void updateBalanceCluster(long time) { public void incrMiscInvocations() { miscCount.incr(); } + + @Override + public void updateBalancerStatus(boolean status) { + metricsRegistry.tag(BALANCER_STATUS,"", String.valueOf(status), true); + } } diff --git a/hbase-hadoop2-compat/src/main/java/org/apache/hadoop/hbase/replication/regionserver/MetricsReplicationGlobalSourceSource.java b/hbase-hadoop2-compat/src/main/java/org/apache/hadoop/hbase/replication/regionserver/MetricsReplicationGlobalSourceSource.java index 9a86cf2f9e45..4e8c810138be 100644 --- a/hbase-hadoop2-compat/src/main/java/org/apache/hadoop/hbase/replication/regionserver/MetricsReplicationGlobalSourceSource.java +++ b/hbase-hadoop2-compat/src/main/java/org/apache/hadoop/hbase/replication/regionserver/MetricsReplicationGlobalSourceSource.java @@ -52,6 +52,7 @@ public class MetricsReplicationGlobalSourceSource implements MetricsReplicationS private final MutableFastCounter repeatedFileBytes; private final MutableFastCounter completedWAL; private final MutableFastCounter completedRecoveryQueue; + private final MutableFastCounter failedRecoveryQueue; public MetricsReplicationGlobalSourceSource(MetricsReplicationSourceImpl rms) { this.rms = rms; @@ -89,6 +90,8 @@ public MetricsReplicationGlobalSourceSource(MetricsReplicationSourceImpl rms) { completedWAL = rms.getMetricsRegistry().getCounter(SOURCE_COMPLETED_LOGS, 0L); completedRecoveryQueue = rms.getMetricsRegistry() .getCounter(SOURCE_COMPLETED_RECOVERY_QUEUES, 0L); + failedRecoveryQueue = rms.getMetricsRegistry() + .getCounter(SOURCE_FAILED_RECOVERY_QUEUES, 0L); } @Override public void setLastShippedAge(long age) { @@ -199,7 +202,10 @@ public void incrCompletedWAL() { public void incrCompletedRecoveryQueue() { completedRecoveryQueue.incr(1L); } - + @Override + public void incrFailedRecoveryQueue() { + failedRecoveryQueue.incr(1L); + } @Override public void init() { rms.init(); diff --git a/hbase-hadoop2-compat/src/main/java/org/apache/hadoop/hbase/replication/regionserver/MetricsReplicationSourceSourceImpl.java b/hbase-hadoop2-compat/src/main/java/org/apache/hadoop/hbase/replication/regionserver/MetricsReplicationSourceSourceImpl.java index 719c91631d26..0ad50524f152 100644 --- a/hbase-hadoop2-compat/src/main/java/org/apache/hadoop/hbase/replication/regionserver/MetricsReplicationSourceSourceImpl.java +++ b/hbase-hadoop2-compat/src/main/java/org/apache/hadoop/hbase/replication/regionserver/MetricsReplicationSourceSourceImpl.java @@ -257,6 +257,9 @@ public void incrCompletedRecoveryQueue() { completedRecoveryQueue.incr(1L); } + @Override + public void incrFailedRecoveryQueue() {/*no op*/} + @Override public void init() { rms.init(); diff --git a/hbase-http/pom.xml b/hbase-http/pom.xml index 667b3f58872e..fb704511cc9c 100644 --- a/hbase-http/pom.xml +++ b/hbase-http/pom.xml @@ -263,7 +263,7 @@ org.bouncycastle - bcprov-jdk16 + bcprov-jdk15on test diff --git a/hbase-it/src/test/java/org/apache/hadoop/hbase/chaos/actions/Action.java b/hbase-it/src/test/java/org/apache/hadoop/hbase/chaos/actions/Action.java index 350e18a4544a..6db6da0a3dbf 100644 --- a/hbase-it/src/test/java/org/apache/hadoop/hbase/chaos/actions/Action.java +++ b/hbase-it/src/test/java/org/apache/hadoop/hbase/chaos/actions/Action.java @@ -21,9 +21,11 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collection; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -139,16 +141,13 @@ protected ServerName[] getCurrentServers() throws IOException { return new ServerName [] {}; } ServerName master = clusterStatus.getMasterName(); - if (master == null || !regionServers.contains(master)) { - return regionServers.toArray(new ServerName[count]); - } - if (count == 1) { - return new ServerName [] {}; - } + Set masters = new HashSet(); + masters.add(master); + masters.addAll(clusterStatus.getBackupMasterNames()); ArrayList tmp = new ArrayList<>(count); tmp.addAll(regionServers); - tmp.remove(master); - return tmp.toArray(new ServerName[count-1]); + tmp.removeAll(masters); + return tmp.toArray(new ServerName[tmp.size()]); } protected void killMaster(ServerName server) throws IOException { diff --git a/hbase-it/src/test/java/org/apache/hadoop/hbase/chaos/actions/ChangeBloomFilterAction.java b/hbase-it/src/test/java/org/apache/hadoop/hbase/chaos/actions/ChangeBloomFilterAction.java index 47ef6489df1c..4faf05df98c1 100644 --- a/hbase-it/src/test/java/org/apache/hadoop/hbase/chaos/actions/ChangeBloomFilterAction.java +++ b/hbase-it/src/test/java/org/apache/hadoop/hbase/chaos/actions/ChangeBloomFilterAction.java @@ -22,6 +22,7 @@ import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.regionserver.BloomType; +import org.apache.hadoop.hbase.util.BloomFilterUtil; /** * Action that tries to adjust the bloom filter setting on all the columns of a @@ -53,6 +54,11 @@ public void perform() throws Exception { LOG.debug("Performing action: About to set bloom filter type to " + bloomType + " on column " + columnName + " of table " + tableName); columnBuilder.setBloomFilterType(bloomType); + if (bloomType == BloomType.ROWPREFIX_FIXED_LENGTH) { + columnBuilder.setConfiguration(BloomFilterUtil.PREFIX_LENGTH_KEY, "10"); + } else if (bloomType == BloomType.ROWPREFIX_DELIMITED) { + columnBuilder.setConfiguration(BloomFilterUtil.DELIMITER_KEY, "#"); + } }); LOG.debug("Performing action: Just set bloom filter types on table " + tableName); diff --git a/hbase-it/src/test/java/org/apache/hadoop/hbase/test/IntegrationTestBigLinkedList.java b/hbase-it/src/test/java/org/apache/hadoop/hbase/test/IntegrationTestBigLinkedList.java index 35bc7a1a42a6..b965bcab68c6 100644 --- a/hbase-it/src/test/java/org/apache/hadoop/hbase/test/IntegrationTestBigLinkedList.java +++ b/hbase-it/src/test/java/org/apache/hadoop/hbase/test/IntegrationTestBigLinkedList.java @@ -15,7 +15,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.apache.hadoop.hbase.test; import java.io.DataInput; @@ -58,6 +57,7 @@ import org.apache.hadoop.hbase.client.BufferedMutator; import org.apache.hadoop.hbase.client.BufferedMutatorParams; import org.apache.hadoop.hbase.client.Connection; +import org.apache.hadoop.hbase.client.ConnectionConfiguration; import org.apache.hadoop.hbase.client.ConnectionFactory; import org.apache.hadoop.hbase.client.Get; import org.apache.hadoop.hbase.client.Mutation; @@ -79,6 +79,7 @@ import org.apache.hadoop.hbase.testclassification.IntegrationTests; import org.apache.hadoop.hbase.util.AbstractHBaseTool; import org.apache.hadoop.hbase.util.Bytes; +import org.apache.hadoop.hbase.util.Random64; import org.apache.hadoop.hbase.util.RegionSplitter; import org.apache.hadoop.hbase.wal.WALEdit; import org.apache.hadoop.hbase.wal.WALKey; @@ -113,6 +114,8 @@ import org.junit.experimental.categories.Category; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + +import org.apache.hbase.thirdparty.com.google.common.base.Preconditions; import org.apache.hbase.thirdparty.com.google.common.collect.Sets; import org.apache.hbase.thirdparty.org.apache.commons.cli.CommandLine; import org.apache.hbase.thirdparty.org.apache.commons.cli.GnuParser; @@ -267,6 +270,15 @@ static class Generator extends Configured implements Tool { public static final String MULTIPLE_UNEVEN_COLUMNFAMILIES_KEY = "generator.multiple.columnfamilies"; + /** + * Set this configuration if you want to scale up the size of test data quickly. + *

+ * $ ./bin/hbase org.apache.hadoop.hbase.test.IntegrationTestBigLinkedList + * -Dgenerator.big.family.value.size=1024 generator 1 10 output + */ + public static final String BIG_FAMILY_VALUE_SIZE_KEY = "generator.big.family.value.size"; + + public static enum Counts { SUCCESS, TERMINATING, UNDEFINED, IOEXCEPTION } @@ -300,7 +312,7 @@ public void write(DataOutput arg0) throws IOException { static class GeneratorRecordReader extends RecordReader { private long count; private long numNodes; - private Random rand; + private Random64 rand; @Override public void close() throws IOException { @@ -327,8 +339,8 @@ public float getProgress() throws IOException, InterruptedException { public void initialize(InputSplit arg0, TaskAttemptContext context) throws IOException, InterruptedException { numNodes = context.getConfiguration().getLong(GENERATOR_NUM_ROWS_PER_MAP_KEY, 25000000); - // Use SecureRandom to avoid issue described in HBASE-13382. - rand = new SecureRandom(); + // Use Random64 to avoid issue described in HBASE-21256. + rand = new Random64(); } @Override @@ -437,6 +449,36 @@ protected void setup(Context context) throws IOException, InterruptedException { this.numWalkers = context.getConfiguration().getInt(CONCURRENT_WALKER_KEY, CONCURRENT_WALKER_DEFAULT); this.walkersStop = false; this.conf = context.getConfiguration(); + + if (multipleUnevenColumnFamilies) { + int n = context.getConfiguration().getInt(BIG_FAMILY_VALUE_SIZE_KEY, 256); + int limit = context.getConfiguration().getInt( + ConnectionConfiguration.MAX_KEYVALUE_SIZE_KEY, + ConnectionConfiguration.MAX_KEYVALUE_SIZE_DEFAULT); + + Preconditions.checkArgument( + n <= limit, + "%s(%s) > %s(%s)", + BIG_FAMILY_VALUE_SIZE_KEY, n, ConnectionConfiguration.MAX_KEYVALUE_SIZE_KEY, limit); + + bigValue = new byte[n]; + ThreadLocalRandom.current().nextBytes(bigValue); + LOG.info("Create a bigValue with " + n + " bytes."); + } + + Preconditions.checkArgument( + numNodes > 0, + "numNodes(%s) <= 0", + numNodes); + Preconditions.checkArgument( + numNodes % width == 0, + "numNodes(%s) mod width(%s) != 0", + numNodes, width); + Preconditions.checkArgument( + numNodes % wrap == 0, + "numNodes(%s) mod wrap(%s) != 0", + numNodes, wrap + ); } protected void instantiateHTable() throws IOException { @@ -457,9 +499,8 @@ protected void map(BytesWritable key, NullWritable value, Context output) throws current[i] = new byte[key.getLength()]; System.arraycopy(key.getBytes(), 0, current[i], 0, key.getLength()); if (++i == current.length) { - LOG.info("Persisting current.length=" + current.length + ", count=" + count + ", id=" + - Bytes.toStringBinary(id) + ", current=" + Bytes.toStringBinary(current[0]) + - ", i=" + i); + LOG.debug("Persisting current.length={}, count={}, id={}, current={}, i=", + current.length, count, Bytes.toStringBinary(id), Bytes.toStringBinary(current[0]), i); persist(output, count, prev, current, id); i = 0; @@ -526,11 +567,6 @@ protected void persist(Context output, long count, byte[][] prev, byte[][] curre if (this.multipleUnevenColumnFamilies) { // Use any column name. put.addColumn(TINY_FAMILY_NAME, TINY_FAMILY_NAME, this.tinyValue); - // If we've not allocated bigValue, do it now. Reuse same value each time. - if (this.bigValue == null) { - this.bigValue = new byte[current[i].length * 10]; - ThreadLocalRandom.current().nextBytes(this.bigValue); - } // Use any column name. put.addColumn(BIG_FAMILY_NAME, BIG_FAMILY_NAME, this.bigValue); } @@ -759,6 +795,7 @@ public int runRandomInputGenerator(int numMappers, long numNodes, Path tmpOutput FileOutputFormat.setOutputPath(job, tmpOutput); job.setOutputFormatClass(SequenceFileOutputFormat.class); + TableMapReduceUtil.addDependencyJarsForClasses(job.getConfiguration(), Random64.class); boolean success = jobCompletion(job); @@ -1155,13 +1192,14 @@ public void reduce(BytesWritable key, Iterable values, Context co // TODO check for more than one def, should not happen StringBuilder refsSb = null; - String keyString = Bytes.toStringBinary(key.getBytes(), 0, key.getLength()); if (defCount == 0 || refs.size() != 1) { + String keyString = Bytes.toStringBinary(key.getBytes(), 0, key.getLength()); refsSb = dumpExtraInfoOnRefs(key, context, refs); LOG.error("LinkedListError: key=" + keyString + ", reference(s)=" + (refsSb != null? refsSb.toString(): "")); } if (lostFamilies) { + String keyString = Bytes.toStringBinary(key.getBytes(), 0, key.getLength()); LOG.error("LinkedListError: key=" + keyString + ", lost big or tiny families"); context.getCounter(Counts.LOST_FAMILIES).increment(1); context.write(key, LOSTFAM); @@ -1188,6 +1226,7 @@ public void reduce(BytesWritable key, Iterable values, Context co // was added which can help a little debugging. This info is only available in mapper // output -- the 'Linked List error Key...' log message above. What we emit here is // useless for debugging. + String keyString = Bytes.toStringBinary(key.getBytes(), 0, key.getLength()); context.getCounter("undef", keyString).increment(1); } } else if (defCount > 0 && refs.isEmpty()) { @@ -1195,6 +1234,7 @@ public void reduce(BytesWritable key, Iterable values, Context co context.write(key, UNREF); context.getCounter(Counts.UNREFERENCED).increment(1); if (rows.addAndGet(1) < MISSING_ROWS_TO_LOG) { + String keyString = Bytes.toStringBinary(key.getBytes(), 0, key.getLength()); context.getCounter("unref", keyString).increment(1); } } else { diff --git a/hbase-mapreduce/src/main/java/org/apache/hadoop/hbase/mapreduce/HFileOutputFormat2.java b/hbase-mapreduce/src/main/java/org/apache/hadoop/hbase/mapreduce/HFileOutputFormat2.java index 0063c56a993f..c911e8c8677a 100644 --- a/hbase-mapreduce/src/main/java/org/apache/hadoop/hbase/mapreduce/HFileOutputFormat2.java +++ b/hbase-mapreduce/src/main/java/org/apache/hadoop/hbase/mapreduce/HFileOutputFormat2.java @@ -73,6 +73,7 @@ import org.apache.hadoop.hbase.regionserver.BloomType; import org.apache.hadoop.hbase.regionserver.HStore; import org.apache.hadoop.hbase.regionserver.StoreFileWriter; +import org.apache.hadoop.hbase.util.BloomFilterUtil; import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; import org.apache.hadoop.hbase.util.FSUtils; @@ -149,6 +150,8 @@ protected static byte[] combineTableNameSuffix(byte[] tableName, byte[] suffix) "hbase.hfileoutputformat.families.compression"; static final String BLOOM_TYPE_FAMILIES_CONF_KEY = "hbase.hfileoutputformat.families.bloomtype"; + static final String BLOOM_PARAM_FAMILIES_CONF_KEY = + "hbase.hfileoutputformat.families.bloomparam"; static final String BLOCK_SIZE_FAMILIES_CONF_KEY = "hbase.mapreduce.hfileoutputformat.blocksize"; static final String DATABLOCK_ENCODING_FAMILIES_CONF_KEY = @@ -216,6 +219,7 @@ protected static byte[] getTableNameSuffixedWithFamily(byte[] tableName, byte[] // create a map from column family to the compression algorithm final Map compressionMap = createFamilyCompressionMap(conf); final Map bloomTypeMap = createFamilyBloomTypeMap(conf); + final Map bloomParamMap = createFamilyBloomParamMap(conf); final Map blockSizeMap = createFamilyBlockSizeMap(conf); String dataBlockEncodingStr = conf.get(DATABLOCK_ENCODING_OVERRIDE_CONF_KEY); @@ -399,6 +403,12 @@ private WriterLength getNewWriter(byte[] tableName, byte[] family, Configuration compression = compression == null ? defaultCompression : compression; BloomType bloomType = bloomTypeMap.get(tableAndFamily); bloomType = bloomType == null ? BloomType.NONE : bloomType; + String bloomParam = bloomParamMap.get(tableAndFamily); + if (bloomType == BloomType.ROWPREFIX_FIXED_LENGTH) { + conf.set(BloomFilterUtil.PREFIX_LENGTH_KEY, bloomParam); + } else if (bloomType == BloomType.ROWPREFIX_DELIMITED) { + conf.set(BloomFilterUtil.DELIMITER_KEY, bloomParam); + } Integer blockSize = blockSizeMap.get(tableAndFamily); blockSize = blockSize == null ? HConstants.DEFAULT_BLOCKSIZE : blockSize; DataBlockEncoding encoding = overriddenEncoding; @@ -667,6 +677,8 @@ static void configureIncrementalLoad(Job job, List multiTableInfo, tableDescriptors)); conf.set(BLOOM_TYPE_FAMILIES_CONF_KEY, serializeColumnFamilyAttribute(bloomTypeDetails, tableDescriptors)); + conf.set(BLOOM_PARAM_FAMILIES_CONF_KEY, serializeColumnFamilyAttribute(bloomParamDetails, + tableDescriptors)); conf.set(DATABLOCK_ENCODING_FAMILIES_CONF_KEY, serializeColumnFamilyAttribute(dataBlockEncodingDetails, tableDescriptors)); @@ -694,6 +706,8 @@ public static void configureIncrementalLoadMap(Job job, TableDescriptor tableDes serializeColumnFamilyAttribute(blockSizeDetails, singleTableDescriptor)); conf.set(BLOOM_TYPE_FAMILIES_CONF_KEY, serializeColumnFamilyAttribute(bloomTypeDetails, singleTableDescriptor)); + conf.set(BLOOM_PARAM_FAMILIES_CONF_KEY, + serializeColumnFamilyAttribute(bloomParamDetails, singleTableDescriptor)); conf.set(DATABLOCK_ENCODING_FAMILIES_CONF_KEY, serializeColumnFamilyAttribute(dataBlockEncodingDetails, singleTableDescriptor)); @@ -741,6 +755,19 @@ static Map createFamilyBloomTypeMap(Configuration conf) { return bloomTypeMap; } + /** + * Runs inside the task to deserialize column family to bloom filter param + * map from the configuration. + * + * @param conf to read the serialized values from + * @return a map from column family to the the configured bloom filter param + */ + @VisibleForTesting + static Map createFamilyBloomParamMap(Configuration conf) { + return createFamilyConfValueMap(conf, BLOOM_PARAM_FAMILIES_CONF_KEY); + } + + /** * Runs inside the task to deserialize column family to block size * map from the configuration. @@ -908,6 +935,30 @@ static String serializeColumnFamilyAttribute(Function bloomParamDetails = familyDescriptor -> { + BloomType bloomType = familyDescriptor.getBloomFilterType(); + String bloomParam = ""; + if (bloomType == BloomType.ROWPREFIX_FIXED_LENGTH) { + bloomParam = familyDescriptor.getConfigurationValue(BloomFilterUtil.PREFIX_LENGTH_KEY); + } else if (bloomType == BloomType.ROWPREFIX_DELIMITED) { + bloomParam = familyDescriptor.getConfigurationValue(BloomFilterUtil.DELIMITER_KEY); + } + return bloomParam; + }; + /** * Serialize column family to data block encoding map to configuration. * Invoked while configuring the MR job for incremental load. diff --git a/hbase-mapreduce/src/main/java/org/apache/hadoop/hbase/mapreduce/replication/VerifyReplication.java b/hbase-mapreduce/src/main/java/org/apache/hadoop/hbase/mapreduce/replication/VerifyReplication.java index d1b5c607fbd3..d5f8215d523f 100644 --- a/hbase-mapreduce/src/main/java/org/apache/hadoop/hbase/mapreduce/replication/VerifyReplication.java +++ b/hbase-mapreduce/src/main/java/org/apache/hadoop/hbase/mapreduce/replication/VerifyReplication.java @@ -352,7 +352,7 @@ public boolean isAborted() { return Pair.newPair(peerConfig, ReplicationUtils.getPeerClusterConfiguration(peerConfig, conf)); } catch (ReplicationException e) { - throw new IOException("An error occurred while trying to connect to the remove peer cluster", + throw new IOException("An error occurred while trying to connect to the remote peer cluster", e); } finally { if (localZKW != null) { diff --git a/hbase-mapreduce/src/test/java/org/apache/hadoop/hbase/mapreduce/TestCopyTable.java b/hbase-mapreduce/src/test/java/org/apache/hadoop/hbase/mapreduce/TestCopyTable.java index 6f416710e1ee..ed6857dd3b6a 100644 --- a/hbase-mapreduce/src/test/java/org/apache/hadoop/hbase/mapreduce/TestCopyTable.java +++ b/hbase-mapreduce/src/test/java/org/apache/hadoop/hbase/mapreduce/TestCopyTable.java @@ -30,15 +30,20 @@ import org.apache.hadoop.hbase.HBaseClassTestRule; import org.apache.hadoop.hbase.HBaseTestingUtility; import org.apache.hadoop.hbase.TableName; +import org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder; import org.apache.hadoop.hbase.client.Get; import org.apache.hadoop.hbase.client.Put; import org.apache.hadoop.hbase.client.Result; import org.apache.hadoop.hbase.client.Table; +import org.apache.hadoop.hbase.client.TableDescriptor; +import org.apache.hadoop.hbase.client.TableDescriptorBuilder; +import org.apache.hadoop.hbase.mob.MobTestUtil; import org.apache.hadoop.hbase.testclassification.LargeTests; import org.apache.hadoop.hbase.testclassification.MapReduceTests; import org.apache.hadoop.hbase.util.Bytes; import org.apache.hadoop.hbase.util.LauncherSecurityManager; import org.apache.hadoop.util.ToolRunner; + import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.ClassRule; @@ -100,7 +105,7 @@ private void doCopyTableTest(boolean bulkload) throws Exception { if (bulkload) { code = ToolRunner.run(new Configuration(TEST_UTIL.getConfiguration()), copy, new String[] { "--new.name=" + tableName2.getNameAsString(), - "--bulkload", tableName1.getNameAsString() }); + "--bulkload", tableName1.getNameAsString() }); } else { code = ToolRunner.run(new Configuration(TEST_UTIL.getConfiguration()), copy, new String[] { "--new.name=" + tableName2.getNameAsString(), @@ -121,9 +126,74 @@ private void doCopyTableTest(boolean bulkload) throws Exception { } } + private void doCopyTableTestWithMob(boolean bulkload) throws Exception { + final TableName tableName1 = TableName.valueOf(name.getMethodName() + "1"); + final TableName tableName2 = TableName.valueOf(name.getMethodName() + "2"); + final byte[] FAMILY = Bytes.toBytes("mob"); + final byte[] COLUMN1 = Bytes.toBytes("c1"); + + ColumnFamilyDescriptorBuilder cfd = ColumnFamilyDescriptorBuilder.newBuilder(FAMILY); + + cfd.setMobEnabled(true); + cfd.setMobThreshold(5); + TableDescriptor desc1 = TableDescriptorBuilder.newBuilder(tableName1) + .setColumnFamily(cfd.build()) + .build(); + TableDescriptor desc2 = TableDescriptorBuilder.newBuilder(tableName2) + .setColumnFamily(cfd.build()) + .build(); + + try (Table t1 = TEST_UTIL.createTable(desc1, null); + Table t2 = TEST_UTIL.createTable(desc2, null);) { + + // put rows into the first table + for (int i = 0; i < 10; i++) { + Put p = new Put(Bytes.toBytes("row" + i)); + p.addColumn(FAMILY, COLUMN1, COLUMN1); + t1.put(p); + } + + CopyTable copy = new CopyTable(); + + int code; + if (bulkload) { + code = ToolRunner.run(new Configuration(TEST_UTIL.getConfiguration()), + copy, new String[] { "--new.name=" + tableName2.getNameAsString(), + "--bulkload", tableName1.getNameAsString() }); + } else { + code = ToolRunner.run(new Configuration(TEST_UTIL.getConfiguration()), + copy, new String[] { "--new.name=" + tableName2.getNameAsString(), + tableName1.getNameAsString() }); + } + assertEquals("copy job failed", 0, code); + + // verify the data was copied into table 2 + for (int i = 0; i < 10; i++) { + Get g = new Get(Bytes.toBytes("row" + i)); + Result r = t2.get(g); + assertEquals(1, r.size()); + assertTrue(CellUtil.matchingQualifier(r.rawCells()[0], COLUMN1)); + assertEquals("compare row values between two tables", + t1.getDescriptor().getValue("row" + i), + t2.getDescriptor().getValue("row" + i)); + } + + assertEquals("compare count of mob rows after table copy", MobTestUtil.countMobRows(t1), + MobTestUtil.countMobRows(t2)); + assertEquals("compare count of mob row values between two tables", + t1.getDescriptor().getValues().size(), + t2.getDescriptor().getValues().size()); + assertTrue("The mob row count is 0 but should be > 0", + MobTestUtil.countMobRows(t2) > 0); + + } finally { + TEST_UTIL.deleteTable(tableName1); + TEST_UTIL.deleteTable(tableName2); + } + } + /** * Simple end-to-end test - * @throws Exception */ @Test public void testCopyTable() throws Exception { @@ -138,56 +208,70 @@ public void testCopyTableWithBulkload() throws Exception { doCopyTableTest(true); } + /** + * Simple end-to-end test on table with MOB + */ + @Test + public void testCopyTableWithMob() throws Exception { + doCopyTableTestWithMob(false); + } + + /** + * Simple end-to-end test with bulkload on table with MOB. + */ + @Test + public void testCopyTableWithBulkloadWithMob() throws Exception { + doCopyTableTestWithMob(true); + } + @Test public void testStartStopRow() throws Exception { final TableName tableName1 = TableName.valueOf(name.getMethodName() + "1"); final TableName tableName2 = TableName.valueOf(name.getMethodName() + "2"); final byte[] FAMILY = Bytes.toBytes("family"); final byte[] COLUMN1 = Bytes.toBytes("c1"); - final byte[] ROW0 = Bytes.toBytesBinary("\\x01row0"); - final byte[] ROW1 = Bytes.toBytesBinary("\\x01row1"); - final byte[] ROW2 = Bytes.toBytesBinary("\\x01row2"); - - Table t1 = TEST_UTIL.createTable(tableName1, FAMILY); - Table t2 = TEST_UTIL.createTable(tableName2, FAMILY); - - // put rows into the first table - Put p = new Put(ROW0); - p.addColumn(FAMILY, COLUMN1, COLUMN1); - t1.put(p); - p = new Put(ROW1); - p.addColumn(FAMILY, COLUMN1, COLUMN1); - t1.put(p); - p = new Put(ROW2); - p.addColumn(FAMILY, COLUMN1, COLUMN1); - t1.put(p); - - CopyTable copy = new CopyTable(); - assertEquals( - 0, - ToolRunner.run(new Configuration(TEST_UTIL.getConfiguration()), - copy, new String[] { "--new.name=" + tableName2, "--startrow=\\x01row1", - "--stoprow=\\x01row2", tableName1.getNameAsString() })); - - // verify the data was copied into table 2 - // row1 exist, row0, row2 do not exist - Get g = new Get(ROW1); - Result r = t2.get(g); - assertEquals(1, r.size()); - assertTrue(CellUtil.matchingQualifier(r.rawCells()[0], COLUMN1)); - - g = new Get(ROW0); - r = t2.get(g); - assertEquals(0, r.size()); - - g = new Get(ROW2); - r = t2.get(g); - assertEquals(0, r.size()); - - t1.close(); - t2.close(); - TEST_UTIL.deleteTable(tableName1); - TEST_UTIL.deleteTable(tableName2); + final byte[] row0 = Bytes.toBytesBinary("\\x01row0"); + final byte[] row1 = Bytes.toBytesBinary("\\x01row1"); + final byte[] row2 = Bytes.toBytesBinary("\\x01row2"); + + try (Table t1 = TEST_UTIL.createTable(tableName1, FAMILY); + Table t2 = TEST_UTIL.createTable(tableName2, FAMILY)) { + + // put rows into the first table + Put p = new Put(row0); + p.addColumn(FAMILY, COLUMN1, COLUMN1); + t1.put(p); + p = new Put(row1); + p.addColumn(FAMILY, COLUMN1, COLUMN1); + t1.put(p); + p = new Put(row2); + p.addColumn(FAMILY, COLUMN1, COLUMN1); + t1.put(p); + + CopyTable copy = new CopyTable(); + assertEquals(0, ToolRunner.run(new Configuration(TEST_UTIL.getConfiguration()), + copy, new String[]{"--new.name=" + tableName2, "--startrow=\\x01row1", + "--stoprow=\\x01row2", tableName1.getNameAsString()})); + + // verify the data was copied into table 2 + // row1 exist, row0, row2 do not exist + Get g = new Get(row1); + Result r = t2.get(g); + assertEquals(1, r.size()); + assertTrue(CellUtil.matchingQualifier(r.rawCells()[0], COLUMN1)); + + g = new Get(row0); + r = t2.get(g); + assertEquals(0, r.size()); + + g = new Get(row2); + r = t2.get(g); + assertEquals(0, r.size()); + + } finally { + TEST_UTIL.deleteTable(tableName1); + TEST_UTIL.deleteTable(tableName2); + } } /** @@ -215,8 +299,8 @@ public void testRenameFamily() throws Exception { long currentTime = System.currentTimeMillis(); String[] args = new String[] { "--new.name=" + targetTable, "--families=a:b", "--all.cells", - "--starttime=" + (currentTime - 100000), "--endtime=" + (currentTime + 100000), - "--versions=1", sourceTable.getNameAsString() }; + "--starttime=" + (currentTime - 100000), "--endtime=" + (currentTime + 100000), + "--versions=1", sourceTable.getNameAsString() }; assertNull(t2.get(new Get(ROW1)).getRow()); assertTrue(runCopy(args)); diff --git a/hbase-mapreduce/src/test/java/org/apache/hadoop/hbase/util/LoadTestTool.java b/hbase-mapreduce/src/test/java/org/apache/hadoop/hbase/util/LoadTestTool.java index f4f483e847f9..f0e04c4d603f 100644 --- a/hbase-mapreduce/src/test/java/org/apache/hadoop/hbase/util/LoadTestTool.java +++ b/hbase-mapreduce/src/test/java/org/apache/hadoop/hbase/util/LoadTestTool.java @@ -119,6 +119,7 @@ public class LoadTestTool extends AbstractHBaseTool { protected static final String OPT_VERBOSE = "verbose"; public static final String OPT_BLOOM = "bloom"; + public static final String OPT_BLOOM_PARAM = "bloom_param"; public static final String OPT_COMPRESSION = "compression"; public static final String OPT_DEFERRED_LOG_FLUSH = "deferredlogflush"; public static final String OPT_DEFERRED_LOG_FLUSH_USAGE = "Enable deferred log flush."; @@ -330,6 +331,7 @@ protected void addOptions() { addOptWithArg(OPT_UPDATE, OPT_USAGE_UPDATE); addOptNoArg(OPT_INIT_ONLY, "Initialize the test table only, don't do any loading"); addOptWithArg(OPT_BLOOM, OPT_USAGE_BLOOM); + addOptWithArg(OPT_BLOOM_PARAM, "the parameter of bloom filter type"); addOptWithArg(OPT_COMPRESSION, OPT_USAGE_COMPRESSION); addOptWithArg(HFileTestUtil.OPT_DATA_BLOCK_ENCODING, HFileTestUtil.OPT_DATA_BLOCK_ENCODING_USAGE); addOptWithArg(OPT_MAX_READ_ERRORS, "The maximum number of read errors " + @@ -551,6 +553,22 @@ private void parseColumnFamilyOptions(CommandLine cmd) { bloomType = bloomStr == null ? BloomType.ROW : BloomType.valueOf(bloomStr); + if (bloomType == BloomType.ROWPREFIX_FIXED_LENGTH) { + if (!cmd.hasOption(OPT_BLOOM_PARAM)) { + LOG.error("the parameter of bloom filter {} is not specified", bloomType.name()); + } else { + conf.set(BloomFilterUtil.PREFIX_LENGTH_KEY, cmd.getOptionValue(OPT_BLOOM_PARAM)); + } + } + + if (bloomType == BloomType.ROWPREFIX_DELIMITED) { + if (!cmd.hasOption(OPT_BLOOM_PARAM)) { + LOG.error("the parameter of bloom filter {} is not specified", bloomType.name()); + } else { + conf.set(BloomFilterUtil.DELIMITER_KEY, cmd.getOptionValue(OPT_BLOOM_PARAM)); + } + } + inMemoryCF = cmd.hasOption(OPT_INMEMORY); if (cmd.hasOption(OPT_ENCRYPTION)) { cipher = Encryption.getCipher(conf, cmd.getOptionValue(OPT_ENCRYPTION)); diff --git a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/AbstractProcedureScheduler.java b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/AbstractProcedureScheduler.java index 5645f89e14d7..7ab1329b32b7 100644 --- a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/AbstractProcedureScheduler.java +++ b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/AbstractProcedureScheduler.java @@ -85,6 +85,11 @@ public void addFront(final Procedure procedure) { push(procedure, true, true); } + @Override + public void addFront(final Procedure procedure, boolean notify) { + push(procedure, true, notify); + } + @Override public void addFront(Iterator procedureIterator) { schedLock(); @@ -109,6 +114,11 @@ public void addBack(final Procedure procedure) { push(procedure, false, true); } + @Override + public void addBack(final Procedure procedure, boolean notify) { + push(procedure, false, notify); + } + protected void push(final Procedure procedure, final boolean addFront, final boolean notify) { schedLock(); try { diff --git a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/LockAndQueue.java b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/LockAndQueue.java index ae8daa283147..4365a2c1950f 100644 --- a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/LockAndQueue.java +++ b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/LockAndQueue.java @@ -73,7 +73,8 @@ public boolean isLockOwner(long procId) { @Override public boolean hasParentLock(Procedure proc) { - // TODO: need to check all the ancestors + // TODO: need to check all the ancestors. need to passed in the procedures + // to find the ancestors. return proc.hasParent() && (isLockOwner(proc.getParentProcId()) || isLockOwner(proc.getRootProcId())); } diff --git a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/Procedure.java b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/Procedure.java index 393bd2fa2c63..a85ccb12925f 100644 --- a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/Procedure.java +++ b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/Procedure.java @@ -145,31 +145,64 @@ public enum LockState { private boolean lockedWhenLoading = false; /** - * Used for force complete of the procedure without - * actually doing any logic in the procedure. + * Used for override complete of the procedure without actually doing any logic in the procedure. * If bypass is set to true, when executing it will return null when - * {@link #doExecute(Object)} to finish the procedure and releasing any locks - * it may currently hold. - * Bypassing a procedure is not like aborting. Aborting a procedure will trigger + * {@link #doExecute(Object)} is called to finish the procedure and release any locks + * it may currently hold. The bypass does cleanup around the Procedure as far as the + * Procedure framework is concerned. It does not clean any internal state that the + * Procedure's themselves may have set. That is for the Procedures to do themselves + * when bypass is called. They should override bypass and do their cleanup in the + * overridden bypass method (be sure to call the parent bypass to ensure proper + * processing). + *

Bypassing a procedure is not like aborting. Aborting a procedure will trigger * a rollback. And since the {@link #abort(Object)} method is overrideable * Some procedures may have chosen to ignore the aborting. */ private volatile boolean bypass = false; + /** + * Indicate whether we need to persist the procedure to ProcedureStore after execution. Default to + * true, and the implementation can all {@link #skipPersistence()} to let the framework skip the + * persistence of the procedure. + *

+ * This is useful when the procedure is in error and you want to retry later. The retry interval + * and the number of retries are usually not critical so skip the persistence can save some + * resources, and also speed up the restart processing. + *

+ * Notice that this value will be reset to true every time before execution. And when rolling back + * we do not test this value. + */ + private boolean persist = true; + public boolean isBypass() { return bypass; } /** - * set the bypass to true - * Only called in {@link ProcedureExecutor#bypassProcedure(long, long, boolean)} for now, - * DO NOT use this method alone, since we can't just bypass - * one single procedure. We need to bypass its ancestor too. So making it package private + * Set the bypass to true. + * Only called in {@link ProcedureExecutor#bypassProcedure(long, long, boolean, boolean)} for now. + * DO NOT use this method alone, since we can't just bypass one single procedure. We need to + * bypass its ancestor too. If your Procedure has set state, it needs to undo it in here. + * @param env Current environment. May be null because of context; e.g. pretty-printing + * procedure WALs where there is no 'environment' (and where Procedures that require + * an 'environment' won't be run. */ - void bypass() { + protected void bypass(TEnvironment env) { this.bypass = true; } + boolean needPersistence() { + return persist; + } + + void resetPersistence() { + persist = true; + } + + protected final void skipPersistence() { + persist = false; + } + /** * The main code of the procedure. It must be idempotent since execute() * may be called multiple times in case of machine failure in the middle @@ -296,7 +329,8 @@ protected boolean holdLock(TEnvironment env) { * @see #holdLock(Object) * @return true if the procedure has the lock, false otherwise. */ - protected final boolean hasLock() { + @VisibleForTesting + public final boolean hasLock() { return locked; } @@ -677,12 +711,16 @@ protected void setResult(byte[] result) { /** * Will only be called when loading procedures from procedure store, where we need to record * whether the procedure has already held a lock. Later we will call - * {@link #doAcquireLock(Object)} to actually acquire the lock. + * {@link #doAcquireLock(Object, ProcedureStore)} to actually acquire the lock. */ final void lockedWhenLoading() { this.lockedWhenLoading = true; } + public boolean isLockedWhenLoading() { + return lockedWhenLoading; + } + // ============================================================================================== // Runtime state, updated every operation by the ProcedureExecutor // @@ -949,7 +987,13 @@ final void restoreLock(TEnvironment env) { LOG.debug("{} is already bypassed, skip acquiring lock.", this); return; } - + // this can happen if the parent stores the sub procedures but before it can + // release its lock, the master restarts + if (getState() == ProcedureState.WAITING && !holdLock(env)) { + LOG.debug("{} is in WAITING STATE, and holdLock= false, skip acquiring lock.", this); + lockedWhenLoading = false; + return; + } LOG.debug("{} held the lock before restarting, call acquireLock to restore it.", this); LockState state = acquireLock(env); assert state == LockState.LOCK_ACQUIRED; diff --git a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/ProcedureExecutor.java b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/ProcedureExecutor.java index e398fa4f292f..01d2b2b501a3 100644 --- a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/ProcedureExecutor.java +++ b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/ProcedureExecutor.java @@ -15,7 +15,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.apache.hadoop.hbase.procedure2; import java.io.IOException; @@ -32,6 +31,8 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -45,6 +46,7 @@ import org.apache.hadoop.hbase.procedure2.Procedure.LockState; import org.apache.hadoop.hbase.procedure2.store.ProcedureStore; import org.apache.hadoop.hbase.procedure2.store.ProcedureStore.ProcedureIterator; +import org.apache.hadoop.hbase.procedure2.store.ProcedureStore.ProcedureStoreListener; import org.apache.hadoop.hbase.procedure2.util.StringUtils; import org.apache.hadoop.hbase.security.User; import org.apache.hadoop.hbase.util.EnvironmentEdgeManager; @@ -57,6 +59,7 @@ import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting; import org.apache.hbase.thirdparty.com.google.common.base.Preconditions; +import org.apache.hbase.thirdparty.com.google.common.util.concurrent.ThreadFactoryBuilder; import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos.ProcedureState; @@ -315,7 +318,7 @@ protected void periodicExecute(final TEnvironment env) { private Configuration conf; /** - * Created in the {@link #start(int, boolean)} method. Destroyed in {@link #join()} (FIX! Doing + * Created in the {@link #init(int, boolean)} method. Destroyed in {@link #join()} (FIX! Doing * resource handling rather than observing in a #join is unexpected). * Overridden when we do the ProcedureTestingUtility.testRecoveryAndDoubleExecution trickery * (Should be ok). @@ -323,7 +326,7 @@ protected void periodicExecute(final TEnvironment env) { private ThreadGroup threadGroup; /** - * Created in the {@link #start(int, boolean)} method. Terminated in {@link #join()} (FIX! Doing + * Created in the {@link #init(int, boolean)} method. Terminated in {@link #join()} (FIX! Doing * resource handling rather than observing in a #join is unexpected). * Overridden when we do the ProcedureTestingUtility.testRecoveryAndDoubleExecution trickery * (Should be ok). @@ -331,7 +334,7 @@ protected void periodicExecute(final TEnvironment env) { private CopyOnWriteArrayList workerThreads; /** - * Created in the {@link #start(int, boolean)} method. Terminated in {@link #join()} (FIX! Doing + * Created in the {@link #init(int, boolean)} method. Terminated in {@link #join()} (FIX! Doing * resource handling rather than observing in a #join is unexpected). * Overridden when we do the ProcedureTestingUtility.testRecoveryAndDoubleExecution trickery * (Should be ok). @@ -348,6 +351,9 @@ protected void periodicExecute(final TEnvironment env) { */ private final ProcedureScheduler scheduler; + private final Executor forceUpdateExecutor = Executors.newSingleThreadExecutor( + new ThreadFactoryBuilder().setDaemon(true).setNameFormat("Force-Update-PEWorker-%d").build()); + private final AtomicLong lastProcId = new AtomicLong(-1); private final AtomicLong workerId = new AtomicLong(0); private final AtomicInteger activeExecutorCount = new AtomicInteger(0); @@ -370,6 +376,32 @@ public ProcedureExecutor(final Configuration conf, final TEnvironment environmen this(conf, environment, store, new SimpleProcedureScheduler()); } + private boolean isRootFinished(Procedure proc) { + Procedure rootProc = procedures.get(proc.getRootProcId()); + return rootProc == null || rootProc.isFinished(); + } + + private void forceUpdateProcedure(long procId) throws IOException { + IdLock.Entry lockEntry = procExecutionLock.getLockEntry(procId); + try { + Procedure proc = procedures.get(procId); + if (proc == null) { + LOG.debug("No pending procedure with id = {}, skip force updating.", procId); + return; + } + // For a sub procedure which root parent has not been finished, we still need to retain the + // wal even if the procedure itself is finished. + if (proc.isFinished() && (!proc.hasParent() || isRootFinished(proc))) { + LOG.debug("Procedure {} has already been finished, skip force updating.", proc); + return; + } + LOG.debug("Force update procedure {}", proc); + store.update(proc); + } finally { + procExecutionLock.releaseLockEntry(lockEntry); + } + } + public ProcedureExecutor(final Configuration conf, final TEnvironment environment, final ProcedureStore store, final ProcedureScheduler scheduler) { this.environment = environment; @@ -378,7 +410,19 @@ public ProcedureExecutor(final Configuration conf, final TEnvironment environmen this.conf = conf; this.checkOwnerSet = conf.getBoolean(CHECK_OWNER_SET_CONF_KEY, DEFAULT_CHECK_OWNER_SET); refreshConfiguration(conf); + store.registerListener(new ProcedureStoreListener() { + @Override + public void forceUpdate(long[] procIds) { + Arrays.stream(procIds).forEach(procId -> forceUpdateExecutor.execute(() -> { + try { + forceUpdateProcedure(procId); + } catch (IOException e) { + LOG.warn("Failed to force update procedure with pid={}", procId); + } + })); + } + }); } private void load(final boolean abortOnCorruption) throws IOException { @@ -461,8 +505,10 @@ private void restoreLocks() { private void loadProcedures(ProcedureIterator procIter, boolean abortOnCorruption) throws IOException { // 1. Build the rollback stack - int runnablesCount = 0; + int runnableCount = 0; int failedCount = 0; + int waitingCount = 0; + int waitingTimeoutCount = 0; while (procIter.hasNext()) { boolean finished = procIter.isNextFinished(); @SuppressWarnings("unchecked") @@ -482,11 +528,21 @@ private void loadProcedures(ProcedureIterator procIter, boolean abortOnCorruptio // add the procedure to the map proc.beforeReplay(getEnvironment()); procedures.put(proc.getProcId(), proc); - - if (proc.getState() == ProcedureState.RUNNABLE) { - runnablesCount++; - } else if (proc.getState() == ProcedureState.FAILED) { - failedCount++; + switch (proc.getState()) { + case RUNNABLE: + runnableCount++; + break; + case FAILED: + failedCount++; + break; + case WAITING: + waitingCount++; + break; + case WAITING_TIMEOUT: + waitingTimeoutCount++; + break; + default: + break; } } @@ -507,9 +563,10 @@ private void loadProcedures(ProcedureIterator procIter, boolean abortOnCorruptio // have been polled out already, so when loading we can not add the procedure to scheduler first // and then call acquireLock, since the procedure is still in the queue, and since we will // remove the queue from runQueue, then no one can poll it out, then there is a dead lock - List> runnableList = new ArrayList<>(runnablesCount); + List> runnableList = new ArrayList<>(runnableCount); List> failedList = new ArrayList<>(failedCount); - Set> waitingSet = null; + List> waitingList = new ArrayList<>(waitingCount); + List> waitingTimeoutList = new ArrayList<>(waitingTimeoutCount); procIter.reset(); while (procIter.hasNext()) { if (procIter.isNextFinished()) { @@ -524,15 +581,11 @@ private void loadProcedures(ProcedureIterator procIter, boolean abortOnCorruptio LOG.debug("Loading {}", proc); Long rootProcId = getRootProcedureId(proc); - if (rootProcId == null) { - // The 'proc' was ready to run but the root procedure was rolledback? - scheduler.addBack(proc); - continue; - } + // The orphan procedures will be passed to handleCorrupted, so add an assert here + assert rootProcId != null; if (proc.hasParent()) { Procedure parent = procedures.get(proc.getParentProcId()); - // corrupted procedures are handled later at step 3 if (parent != null && !proc.isFinished()) { parent.incChildrenLatch(); } @@ -547,26 +600,10 @@ private void loadProcedures(ProcedureIterator procIter, boolean abortOnCorruptio runnableList.add(proc); break; case WAITING: - if (!proc.hasChildren()) { - // Normally, WAITING procedures should be waken by its children. - // But, there is a case that, all the children are successful and before - // they can wake up their parent procedure, the master was killed. - // So, during recovering the procedures from ProcedureWal, its children - // are not loaded because of their SUCCESS state. - // So we need to continue to run this WAITING procedure. But before - // executing, we need to set its state to RUNNABLE, otherwise, a exception - // will throw: - // Preconditions.checkArgument(procedure.getState() == ProcedureState.RUNNABLE, - // "NOT RUNNABLE! " + procedure.toString()); - proc.setState(ProcedureState.RUNNABLE); - runnableList.add(proc); - } + waitingList.add(proc); break; case WAITING_TIMEOUT: - if (waitingSet == null) { - waitingSet = new HashSet<>(); - } - waitingSet.add(proc); + waitingTimeoutList.add(proc); break; case FAILED: failedList.add(proc); @@ -581,36 +618,31 @@ private void loadProcedures(ProcedureIterator procIter, boolean abortOnCorruptio } } - // 3. Validate the stacks - int corruptedCount = 0; - Iterator>> itStack = - rollbackStack.entrySet().iterator(); - while (itStack.hasNext()) { - Map.Entry> entry = itStack.next(); - RootProcedureState procStack = entry.getValue(); - if (procStack.isValid()) continue; - - for (Procedure proc : procStack.getSubproceduresStack()) { - LOG.error("Corrupted " + proc); - procedures.remove(proc.getProcId()); - runnableList.remove(proc); - if (waitingSet != null) waitingSet.remove(proc); - corruptedCount++; + // 3. Check the waiting procedures to see if some of them can be added to runnable. + waitingList.forEach(proc -> { + if (!proc.hasChildren()) { + // Normally, WAITING procedures should be waken by its children. + // But, there is a case that, all the children are successful and before + // they can wake up their parent procedure, the master was killed. + // So, during recovering the procedures from ProcedureWal, its children + // are not loaded because of their SUCCESS state. + // So we need to continue to run this WAITING procedure. But before + // executing, we need to set its state to RUNNABLE, otherwise, a exception + // will throw: + // Preconditions.checkArgument(procedure.getState() == ProcedureState.RUNNABLE, + // "NOT RUNNABLE! " + procedure.toString()); + proc.setState(ProcedureState.RUNNABLE); + runnableList.add(proc); + } else { + proc.afterReplay(getEnvironment()); } - itStack.remove(); - } - - if (abortOnCorruption && corruptedCount > 0) { - throw new IOException("found " + corruptedCount + " procedures on replay"); - } + }); // 4. Push the procedures to the timeout executor - if (waitingSet != null && !waitingSet.isEmpty()) { - for (Procedure proc: waitingSet) { - proc.afterReplay(getEnvironment()); - timeoutExecutor.add(proc); - } - } + waitingTimeoutList.forEach(proc -> { + proc.afterReplay(getEnvironment()); + timeoutExecutor.add(proc); + }); // 5. restore locks restoreLocks(); // 6. Push the procedure to the scheduler @@ -620,8 +652,30 @@ private void loadProcedures(ProcedureIterator procIter, boolean abortOnCorruptio if (!p.hasParent()) { sendProcedureLoadedNotification(p.getProcId()); } - scheduler.addBack(p); + // If the procedure holds the lock, put the procedure in front + // If its parent holds the lock, put the procedure in front + // TODO. Is that possible that its ancestor holds the lock? + // For now, the deepest procedure hierarchy is: + // ModifyTableProcedure -> ReopenTableProcedure -> + // MoveTableProcedure -> Unassign/AssignProcedure + // But ModifyTableProcedure and ReopenTableProcedure won't hold the lock + // So, check parent lock is enough(a tricky case is resovled by HBASE-21384). + // If some one change or add new procedures making 'grandpa' procedure + // holds the lock, but parent procedure don't hold the lock, there will + // be a problem here. We have to check one procedure's ancestors. + // And we need to change LockAndQueue.hasParentLock(Procedure proc) method + // to check all ancestors too. + if (p.isLockedWhenLoading() || (p.hasParent() && procedures + .get(p.getParentProcId()).isLockedWhenLoading())) { + scheduler.addFront(p, false); + } else { + // if it was not, it can wait. + scheduler.addBack(p, false); + } }); + // After all procedures put into the queue, signal the worker threads. + // Otherwise, there is a race condition. See HBASE-21364. + scheduler.signalAll(); } /** @@ -977,7 +1031,7 @@ public long submitProcedure(Procedure proc) { * Bypass a procedure. If the procedure is set to bypass, all the logic in * execute/rollback will be ignored and it will return success, whatever. * It is used to recover buggy stuck procedures, releasing the lock resources - * and letting other procedures to run. Bypassing one procedure (and its ancestors will + * and letting other procedures run. Bypassing one procedure (and its ancestors will * be bypassed automatically) may leave the cluster in a middle state, e.g. region * not assigned, or some hdfs files left behind. After getting rid of those stuck procedures, * the operators may have to do some clean up on hdfs or schedule some assign procedures @@ -996,7 +1050,7 @@ public long submitProcedure(Procedure proc) { *

* If the procedure is in WAITING state, will set it to RUNNABLE add it to run queue. * TODO: What about WAITING_TIMEOUT? - * @param id the procedure id + * @param pids the procedure id * @param lockWait time to wait lock * @param force if force set to true, we will bypass the procedure even if it is executing. * This is for procedures which can't break out during executing(due to bug, mostly) @@ -1004,26 +1058,39 @@ public long submitProcedure(Procedure proc) { * there. We need to restart the master after bypassing, and letting the problematic * procedure to execute wth bypass=true, so in that condition, the procedure can be * successfully bypassed. + * @param recursive We will do an expensive search for children of each pid. EXPENSIVE! * @return true if bypass success * @throws IOException IOException */ - public boolean bypassProcedure(long id, long lockWait, boolean force) throws IOException { - Procedure procedure = getProcedure(id); + public List bypassProcedure(List pids, long lockWait, boolean force, + boolean recursive) + throws IOException { + List result = new ArrayList(pids.size()); + for(long pid: pids) { + result.add(bypassProcedure(pid, lockWait, force, recursive)); + } + return result; + } + + boolean bypassProcedure(long pid, long lockWait, boolean override, boolean recursive) + throws IOException { + Preconditions.checkArgument(lockWait > 0, "lockWait should be positive"); + final Procedure procedure = getProcedure(pid); if (procedure == null) { - LOG.debug("Procedure with id={} does not exist, skipping bypass", id); + LOG.debug("Procedure pid={} does not exist, skipping bypass", pid); return false; } - LOG.debug("Begin bypass {} with lockWait={}, force={}", procedure, lockWait, force); - + LOG.debug("Begin bypass {} with lockWait={}, override={}, recursive={}", + procedure, lockWait, override, recursive); IdLock.Entry lockEntry = procExecutionLock.tryLockEntry(procedure.getProcId(), lockWait); - if (lockEntry == null && !force) { + if (lockEntry == null && !override) { LOG.debug("Waited {} ms, but {} is still running, skipping bypass with force={}", - lockWait, procedure, force); + lockWait, procedure, override); return false; } else if (lockEntry == null) { LOG.debug("Waited {} ms, but {} is still running, begin bypass with force={}", - lockWait, procedure, force); + lockWait, procedure, override); } try { // check whether the procedure is already finished @@ -1033,8 +1100,25 @@ public boolean bypassProcedure(long id, long lockWait, boolean force) throws IOE } if (procedure.hasChildren()) { - LOG.debug("{} has children, skipping bypass", procedure); - return false; + if (recursive) { + // EXPENSIVE. Checks each live procedure of which there could be many!!! + // Is there another way to get children of a procedure? + LOG.info("Recursive bypass on children of pid={}", procedure.getProcId()); + this.procedures.forEachValue(1 /*Single-threaded*/, + // Transformer + v -> v.getParentProcId() == procedure.getProcId()? v: null, + // Consumer + v -> { + try { + bypassProcedure(v.getProcId(), lockWait, override, recursive); + } catch (IOException e) { + LOG.warn("Recursive bypass of pid={}", v.getProcId(), e); + } + }); + } else { + LOG.debug("{} has children, skipping bypass", procedure); + return false; + } } // If the procedure has no parent or no child, we are safe to bypass it in whatever state @@ -1044,16 +1128,17 @@ public boolean bypassProcedure(long id, long lockWait, boolean force) throws IOE LOG.debug("Bypassing procedures in RUNNABLE, WAITING and WAITING_TIMEOUT states " + "(with no parent), {}", procedure); + // Question: how is the bypass done here? return false; } // Now, the procedure is not finished, and no one can execute it since we take the lock now // And we can be sure that its ancestor is not running too, since their child has not // finished yet - Procedure current = procedure; + Procedure current = procedure; while (current != null) { LOG.debug("Bypassing {}", current); - current.bypass(); + current.bypass(getEnvironment()); store.update(procedure); long parentID = current.getParentProcId(); current = getProcedure(parentID); @@ -1549,7 +1634,19 @@ private LockState executeRollback(long rootProcId, RootProcedureState 0) { Procedure proc = subprocStack.get(stackTail); - + // For the sub procedures which are successfully finished, we do not rollback them. + // Typically, if we want to rollback a procedure, we first need to rollback it, and then + // recursively rollback its ancestors. The state changes which are done by sub procedures + // should be handled by parent procedures when rolling back. For example, when rolling back a + // MergeTableProcedure, we will schedule new procedures to bring the offline regions online, + // instead of rolling back the original procedures which offlined the regions(in fact these + // procedures can not be rolled back...). + if (proc.isSuccess()) { + // Just do the cleanup work, without actually executing the rollback + subprocStack.remove(stackTail); + cleanupAfterRollbackOneStep(proc); + continue; + } LockState lockState = acquireLock(proc); if (lockState != LockState.LOCK_ACQUIRED) { // can't take a lock on the procedure, add the root-proc back on the @@ -1588,6 +1685,31 @@ private LockState executeRollback(long rootProcId, RootProcedureState proc) { + if (proc.removeStackIndex()) { + if (!proc.isSuccess()) { + proc.setState(ProcedureState.ROLLEDBACK); + } + + // update metrics on finishing the procedure (fail) + proc.updateMetricsOnFinish(getEnvironment(), proc.elapsedTime(), false); + + if (proc.hasParent()) { + store.delete(proc.getProcId()); + procedures.remove(proc.getProcId()); + } else { + final long[] childProcIds = rollbackStack.get(proc.getProcId()).getSubprocedureIds(); + if (childProcIds != null) { + store.delete(proc, childProcIds); + } else { + store.update(proc); + } + } + } else { + store.update(proc); + } + } + /** * Execute the rollback of the procedure step. * It updates the store with the new state (stack index) @@ -1616,26 +1738,7 @@ private LockState executeRollback(Procedure proc) { throw new RuntimeException(msg); } - if (proc.removeStackIndex()) { - proc.setState(ProcedureState.ROLLEDBACK); - - // update metrics on finishing the procedure (fail) - proc.updateMetricsOnFinish(getEnvironment(), proc.elapsedTime(), false); - - if (proc.hasParent()) { - store.delete(proc.getProcId()); - procedures.remove(proc.getProcId()); - } else { - final long[] childProcIds = rollbackStack.get(proc.getProcId()).getSubprocedureIds(); - if (childProcIds != null) { - store.delete(proc, childProcIds); - } else { - store.update(proc); - } - } - } else { - store.update(proc); - } + cleanupAfterRollbackOneStep(proc); return LockState.LOCK_ACQUIRED; } @@ -1691,6 +1794,7 @@ private void execProcedure(RootProcedureState procStack, Procedure[] subprocs = null; do { reExecute = false; + procedure.resetPersistence(); try { subprocs = procedure.doExecute(getEnvironment()); if (subprocs != null && subprocs.length == 0) { @@ -1759,7 +1863,9 @@ private void execProcedure(RootProcedureState procStack, // // Commit the transaction even if a suspend (state may have changed). Note this append // can take a bunch of time to complete. - updateStoreOnExec(procStack, procedure, subprocs); + if (procedure.needPersistence()) { + updateStoreOnExec(procStack, procedure, subprocs); + } // if the store is not running we are aborting if (!store.isRunning()) { @@ -1953,6 +2059,11 @@ RootProcedureState getProcStack(long rootProcId) { return rollbackStack.get(rootProcId); } + @VisibleForTesting + ProcedureScheduler getProcedureScheduler() { + return scheduler; + } + // ========================================================================== // Worker Thread // ========================================================================== @@ -1973,7 +2084,6 @@ protected WorkerThread(ThreadGroup group, String prefix) { public void sendStopSignal() { scheduler.signalAll(); } - @Override public void run() { long lastUpdate = EnvironmentEdgeManager.currentTime(); diff --git a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/ProcedureScheduler.java b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/ProcedureScheduler.java index e7e1cdbe4076..9489f52f3fa0 100644 --- a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/ProcedureScheduler.java +++ b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/ProcedureScheduler.java @@ -52,6 +52,13 @@ public interface ProcedureScheduler { */ void addFront(Procedure proc); + /** + * Inserts the specified element at the front of this queue. + * @param proc the Procedure to add + * @param notify whether need to notify worker + */ + void addFront(Procedure proc, boolean notify); + /** * Inserts all elements in the iterator at the front of this queue. */ @@ -63,6 +70,13 @@ public interface ProcedureScheduler { */ void addBack(Procedure proc); + /** + * Inserts the specified element at the end of this queue. + * @param proc the Procedure to add + * @param notify whether need to notify worker + */ + void addBack(Procedure proc, boolean notify); + /** * The procedure can't run at the moment. * add it back to the queue, giving priority to someone else. diff --git a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/ProcedureUtil.java b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/ProcedureUtil.java index 8c8746e84bb4..b7362200d0a2 100644 --- a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/ProcedureUtil.java +++ b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/ProcedureUtil.java @@ -21,6 +21,7 @@ import java.io.InputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Modifier; +import java.util.concurrent.ThreadLocalRandom; import org.apache.hadoop.hbase.HConstants; import org.apache.yetus.audience.InterfaceAudience; import org.apache.hbase.thirdparty.com.google.common.base.Preconditions; @@ -271,7 +272,7 @@ public static Procedure convertToProcedure(ProcedureProtos.Procedure proto) } if (proto.getBypass()) { - proc.bypass(); + proc.bypass(null); } ProcedureStateSerializer serializer = null; @@ -343,6 +344,9 @@ public static long getBackoffTimeMs(int attempts) { if (attempts >= 30) { return maxBackoffTime; } - return Math.min((long) (1000 * Math.pow(2, attempts)), maxBackoffTime); + long backoffTimeMs = Math.min((long) (1000 * Math.pow(2, attempts)), maxBackoffTime); + // 1% possible jitter + long jitter = (long) (backoffTimeMs * ThreadLocalRandom.current().nextFloat() * 0.01f); + return backoffTimeMs + jitter; } } diff --git a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/RemoteProcedureException.java b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/RemoteProcedureException.java index 6f4795d9dc48..f2ad17fbdbd3 100644 --- a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/RemoteProcedureException.java +++ b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/RemoteProcedureException.java @@ -74,6 +74,10 @@ public Exception unwrapRemoteException() { return new Exception(cause); } + // NOTE: Does not throw DoNotRetryIOE because it does not + // have access (DNRIOE is in the client module). Use + // MasterProcedureUtil.unwrapRemoteIOException if need to + // throw DNRIOE. public IOException unwrapRemoteIOException() { final Exception cause = unwrapRemoteException(); if (cause instanceof IOException) { diff --git a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/RootProcedureState.java b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/RootProcedureState.java index 2fc00301e915..a7cdaabb2489 100644 --- a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/RootProcedureState.java +++ b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/RootProcedureState.java @@ -24,6 +24,8 @@ import java.util.Set; import org.apache.yetus.audience.InterfaceAudience; import org.apache.yetus.audience.InterfaceStability; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos.ProcedureState; @@ -42,6 +44,8 @@ @InterfaceStability.Evolving class RootProcedureState { + private static final Logger LOG = LoggerFactory.getLogger(RootProcedureState.class); + private enum State { RUNNING, // The Procedure is running or ready to run FAILED, // The Procedure failed, waiting for the rollback executing @@ -146,6 +150,7 @@ protected synchronized void addRollbackStep(Procedure proc) { subprocStack = new ArrayList<>(); } proc.addStackIndex(subprocStack.size()); + LOG.debug("Add procedure {} as the {}th rollback step", proc, subprocStack.size()); subprocStack.add(proc); } diff --git a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/StateMachineProcedure.java b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/StateMachineProcedure.java index 986b250d9bda..0a087a6a0dd0 100644 --- a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/StateMachineProcedure.java +++ b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/StateMachineProcedure.java @@ -219,12 +219,16 @@ protected boolean isEofState() { @Override protected boolean abort(final TEnvironment env) { LOG.debug("Abort requested for {}", this); - if (hasMoreState()) { - aborted.set(true); - return true; + if (!hasMoreState()) { + LOG.warn("Ignore abort request on {} because it has already been finished", this); + return false; } - LOG.debug("Ignoring abort request on {}", this); - return false; + if (!isRollbackSupported(getCurrentState())) { + LOG.warn("Ignore abort request on {} because it does not support rollback", this); + return false; + } + aborted.set(true); + return true; } /** diff --git a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/BitSetNode.java b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/BitSetNode.java new file mode 100644 index 000000000000..3102bde7d193 --- /dev/null +++ b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/BitSetNode.java @@ -0,0 +1,423 @@ +/** + * 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.hadoop.hbase.procedure2.store; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.apache.hadoop.hbase.procedure2.Procedure; +import org.apache.hadoop.hbase.procedure2.store.ProcedureStoreTracker.DeleteState; +import org.apache.yetus.audience.InterfaceAudience; + +import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos; + +/** + * A bitmap which can grow/merge with other {@link BitSetNode} (if certain conditions are met). + * Boundaries of bitmap are aligned to multiples of {@link BitSetNode#BITS_PER_WORD}. So the range + * of a {@link BitSetNode} is from [x * K, y * K) where x and y are integers, y > x and K is + * BITS_PER_WORD. + *

+ * We have two main bit sets to describe the state of procedures, the meanings are: + * + *

+ *  ----------------------
+ * | modified | deleted |  meaning
+ * |     0    |   0     |  proc exists, but hasn't been updated since last resetUpdates().
+ * |     1    |   0     |  proc was updated (but not deleted).
+ * |     1    |   1     |  proc was deleted.
+ * |     0    |   1     |  proc doesn't exist (maybe never created, maybe deleted in past).
+ * ----------------------
+ * 
+ * + * The meaning of modified is that, we have modified the state of the procedure, no matter insert, + * update, or delete. And if it is an insert or update, we will set the deleted to 0, if not we will + * set the delete to 1. + *

+ * For a non-partial BitSetNode, the initial modified value is 0 and deleted value is 1. For the + * partial one, the initial modified value is 0 and the initial deleted value is also 0. In + * {@link #unsetPartialFlag()} we will reset the deleted to 1 if it is not modified. + */ +@InterfaceAudience.Private +class BitSetNode { + private static final long WORD_MASK = 0xffffffffffffffffL; + private static final int ADDRESS_BITS_PER_WORD = 6; + private static final int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD; + private static final int MAX_NODE_SIZE = 1 << ADDRESS_BITS_PER_WORD; + + /** + * Mimics {@link ProcedureStoreTracker#partial}. It will effect how we fill the new deleted bits + * when growing. + */ + private boolean partial; + + /** + * Set of procedures which have been modified since last {@link #resetModified()}. Useful to track + * procedures which have been modified since last WAL write. + */ + private long[] modified; + + /** + * Keeps track of procedure ids which belong to this bitmap's range and have been deleted. This + * represents global state since it's not reset on WAL rolls. + */ + private long[] deleted; + /** + * Offset of bitmap i.e. procedure id corresponding to first bit. + */ + private long start; + + public void dump() { + System.out.printf("%06d:%06d min=%d max=%d%n", getStart(), getEnd(), getActiveMinProcId(), + getActiveMaxProcId()); + System.out.println("Modified:"); + for (int i = 0; i < modified.length; ++i) { + for (int j = 0; j < BITS_PER_WORD; ++j) { + System.out.print((modified[i] & (1L << j)) != 0 ? "1" : "0"); + } + System.out.println(" " + i); + } + System.out.println(); + System.out.println("Delete:"); + for (int i = 0; i < deleted.length; ++i) { + for (int j = 0; j < BITS_PER_WORD; ++j) { + System.out.print((deleted[i] & (1L << j)) != 0 ? "1" : "0"); + } + System.out.println(" " + i); + } + System.out.println(); + } + + public BitSetNode(long procId, boolean partial) { + start = alignDown(procId); + + int count = 1; + modified = new long[count]; + deleted = new long[count]; + if (!partial) { + Arrays.fill(deleted, WORD_MASK); + } + + this.partial = partial; + updateState(procId, false); + } + + public BitSetNode(ProcedureProtos.ProcedureStoreTracker.TrackerNode data) { + start = data.getStartId(); + int size = data.getUpdatedCount(); + assert size == data.getDeletedCount(); + modified = new long[size]; + deleted = new long[size]; + for (int i = 0; i < size; ++i) { + modified[i] = data.getUpdated(i); + deleted[i] = data.getDeleted(i); + } + partial = false; + } + + public BitSetNode(BitSetNode other, boolean resetDelete) { + this.start = other.start; + // The resetDelete will be set to true when building cleanup tracker. + // as we will reset deleted flags for all the unmodified bits to 1, the partial flag is useless + // so set it to false for not confusing the developers when debugging. + this.partial = resetDelete ? false : other.partial; + this.modified = other.modified.clone(); + // The intention here is that, if a procedure is not modified in this tracker, then we do not + // need to take care of it, so we will set deleted to true for these bits, i.e, if modified is + // 0, then we set deleted to 1, otherwise keep it as is. So here, the equation is + // deleted |= ~modified, i.e, + if (resetDelete) { + this.deleted = new long[other.deleted.length]; + for (int i = 0; i < this.deleted.length; ++i) { + this.deleted[i] |= ~(other.modified[i]); + } + } else { + this.deleted = other.deleted.clone(); + } + } + + public void insertOrUpdate(final long procId) { + updateState(procId, false); + } + + public void delete(final long procId) { + updateState(procId, true); + } + + public long getStart() { + return start; + } + + public long getEnd() { + return start + (modified.length << ADDRESS_BITS_PER_WORD) - 1; + } + + public boolean contains(final long procId) { + return start <= procId && procId <= getEnd(); + } + + public DeleteState isDeleted(final long procId) { + int bitmapIndex = getBitmapIndex(procId); + int wordIndex = bitmapIndex >> ADDRESS_BITS_PER_WORD; + if (wordIndex >= deleted.length) { + return DeleteState.MAYBE; + } + return (deleted[wordIndex] & (1L << bitmapIndex)) != 0 ? DeleteState.YES : DeleteState.NO; + } + + public boolean isModified(long procId) { + int bitmapIndex = getBitmapIndex(procId); + int wordIndex = bitmapIndex >> ADDRESS_BITS_PER_WORD; + if (wordIndex >= modified.length) { + return false; + } + return (modified[wordIndex] & (1L << bitmapIndex)) != 0; + } + + /** + * @return true, if all the procedures has been modified. + */ + public boolean isAllModified() { + // TODO: cache the value + for (int i = 0; i < modified.length; ++i) { + if ((modified[i] | deleted[i]) != WORD_MASK) { + return false; + } + } + return true; + } + + /** + * @return all the active procedure ids in this bit set. + */ + public long[] getActiveProcIds() { + List procIds = new ArrayList<>(); + for (int wordIndex = 0; wordIndex < modified.length; wordIndex++) { + if (deleted[wordIndex] == WORD_MASK || modified[wordIndex] == 0) { + // This should be the common case, where most procedures has been deleted. + continue; + } + long baseProcId = getStart() + (wordIndex << ADDRESS_BITS_PER_WORD); + for (int i = 0; i < (1 << ADDRESS_BITS_PER_WORD); i++) { + long mask = 1L << i; + if ((deleted[wordIndex] & mask) == 0 && (modified[wordIndex] & mask) != 0) { + procIds.add(baseProcId + i); + } + } + } + return procIds.stream().mapToLong(Long::longValue).toArray(); + } + + /** + * @return true, if there are no active procedures in this BitSetNode, else false. + */ + public boolean isEmpty() { + // TODO: cache the value + for (int i = 0; i < deleted.length; ++i) { + if (deleted[i] != WORD_MASK) { + return false; + } + } + return true; + } + + public void resetModified() { + Arrays.fill(modified, 0); + } + + public void unsetPartialFlag() { + partial = false; + for (int i = 0; i < modified.length; ++i) { + for (int j = 0; j < BITS_PER_WORD; ++j) { + if ((modified[i] & (1L << j)) == 0) { + deleted[i] |= (1L << j); + } + } + } + } + + /** + * Convert to + * org.apache.hadoop.hbase.protobuf.generated.ProcedureProtos.ProcedureStoreTracker.TrackerNode + * protobuf. + */ + public ProcedureProtos.ProcedureStoreTracker.TrackerNode convert() { + ProcedureProtos.ProcedureStoreTracker.TrackerNode.Builder builder = + ProcedureProtos.ProcedureStoreTracker.TrackerNode.newBuilder(); + builder.setStartId(start); + for (int i = 0; i < modified.length; ++i) { + builder.addUpdated(modified[i]); + builder.addDeleted(deleted[i]); + } + return builder.build(); + } + + // ======================================================================== + // Grow/Merge Helpers + // ======================================================================== + public boolean canGrow(final long procId) { + return Math.abs(procId - start) < MAX_NODE_SIZE; + } + + public boolean canMerge(final BitSetNode rightNode) { + // Can just compare 'starts' since boundaries are aligned to multiples of BITS_PER_WORD. + assert start < rightNode.start; + return (rightNode.getEnd() - start) < MAX_NODE_SIZE; + } + + public void grow(final long procId) { + int delta, offset; + + if (procId < start) { + // add to head + long newStart = alignDown(procId); + delta = (int) (start - newStart) >> ADDRESS_BITS_PER_WORD; + offset = delta; + start = newStart; + } else { + // Add to tail + long newEnd = alignUp(procId + 1); + delta = (int) (newEnd - getEnd()) >> ADDRESS_BITS_PER_WORD; + offset = 0; + } + + long[] newBitmap; + int oldSize = modified.length; + + newBitmap = new long[oldSize + delta]; + for (int i = 0; i < newBitmap.length; ++i) { + newBitmap[i] = 0; + } + System.arraycopy(modified, 0, newBitmap, offset, oldSize); + modified = newBitmap; + + newBitmap = new long[deleted.length + delta]; + for (int i = 0; i < newBitmap.length; ++i) { + newBitmap[i] = partial ? 0 : WORD_MASK; + } + System.arraycopy(deleted, 0, newBitmap, offset, oldSize); + deleted = newBitmap; + } + + public void merge(final BitSetNode rightNode) { + int delta = (int) (rightNode.getEnd() - getEnd()) >> ADDRESS_BITS_PER_WORD; + + long[] newBitmap; + int oldSize = modified.length; + int newSize = (delta - rightNode.modified.length); + int offset = oldSize + newSize; + + newBitmap = new long[oldSize + delta]; + System.arraycopy(modified, 0, newBitmap, 0, oldSize); + System.arraycopy(rightNode.modified, 0, newBitmap, offset, rightNode.modified.length); + modified = newBitmap; + + newBitmap = new long[oldSize + delta]; + System.arraycopy(deleted, 0, newBitmap, 0, oldSize); + System.arraycopy(rightNode.deleted, 0, newBitmap, offset, rightNode.deleted.length); + deleted = newBitmap; + + for (int i = 0; i < newSize; ++i) { + modified[offset + i] = 0; + deleted[offset + i] = partial ? 0 : WORD_MASK; + } + } + + @Override + public String toString() { + return "BitSetNode(" + getStart() + "-" + getEnd() + ")"; + } + + // ======================================================================== + // Min/Max Helpers + // ======================================================================== + public long getActiveMinProcId() { + long minProcId = start; + for (int i = 0; i < deleted.length; ++i) { + if (deleted[i] == 0) { + return minProcId; + } + + if (deleted[i] != WORD_MASK) { + for (int j = 0; j < BITS_PER_WORD; ++j) { + if ((deleted[i] & (1L << j)) == 0) { + return minProcId + j; + } + } + } + + minProcId += BITS_PER_WORD; + } + return Procedure.NO_PROC_ID; + } + + public long getActiveMaxProcId() { + long maxProcId = getEnd(); + for (int i = deleted.length - 1; i >= 0; --i) { + if (deleted[i] == 0) { + return maxProcId; + } + + if (deleted[i] != WORD_MASK) { + for (int j = BITS_PER_WORD - 1; j >= 0; --j) { + if ((deleted[i] & (1L << j)) == 0) { + return maxProcId - (BITS_PER_WORD - 1 - j); + } + } + } + maxProcId -= BITS_PER_WORD; + } + return Procedure.NO_PROC_ID; + } + + // ======================================================================== + // Bitmap Helpers + // ======================================================================== + private int getBitmapIndex(final long procId) { + return (int) (procId - start); + } + + void updateState(long procId, boolean isDeleted) { + int bitmapIndex = getBitmapIndex(procId); + int wordIndex = bitmapIndex >> ADDRESS_BITS_PER_WORD; + long value = (1L << bitmapIndex); + + modified[wordIndex] |= value; + if (isDeleted) { + deleted[wordIndex] |= value; + } else { + deleted[wordIndex] &= ~value; + } + } + + // ======================================================================== + // Helpers + // ======================================================================== + /** + * @return upper boundary (aligned to multiple of BITS_PER_WORD) of bitmap range x belongs to. + */ + private static long alignUp(final long x) { + return (x + (BITS_PER_WORD - 1)) & -BITS_PER_WORD; + } + + /** + * @return lower boundary (aligned to multiple of BITS_PER_WORD) of bitmap range x belongs to. + */ + private static long alignDown(final long x) { + return x & -BITS_PER_WORD; + } +} \ No newline at end of file diff --git a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/NoopProcedureStore.java b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/NoopProcedureStore.java index 9c6176d4bb88..8fbc1473ed7e 100644 --- a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/NoopProcedureStore.java +++ b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/NoopProcedureStore.java @@ -15,7 +15,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.apache.hadoop.hbase.procedure2.store; import java.io.IOException; @@ -64,17 +63,17 @@ public void load(final ProcedureLoader loader) throws IOException { } @Override - public void insert(Procedure proc, Procedure[] subprocs) { + public void insert(Procedure proc, Procedure[] subprocs) { // no-op } @Override - public void insert(Procedure[] proc) { + public void insert(Procedure[] proc) { // no-op } @Override - public void update(Procedure proc) { + public void update(Procedure proc) { // no-op } @@ -84,7 +83,7 @@ public void delete(long procId) { } @Override - public void delete(Procedure proc, long[] subprocs) { + public void delete(Procedure proc, long[] subprocs) { // no-op } diff --git a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/ProcedureStore.java b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/ProcedureStore.java index 72883405d7c1..7a77a20171c8 100644 --- a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/ProcedureStore.java +++ b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/ProcedureStore.java @@ -34,19 +34,37 @@ public interface ProcedureStore { /** * Store listener interface. + *

* The main process should register a listener and respond to the store events. */ public interface ProcedureStoreListener { + /** * triggered when the store sync is completed. */ - void postSync(); + default void postSync() { + } + + /** + * triggered when the store is not able to write out data. the main process should abort. + */ + default void abortProcess() { + } /** - * triggered when the store is not able to write out data. - * the main process should abort. + * Suggest that the upper layer should update the state of some procedures. Ignore this call + * will not effect correctness but performance. + *

+ * For a WAL based ProcedureStore implementation, if all the procedures stored in a WAL file + * have been deleted, or updated later in another WAL file, then we can delete the WAL file. If + * there are old procedures in a WAL file which are never deleted or updated, then we can not + * delete the WAL file and this will cause we hold lots of WAL file and slow down the master + * restarts. So here we introduce this method to tell the upper layer that please update the + * states of these procedures so that we can delete the old WAL file. + * @param procIds the id for the procedures */ - void abortProcess(); + default void forceUpdate(long[] procIds) { + } } /** @@ -67,12 +85,18 @@ public interface ProcedureIterator { boolean hasNext(); /** + * Calling this method does not need to convert the protobuf message to the Procedure class, so + * if it returns true we can call {@link #skipNext()} to skip the procedure without + * deserializing. This could increase the performance. * @return true if the iterator next element is a completed procedure. */ boolean isNextFinished(); /** * Skip the next procedure + *

+ * This method is used to skip the deserializing of the procedure to increase performance, as + * when calling next we need to convert the protobuf message to the Procedure class. */ void skipNext(); @@ -81,6 +105,7 @@ public interface ProcedureIterator { * @throws IOException if there was an error fetching/deserializing the procedure * @return the next procedure in the iteration. */ + @SuppressWarnings("rawtypes") Procedure next() throws IOException; } @@ -173,7 +198,7 @@ public interface ProcedureLoader { * @param proc the procedure to serialize and write to the store. * @param subprocs the newly created child of the proc. */ - void insert(Procedure proc, Procedure[] subprocs); + void insert(Procedure proc, Procedure[] subprocs); /** * Serialize a set of new procedures. @@ -182,14 +207,14 @@ public interface ProcedureLoader { * * @param procs the procedures to serialize and write to the store. */ - void insert(Procedure[] procs); + void insert(Procedure[] procs); /** * The specified procedure was executed, * and the new state should be written to the store. * @param proc the procedure to serialize and write to the store. */ - void update(Procedure proc); + void update(Procedure proc); /** * The specified procId was removed from the executor, @@ -205,7 +230,7 @@ public interface ProcedureLoader { * @param parentProc the parent procedure to serialize and write to the store. * @param subProcIds the IDs of the sub-procedure to remove. */ - void delete(Procedure parentProc, long[] subProcIds); + void delete(Procedure parentProc, long[] subProcIds); /** * The specified procIds were removed from the executor, diff --git a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/ProcedureStoreBase.java b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/ProcedureStoreBase.java index 90da9331f046..b1a8d3d1a2e0 100644 --- a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/ProcedureStoreBase.java +++ b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/ProcedureStoreBase.java @@ -15,12 +15,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.apache.hadoop.hbase.procedure2.store; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicBoolean; - import org.apache.yetus.audience.InterfaceAudience; /** @@ -58,19 +56,15 @@ public boolean unregisterListener(ProcedureStoreListener listener) { return listeners.remove(listener); } - protected void sendPostSyncSignal() { - if (!this.listeners.isEmpty()) { - for (ProcedureStoreListener listener : this.listeners) { - listener.postSync(); - } - } + protected final void sendPostSyncSignal() { + listeners.forEach(ProcedureStoreListener::postSync); + } + + protected final void sendAbortProcessSignal() { + listeners.forEach(ProcedureStoreListener::abortProcess); } - protected void sendAbortProcessSignal() { - if (!this.listeners.isEmpty()) { - for (ProcedureStoreListener listener : this.listeners) { - listener.abortProcess(); - } - } + protected final void sendForceUpdateSignal(long[] procIds) { + listeners.forEach(l -> l.forceUpdate(procIds)); } } diff --git a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/ProcedureStoreTracker.java b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/ProcedureStoreTracker.java index 2dad5ac72ce0..25c94272f088 100644 --- a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/ProcedureStoreTracker.java +++ b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/ProcedureStoreTracker.java @@ -23,9 +23,13 @@ import java.util.Iterator; import java.util.Map; import java.util.TreeMap; - +import java.util.function.BiFunction; +import java.util.stream.LongStream; +import org.apache.hadoop.hbase.procedure2.Procedure; import org.apache.yetus.audience.InterfaceAudience; -import org.apache.yetus.audience.InterfaceStability; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos; /** @@ -35,8 +39,10 @@ * deleted/completed to avoid the deserialization step on restart */ @InterfaceAudience.Private -@InterfaceStability.Evolving public class ProcedureStoreTracker { + + private static final Logger LOG = LoggerFactory.getLogger(ProcedureStoreTracker.class); + // Key is procedure id corresponding to first bit of the bitmap. private final TreeMap map = new TreeMap<>(); @@ -53,383 +59,14 @@ public class ProcedureStoreTracker { * It's set to true only when recovering from old logs. See {@link #isDeleted(long)} docs to * understand it's real use. */ - private boolean partial = false; + boolean partial = false; - private long minUpdatedProcId = Long.MAX_VALUE; - private long maxUpdatedProcId = Long.MIN_VALUE; + private long minModifiedProcId = Long.MAX_VALUE; + private long maxModifiedProcId = Long.MIN_VALUE; public enum DeleteState { YES, NO, MAYBE } - /** - * A bitmap which can grow/merge with other {@link BitSetNode} (if certain conditions are met). - * Boundaries of bitmap are aligned to multiples of {@link BitSetNode#BITS_PER_WORD}. So the - * range of a {@link BitSetNode} is from [x * K, y * K) where x and y are integers, y > x and K - * is BITS_PER_WORD. - */ - public static class BitSetNode { - private final static long WORD_MASK = 0xffffffffffffffffL; - private final static int ADDRESS_BITS_PER_WORD = 6; - private final static int BITS_PER_WORD = 1 << ADDRESS_BITS_PER_WORD; - private final static int MAX_NODE_SIZE = 1 << ADDRESS_BITS_PER_WORD; - - /** - * Mimics {@link ProcedureStoreTracker#partial}. - */ - private final boolean partial; - - /* ---------------------- - * | updated | deleted | meaning - * | 0 | 0 | proc exists, but hasn't been updated since last resetUpdates(). - * | 1 | 0 | proc was updated (but not deleted). - * | 1 | 1 | proc was deleted. - * | 0 | 1 | proc doesn't exist (maybe never created, maybe deleted in past). - /* ---------------------- - */ - - /** - * Set of procedures which have been updated since last {@link #resetUpdates()}. - * Useful to track procedures which have been updated since last WAL write. - */ - private long[] updated; - /** - * Keeps track of procedure ids which belong to this bitmap's range and have been deleted. - * This represents global state since it's not reset on WAL rolls. - */ - private long[] deleted; - /** - * Offset of bitmap i.e. procedure id corresponding to first bit. - */ - private long start; - - public void dump() { - System.out.printf("%06d:%06d min=%d max=%d%n", getStart(), getEnd(), - getActiveMinProcId(), getActiveMaxProcId()); - System.out.println("Update:"); - for (int i = 0; i < updated.length; ++i) { - for (int j = 0; j < BITS_PER_WORD; ++j) { - System.out.print((updated[i] & (1L << j)) != 0 ? "1" : "0"); - } - System.out.println(" " + i); - } - System.out.println(); - System.out.println("Delete:"); - for (int i = 0; i < deleted.length; ++i) { - for (int j = 0; j < BITS_PER_WORD; ++j) { - System.out.print((deleted[i] & (1L << j)) != 0 ? "1" : "0"); - } - System.out.println(" " + i); - } - System.out.println(); - } - - public BitSetNode(final long procId, final boolean partial) { - start = alignDown(procId); - - int count = 1; - updated = new long[count]; - deleted = new long[count]; - for (int i = 0; i < count; ++i) { - updated[i] = 0; - deleted[i] = partial ? 0 : WORD_MASK; - } - - this.partial = partial; - updateState(procId, false); - } - - protected BitSetNode(final long start, final long[] updated, final long[] deleted) { - this.start = start; - this.updated = updated; - this.deleted = deleted; - this.partial = false; - } - - public BitSetNode(ProcedureProtos.ProcedureStoreTracker.TrackerNode data) { - start = data.getStartId(); - int size = data.getUpdatedCount(); - updated = new long[size]; - deleted = new long[size]; - for (int i = 0; i < size; ++i) { - updated[i] = data.getUpdated(i); - deleted[i] = data.getDeleted(i); - } - partial = false; - } - - public BitSetNode(final BitSetNode other, final boolean resetDelete) { - this.start = other.start; - this.partial = other.partial; - this.updated = other.updated.clone(); - if (resetDelete) { - this.deleted = new long[other.deleted.length]; - for (int i = 0; i < this.deleted.length; ++i) { - this.deleted[i] = ~(other.updated[i]); - } - } else { - this.deleted = other.deleted.clone(); - } - } - - public void update(final long procId) { - updateState(procId, false); - } - - public void delete(final long procId) { - updateState(procId, true); - } - - public long getStart() { - return start; - } - - public long getEnd() { - return start + (updated.length << ADDRESS_BITS_PER_WORD) - 1; - } - - public boolean contains(final long procId) { - return start <= procId && procId <= getEnd(); - } - - public DeleteState isDeleted(final long procId) { - int bitmapIndex = getBitmapIndex(procId); - int wordIndex = bitmapIndex >> ADDRESS_BITS_PER_WORD; - if (wordIndex >= deleted.length) { - return DeleteState.MAYBE; - } - return (deleted[wordIndex] & (1L << bitmapIndex)) != 0 ? DeleteState.YES : DeleteState.NO; - } - - private boolean isUpdated(final long procId) { - int bitmapIndex = getBitmapIndex(procId); - int wordIndex = bitmapIndex >> ADDRESS_BITS_PER_WORD; - if (wordIndex >= updated.length) { - return false; - } - return (updated[wordIndex] & (1L << bitmapIndex)) != 0; - } - - public boolean isUpdated() { - // TODO: cache the value - for (int i = 0; i < updated.length; ++i) { - if ((updated[i] | deleted[i]) != WORD_MASK) { - return false; - } - } - return true; - } - - /** - * @return true, if there are no active procedures in this BitSetNode, else false. - */ - public boolean isEmpty() { - // TODO: cache the value - for (int i = 0; i < deleted.length; ++i) { - if (deleted[i] != WORD_MASK) { - return false; - } - } - return true; - } - - public void resetUpdates() { - for (int i = 0; i < updated.length; ++i) { - updated[i] = 0; - } - } - - /** - * Clears the {@link #deleted} bitmaps. - */ - public void undeleteAll() { - for (int i = 0; i < updated.length; ++i) { - deleted[i] = 0; - } - } - - public void unsetPartialFlag() { - for (int i = 0; i < updated.length; ++i) { - for (int j = 0; j < BITS_PER_WORD; ++j) { - if ((updated[i] & (1L << j)) == 0) { - deleted[i] |= (1L << j); - } - } - } - } - - /** - * Convert to - * org.apache.hadoop.hbase.protobuf.generated.ProcedureProtos.ProcedureStoreTracker.TrackerNode - * protobuf. - */ - public ProcedureProtos.ProcedureStoreTracker.TrackerNode convert() { - ProcedureProtos.ProcedureStoreTracker.TrackerNode.Builder builder = - ProcedureProtos.ProcedureStoreTracker.TrackerNode.newBuilder(); - builder.setStartId(start); - for (int i = 0; i < updated.length; ++i) { - builder.addUpdated(updated[i]); - builder.addDeleted(deleted[i]); - } - return builder.build(); - } - - // ======================================================================== - // Grow/Merge Helpers - // ======================================================================== - public boolean canGrow(final long procId) { - return Math.abs(procId - start) < MAX_NODE_SIZE; - } - - public boolean canMerge(final BitSetNode rightNode) { - // Can just compare 'starts' since boundaries are aligned to multiples of BITS_PER_WORD. - assert start < rightNode.start; - return (rightNode.getEnd() - start) < MAX_NODE_SIZE; - } - - public void grow(final long procId) { - int delta, offset; - - if (procId < start) { - // add to head - long newStart = alignDown(procId); - delta = (int)(start - newStart) >> ADDRESS_BITS_PER_WORD; - offset = delta; - start = newStart; - } else { - // Add to tail - long newEnd = alignUp(procId + 1); - delta = (int)(newEnd - getEnd()) >> ADDRESS_BITS_PER_WORD; - offset = 0; - } - - long[] newBitmap; - int oldSize = updated.length; - - newBitmap = new long[oldSize + delta]; - for (int i = 0; i < newBitmap.length; ++i) { - newBitmap[i] = 0; - } - System.arraycopy(updated, 0, newBitmap, offset, oldSize); - updated = newBitmap; - - newBitmap = new long[deleted.length + delta]; - for (int i = 0; i < newBitmap.length; ++i) { - newBitmap[i] = partial ? 0 : WORD_MASK; - } - System.arraycopy(deleted, 0, newBitmap, offset, oldSize); - deleted = newBitmap; - } - - public void merge(final BitSetNode rightNode) { - int delta = (int)(rightNode.getEnd() - getEnd()) >> ADDRESS_BITS_PER_WORD; - - long[] newBitmap; - int oldSize = updated.length; - int newSize = (delta - rightNode.updated.length); - int offset = oldSize + newSize; - - newBitmap = new long[oldSize + delta]; - System.arraycopy(updated, 0, newBitmap, 0, oldSize); - System.arraycopy(rightNode.updated, 0, newBitmap, offset, rightNode.updated.length); - updated = newBitmap; - - newBitmap = new long[oldSize + delta]; - System.arraycopy(deleted, 0, newBitmap, 0, oldSize); - System.arraycopy(rightNode.deleted, 0, newBitmap, offset, rightNode.deleted.length); - deleted = newBitmap; - - for (int i = 0; i < newSize; ++i) { - updated[offset + i] = 0; - deleted[offset + i] = partial ? 0 : WORD_MASK; - } - } - - @Override - public String toString() { - return "BitSetNode(" + getStart() + "-" + getEnd() + ")"; - } - - // ======================================================================== - // Min/Max Helpers - // ======================================================================== - public long getActiveMinProcId() { - long minProcId = start; - for (int i = 0; i < deleted.length; ++i) { - if (deleted[i] == 0) { - return(minProcId); - } - - if (deleted[i] != WORD_MASK) { - for (int j = 0; j < BITS_PER_WORD; ++j) { - if ((deleted[i] & (1L << j)) != 0) { - return minProcId + j; - } - } - } - - minProcId += BITS_PER_WORD; - } - return minProcId; - } - - public long getActiveMaxProcId() { - long maxProcId = getEnd(); - for (int i = deleted.length - 1; i >= 0; --i) { - if (deleted[i] == 0) { - return maxProcId; - } - - if (deleted[i] != WORD_MASK) { - for (int j = BITS_PER_WORD - 1; j >= 0; --j) { - if ((deleted[i] & (1L << j)) == 0) { - return maxProcId - (BITS_PER_WORD - 1 - j); - } - } - } - maxProcId -= BITS_PER_WORD; - } - return maxProcId; - } - - // ======================================================================== - // Bitmap Helpers - // ======================================================================== - private int getBitmapIndex(final long procId) { - return (int)(procId - start); - } - - private void updateState(final long procId, final boolean isDeleted) { - int bitmapIndex = getBitmapIndex(procId); - int wordIndex = bitmapIndex >> ADDRESS_BITS_PER_WORD; - long value = (1L << bitmapIndex); - - updated[wordIndex] |= value; - if (isDeleted) { - deleted[wordIndex] |= value; - } else { - deleted[wordIndex] &= ~value; - } - } - - - // ======================================================================== - // Helpers - // ======================================================================== - /** - * @return upper boundary (aligned to multiple of BITS_PER_WORD) of bitmap range x belongs to. - */ - private static long alignUp(final long x) { - return (x + (BITS_PER_WORD - 1)) & -BITS_PER_WORD; - } - - /** - * @return lower boundary (aligned to multiple of BITS_PER_WORD) of bitmap range x belongs to. - */ - private static long alignDown(final long x) { - return x & -BITS_PER_WORD; - } - } - - public void resetToProto(final ProcedureProtos.ProcedureStoreTracker trackerProtoBuf) { + public void resetToProto(ProcedureProtos.ProcedureStoreTracker trackerProtoBuf) { reset(); for (ProcedureProtos.ProcedureStoreTracker.TrackerNode protoNode: trackerProtoBuf.getNodeList()) { final BitSetNode node = new BitSetNode(protoNode); @@ -440,14 +77,26 @@ public void resetToProto(final ProcedureProtos.ProcedureStoreTracker trackerProt /** * Resets internal state to same as given {@code tracker}. Does deep copy of the bitmap. */ - public void resetTo(final ProcedureStoreTracker tracker) { + public void resetTo(ProcedureStoreTracker tracker) { resetTo(tracker, false); } - public void resetTo(final ProcedureStoreTracker tracker, final boolean resetDelete) { - this.partial = tracker.partial; - this.minUpdatedProcId = tracker.minUpdatedProcId; - this.maxUpdatedProcId = tracker.maxUpdatedProcId; + /** + * Resets internal state to same as given {@code tracker}, and change the deleted flag according + * to the modified flag if {@code resetDelete} is true. Does deep copy of the bitmap. + *

+ * The {@code resetDelete} will be set to true when building cleanup tracker, please see the + * comments in {@link BitSetNode#BitSetNode(BitSetNode, boolean)} to learn how we change the + * deleted flag if {@code resetDelete} is true. + */ + public void resetTo(ProcedureStoreTracker tracker, boolean resetDelete) { + reset(); + // resetDelete will true if we are building the cleanup tracker, as we will reset deleted flags + // for all the unmodified bits to 1, the partial flag is useless so set it to false for not + // confusing the developers when debugging. + this.partial = resetDelete ? false : tracker.partial; + this.minModifiedProcId = tracker.minModifiedProcId; + this.maxModifiedProcId = tracker.maxModifiedProcId; this.keepDeletes = tracker.keepDeletes; for (Map.Entry entry : tracker.map.entrySet()) { map.put(entry.getKey(), new BitSetNode(entry.getValue(), resetDelete)); @@ -458,25 +107,24 @@ public void insert(long procId) { insert(null, procId); } - public void insert(final long[] procIds) { + public void insert(long[] procIds) { for (int i = 0; i < procIds.length; ++i) { insert(procIds[i]); } } - public void insert(final long procId, final long[] subProcIds) { - BitSetNode node = null; - node = update(node, procId); + public void insert(long procId, long[] subProcIds) { + BitSetNode node = update(null, procId); for (int i = 0; i < subProcIds.length; ++i) { node = insert(node, subProcIds[i]); } } - private BitSetNode insert(BitSetNode node, final long procId) { + private BitSetNode insert(BitSetNode node, long procId) { if (node == null || !node.contains(procId)) { node = getOrCreateNode(procId); } - node.update(procId); + node.insertOrUpdate(procId); trackProcIds(procId); return node; } @@ -485,11 +133,11 @@ public void update(long procId) { update(null, procId); } - private BitSetNode update(BitSetNode node, final long procId) { + private BitSetNode update(BitSetNode node, long procId) { node = lookupClosestNode(node, procId); assert node != null : "expected node to update procId=" + procId; assert node.contains(procId) : "expected procId=" + procId + " in the node"; - node.update(procId); + node.insertOrUpdate(procId); trackProcIds(procId); return node; } @@ -506,10 +154,12 @@ public void delete(final long[] procIds) { } } - private BitSetNode delete(BitSetNode node, final long procId) { + private BitSetNode delete(BitSetNode node, long procId) { node = lookupClosestNode(node, procId); - assert node != null : "expected node to delete procId=" + procId; - assert node.contains(procId) : "expected procId=" + procId + " in the node"; + if (node == null || !node.contains(procId)) { + LOG.warn("The BitSetNode for procId={} does not exist, maybe a double deletion?", procId); + return node; + } node.delete(procId); if (!keepDeletes && node.isEmpty()) { // TODO: RESET if (map.size() == 1) @@ -520,41 +170,81 @@ private BitSetNode delete(BitSetNode node, final long procId) { return node; } - @InterfaceAudience.Private - public void setDeleted(final long procId, final boolean isDeleted) { + /** + * Will be called when restarting where we need to rebuild the ProcedureStoreTracker. + */ + public void setMinMaxModifiedProcIds(long min, long max) { + this.minModifiedProcId = min; + this.maxModifiedProcId = max; + } + /** + * This method is used when restarting where we need to rebuild the ProcedureStoreTracker. The + * {@link #delete(long)} method above assume that the {@link BitSetNode} exists, but when restart + * this is not true, as we will read the wal files in reverse order so a delete may come first. + */ + public void setDeleted(long procId, boolean isDeleted) { BitSetNode node = getOrCreateNode(procId); assert node.contains(procId) : "expected procId=" + procId + " in the node=" + node; node.updateState(procId, isDeleted); trackProcIds(procId); } - public void setDeletedIfSet(final long... procId) { + /** + * Set the given bit for the procId to delete if it was modified before. + *

+ * This method is used to test whether a procedure wal file can be safely deleted, as if all the + * procedures in the given procedure wal file has been modified in the new procedure wal files, + * then we can delete it. + */ + public void setDeletedIfModified(long... procId) { BitSetNode node = null; for (int i = 0; i < procId.length; ++i) { node = lookupClosestNode(node, procId[i]); - if (node != null && node.isUpdated(procId[i])) { + if (node != null && node.isModified(procId[i])) { node.delete(procId[i]); } } } - public void setDeletedIfSet(final ProcedureStoreTracker tracker) { + private void setDeleteIf(ProcedureStoreTracker tracker, + BiFunction func) { BitSetNode trackerNode = null; - for (BitSetNode node: map.values()) { - final long minProcId = node.getStart(); - final long maxProcId = node.getEnd(); + for (BitSetNode node : map.values()) { + long minProcId = node.getStart(); + long maxProcId = node.getEnd(); for (long procId = minProcId; procId <= maxProcId; ++procId) { - if (!node.isUpdated(procId)) continue; + if (!node.isModified(procId)) { + continue; + } trackerNode = tracker.lookupClosestNode(trackerNode, procId); - if (trackerNode == null || !trackerNode.contains(procId) || trackerNode.isUpdated(procId)) { - // the procedure was removed or updated + if (func.apply(trackerNode, procId)) { node.delete(procId); } } } } + /** + * For the global tracker, we will use this method to build the holdingCleanupTracker, as the + * modified flags will be cleared after rolling so we only need to test the deleted flags. + * @see #setDeletedIfModifiedInBoth(ProcedureStoreTracker) + */ + public void setDeletedIfDeletedByThem(ProcedureStoreTracker tracker) { + setDeleteIf(tracker, (node, procId) -> node == null || !node.contains(procId) || + node.isDeleted(procId) == DeleteState.YES); + } + + /** + * Similar with {@link #setDeletedIfModified(long...)}, but here the {@code procId} are given by + * the {@code tracker}. If a procedure is modified by us, and also by the given {@code tracker}, + * then we mark it as deleted. + * @see #setDeletedIfModified(long...) + */ + public void setDeletedIfModifiedInBoth(ProcedureStoreTracker tracker) { + setDeleteIf(tracker, (node, procId) -> node != null && node.isModified(procId)); + } + /** * lookup the node containing the specified procId. * @param node cached node to check before doing a lookup @@ -568,28 +258,29 @@ private BitSetNode lookupClosestNode(final BitSetNode node, final long procId) { } private void trackProcIds(long procId) { - minUpdatedProcId = Math.min(minUpdatedProcId, procId); - maxUpdatedProcId = Math.max(maxUpdatedProcId, procId); + minModifiedProcId = Math.min(minModifiedProcId, procId); + maxModifiedProcId = Math.max(maxModifiedProcId, procId); } - public long getUpdatedMinProcId() { - return minUpdatedProcId; + public long getModifiedMinProcId() { + return minModifiedProcId; } - public long getUpdatedMaxProcId() { - return maxUpdatedProcId; + public long getModifiedMaxProcId() { + return maxModifiedProcId; } public void reset() { this.keepDeletes = false; this.partial = false; this.map.clear(); - resetUpdates(); + resetModified(); } - public boolean isUpdated(long procId) { + public boolean isModified(long procId) { final Map.Entry entry = map.floorEntry(procId); - return entry != null && entry.getValue().contains(procId) && entry.getValue().isUpdated(procId); + return entry != null && entry.getValue().contains(procId) && + entry.getValue().isModified(procId); } /** @@ -604,15 +295,14 @@ public DeleteState isDeleted(long procId) { if (entry != null && entry.getValue().contains(procId)) { BitSetNode node = entry.getValue(); DeleteState state = node.isDeleted(procId); - return partial && !node.isUpdated(procId) ? DeleteState.MAYBE : state; + return partial && !node.isModified(procId) ? DeleteState.MAYBE : state; } return partial ? DeleteState.MAYBE : DeleteState.YES; } public long getActiveMinProcId() { - // TODO: Cache? Map.Entry entry = map.firstEntry(); - return entry == null ? 0 : entry.getValue().getActiveMinProcId(); + return entry == null ? Procedure.NO_PROC_ID : entry.getValue().getActiveMinProcId(); } public void setKeepDeletes(boolean keepDeletes) { @@ -656,11 +346,12 @@ public boolean isEmpty() { } /** - * @return true if any procedure was updated since last call to {@link #resetUpdates()}. + * @return true if all procedure was modified or deleted since last call to + * {@link #resetModified()}. */ - public boolean isUpdated() { + public boolean isAllModified() { for (Map.Entry entry : map.entrySet()) { - if (!entry.getValue().isUpdated()) { + if (!entry.getValue().isAllModified()) { return false; } } @@ -668,24 +359,28 @@ public boolean isUpdated() { } /** - * Clears the list of updated procedure ids. This doesn't affect global list of active - * procedure ids. + * Will be used when there are too many proc wal files. We will rewrite the states of the active + * procedures in the oldest proc wal file so that we can delete it. + * @return all the active procedure ids in this tracker. */ - public void resetUpdates() { - for (Map.Entry entry : map.entrySet()) { - entry.getValue().resetUpdates(); - } - minUpdatedProcId = Long.MAX_VALUE; - maxUpdatedProcId = Long.MIN_VALUE; + public long[] getAllActiveProcIds() { + return map.values().stream().map(BitSetNode::getActiveProcIds).filter(p -> p.length > 0) + .flatMapToLong(LongStream::of).toArray(); } - public void undeleteAll() { + /** + * Clears the list of updated procedure ids. This doesn't affect global list of active + * procedure ids. + */ + public void resetModified() { for (Map.Entry entry : map.entrySet()) { - entry.getValue().undeleteAll(); + entry.getValue().resetModified(); } + minModifiedProcId = Long.MAX_VALUE; + maxModifiedProcId = Long.MIN_VALUE; } - private BitSetNode getOrCreateNode(final long procId) { + private BitSetNode getOrCreateNode(long procId) { // If procId can fit in left node (directly or by growing it) BitSetNode leftNode = null; boolean leftCanGrow = false; @@ -760,7 +455,7 @@ private BitSetNode mergeNodes(BitSetNode leftNode, BitSetNode rightNode) { public void dump() { System.out.println("map " + map.size()); - System.out.println("isUpdated " + isUpdated()); + System.out.println("isAllModified " + isAllModified()); System.out.println("isEmpty " + isEmpty()); for (Map.Entry entry : map.entrySet()) { entry.getValue().dump(); diff --git a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/CorruptedWALProcedureStoreException.java b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/CorruptedWALProcedureStoreException.java index dd34896ebb34..ba4480fca7e6 100644 --- a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/CorruptedWALProcedureStoreException.java +++ b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/CorruptedWALProcedureStoreException.java @@ -15,19 +15,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.apache.hadoop.hbase.procedure2.store.wal; import org.apache.hadoop.hbase.HBaseIOException; import org.apache.yetus.audience.InterfaceAudience; -import org.apache.yetus.audience.InterfaceStability; /** * Thrown when a procedure WAL is corrupted */ @InterfaceAudience.Private -@InterfaceStability.Stable public class CorruptedWALProcedureStoreException extends HBaseIOException { + + private static final long serialVersionUID = -3407300445435898074L; + /** default constructor */ public CorruptedWALProcedureStoreException() { super(); diff --git a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/ProcedureWALFile.java b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/ProcedureWALFile.java index 6226350a472b..16767446e17b 100644 --- a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/ProcedureWALFile.java +++ b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/ProcedureWALFile.java @@ -15,20 +15,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.apache.hadoop.hbase.procedure2.store.wal; import java.io.IOException; - import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hbase.procedure2.store.ProcedureStoreTracker; import org.apache.yetus.audience.InterfaceAudience; -import org.apache.yetus.audience.InterfaceStability; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.hadoop.hbase.procedure2.store.ProcedureStoreTracker; + import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos; import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos.ProcedureWALHeader; import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos.ProcedureWALTrailer; @@ -37,7 +35,6 @@ * Describes a WAL File */ @InterfaceAudience.Private -@InterfaceStability.Evolving public class ProcedureWALFile implements Comparable { private static final Logger LOG = LoggerFactory.getLogger(ProcedureWALFile.class); diff --git a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/ProcedureWALFormat.java b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/ProcedureWALFormat.java index ac3a52941e95..179c7404d95f 100644 --- a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/ProcedureWALFormat.java +++ b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/ProcedureWALFormat.java @@ -18,25 +18,22 @@ package org.apache.hadoop.hbase.procedure2.store.wal; -import org.apache.hbase.thirdparty.com.google.protobuf.InvalidProtocolBufferException; - import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Iterator; - import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FSDataOutputStream; -import org.apache.yetus.audience.InterfaceAudience; -import org.apache.yetus.audience.InterfaceStability; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.apache.hadoop.hbase.io.util.StreamUtils; import org.apache.hadoop.hbase.procedure2.Procedure; import org.apache.hadoop.hbase.procedure2.ProcedureUtil; import org.apache.hadoop.hbase.procedure2.store.ProcedureStore.ProcedureLoader; import org.apache.hadoop.hbase.procedure2.store.ProcedureStoreTracker; import org.apache.hadoop.hbase.procedure2.util.ByteSlot; +import org.apache.yetus.audience.InterfaceAudience; + +import org.apache.hbase.thirdparty.com.google.protobuf.InvalidProtocolBufferException; + import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos.ProcedureWALEntry; import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos.ProcedureWALHeader; import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos.ProcedureWALTrailer; @@ -45,9 +42,7 @@ * Helper class that contains the WAL serialization utils. */ @InterfaceAudience.Private -@InterfaceStability.Evolving public final class ProcedureWALFormat { - private static final Logger LOG = LoggerFactory.getLogger(ProcedureWALFormat.class); static final byte LOG_TYPE_STREAM = 0; static final byte LOG_TYPE_COMPACTED = 1; @@ -60,6 +55,9 @@ public final class ProcedureWALFormat { @InterfaceAudience.Private public static class InvalidWALDataException extends IOException { + + private static final long serialVersionUID = 5471733223070202196L; + public InvalidWALDataException(String s) { super(s); } @@ -75,9 +73,20 @@ interface Loader extends ProcedureLoader { private ProcedureWALFormat() {} - public static void load(final Iterator logs, - final ProcedureStoreTracker tracker, final Loader loader) throws IOException { - final ProcedureWALFormatReader reader = new ProcedureWALFormatReader(tracker, loader); + /** + * Load all the procedures in these ProcedureWALFiles, and rebuild the given {@code tracker} if + * needed, i.e, the {@code tracker} is a partial one. + *

+ * The method in the give {@code loader} will be called at the end after we load all the + * procedures and construct the hierarchy. + *

+ * And we will call the {@link ProcedureStoreTracker#resetModified()} method for the given + * {@code tracker} before returning, as it will be used to track the next proc wal file's modified + * procedures. + */ + public static void load(Iterator logs, ProcedureStoreTracker tracker, + Loader loader) throws IOException { + ProcedureWALFormatReader reader = new ProcedureWALFormatReader(tracker, loader); tracker.setKeepDeletes(true); try { // Ignore the last log which is current active log. @@ -93,8 +102,10 @@ public static void load(final Iterator logs, reader.finish(); // The tracker is now updated with all the procedures read from the logs - tracker.setPartialFlag(false); - tracker.resetUpdates(); + if (tracker.isPartial()) { + tracker.setPartialFlag(false); + } + tracker.resetModified(); } finally { tracker.setKeepDeletes(false); } @@ -205,7 +216,7 @@ public static ProcedureWALEntry readEntry(InputStream stream) throws IOException } public static void writeEntry(ByteSlot slot, ProcedureWALEntry.Type type, - Procedure proc, Procedure[] subprocs) throws IOException { + Procedure proc, Procedure[] subprocs) throws IOException { final ProcedureWALEntry.Builder builder = ProcedureWALEntry.newBuilder(); builder.setType(type); builder.addProcedure(ProcedureUtil.convertToProtoProcedure(proc)); @@ -217,17 +228,17 @@ public static void writeEntry(ByteSlot slot, ProcedureWALEntry.Type type, builder.build().writeDelimitedTo(slot); } - public static void writeInsert(ByteSlot slot, Procedure proc) + public static void writeInsert(ByteSlot slot, Procedure proc) throws IOException { writeEntry(slot, ProcedureWALEntry.Type.PROCEDURE_WAL_INIT, proc, null); } - public static void writeInsert(ByteSlot slot, Procedure proc, Procedure[] subprocs) + public static void writeInsert(ByteSlot slot, Procedure proc, Procedure[] subprocs) throws IOException { writeEntry(slot, ProcedureWALEntry.Type.PROCEDURE_WAL_INSERT, proc, subprocs); } - public static void writeUpdate(ByteSlot slot, Procedure proc) + public static void writeUpdate(ByteSlot slot, Procedure proc) throws IOException { writeEntry(slot, ProcedureWALEntry.Type.PROCEDURE_WAL_UPDATE, proc, null); } @@ -240,7 +251,7 @@ public static void writeDelete(ByteSlot slot, long procId) builder.build().writeDelimitedTo(slot); } - public static void writeDelete(ByteSlot slot, Procedure proc, long[] subprocs) + public static void writeDelete(ByteSlot slot, Procedure proc, long[] subprocs) throws IOException { final ProcedureWALEntry.Builder builder = ProcedureWALEntry.newBuilder(); builder.setType(ProcedureWALEntry.Type.PROCEDURE_WAL_DELETE); diff --git a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/ProcedureWALFormatReader.java b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/ProcedureWALFormatReader.java index 4ab70f18e100..1b19abbeb926 100644 --- a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/ProcedureWALFormatReader.java +++ b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/ProcedureWALFormatReader.java @@ -15,91 +15,40 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.apache.hadoop.hbase.procedure2.store.wal; -import org.apache.hbase.thirdparty.com.google.protobuf.InvalidProtocolBufferException; - import java.io.IOException; - import org.apache.hadoop.fs.FSDataInputStream; +import org.apache.hadoop.hbase.procedure2.store.ProcedureStoreTracker; import org.apache.yetus.audience.InterfaceAudience; -import org.apache.yetus.audience.InterfaceStability; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.hadoop.hbase.procedure2.Procedure; -import org.apache.hadoop.hbase.procedure2.ProcedureUtil; -import org.apache.hadoop.hbase.procedure2.store.ProcedureStore.ProcedureIterator; -import org.apache.hadoop.hbase.procedure2.store.ProcedureStoreTracker; + +import org.apache.hbase.thirdparty.com.google.protobuf.InvalidProtocolBufferException; + import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos; import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos.ProcedureWALEntry; /** - * Helper class that loads the procedures stored in a WAL + * Helper class that loads the procedures stored in a WAL. */ @InterfaceAudience.Private -@InterfaceStability.Evolving public class ProcedureWALFormatReader { private static final Logger LOG = LoggerFactory.getLogger(ProcedureWALFormatReader.class); - // ============================================================================================== - // We read the WALs in reverse order from the newest to the oldest. - // We have different entry types: - // - INIT: Procedure submitted by the user (also known as 'root procedure') - // - INSERT: Children added to the procedure :[, ...] - // - UPDATE: The specified procedure was updated - // - DELETE: The procedure was removed (finished/rolledback and result TTL expired) - // - // In the WAL we can find multiple times the same procedure as UPDATE or INSERT. - // We read the WAL from top to bottom, so every time we find an entry of the - // same procedure, that will be the "latest" update (Caveat: with multiple threads writing - // the store, this assumption does not hold). - // - // We keep two in-memory maps: - // - localProcedureMap: is the map containing the entries in the WAL we are processing - // - procedureMap: is the map containing all the procedures we found up to the WAL in process. - // localProcedureMap is merged with the procedureMap once we reach the WAL EOF. - // - // Since we are reading the WALs in reverse order (newest to oldest), - // if we find an entry related to a procedure we already have in 'procedureMap' we can discard it. - // - // The WAL is append-only so the last procedure in the WAL is the one that - // was in execution at the time we crashed/closed the server. - // Given that, the procedure replay order can be inferred by the WAL order. - // - // Example: - // WAL-2: [A, B, A, C, D] - // WAL-1: [F, G, A, F, B] - // Replay-Order: [D, C, A, B, F, G] - // - // The "localProcedureMap" keeps a "replayOrder" list. Every time we add the - // record to the map that record is moved to the head of the "replayOrder" list. - // Using the example above: - // WAL-2 localProcedureMap.replayOrder is [D, C, A, B] - // WAL-1 localProcedureMap.replayOrder is [F, G] - // - // Each time we reach the WAL-EOF, the "replayOrder" list is merged/appended in 'procedureMap' - // so using the example above we end up with: [D, C, A, B] + [F, G] as replay order. - // - // Fast Start: INIT/INSERT record and StackIDs - // --------------------------------------------- - // We have two special records, INIT and INSERT, that track the first time - // the procedure was added to the WAL. We can use this information to be able - // to start procedures before reaching the end of the WAL, or before reading all WALs. - // But in some cases, the WAL with that record can be already gone. - // As an alternative, we can use the stackIds on each procedure, - // to identify when a procedure is ready to start. - // If there are gaps in the sum of the stackIds we need to read more WALs. - // - // Example (all procs child of A): - // WAL-2: [A, B] A stackIds = [0, 4], B stackIds = [1, 5] - // WAL-1: [A, B, C, D] - // - // In the case above we need to read one more WAL to be able to consider - // the root procedure A and all children as ready. - // ============================================================================================== - private final WalProcedureMap localProcedureMap = new WalProcedureMap(1024); - private final WalProcedureMap procedureMap = new WalProcedureMap(1024); + /** + * We will use the localProcedureMap to track the active procedures for the current proc wal file, + * and when we finished reading one proc wal file, we will merge he localProcedureMap to the + * procedureMap, which tracks the global active procedures. + *

+ * See the comments of {@link WALProcedureMap} for more details. + *

+ * After reading all the proc wal files, we will use the procedures in the procedureMap to build a + * {@link WALProcedureTree}, and then give the result to the upper layer. See the comments of + * {@link WALProcedureTree} and the code in {@link #finish()} for more details. + */ + private final WALProcedureMap localProcedureMap = new WALProcedureMap(); + private final WALProcedureMap procedureMap = new WALProcedureMap(); private final ProcedureWALFormat.Loader loader; @@ -111,33 +60,31 @@ public class ProcedureWALFormatReader { * to rebuild the tracker. */ private final ProcedureStoreTracker tracker; - // TODO: private final boolean hasFastStartSupport; /** - * If tracker for a log file is partial (see {@link ProcedureStoreTracker#partial}), we - * re-build the list of procedures updated in that WAL because we need it for log cleaning - * purposes. If all procedures updated in a WAL are found to be obsolete, it can be safely deleted. - * (see {@link WALProcedureStore#removeInactiveLogs()}). - * However, we don't need deleted part of a WAL's tracker for this purpose, so we don't bother - * re-building it. + * If tracker for a log file is partial (see {@link ProcedureStoreTracker#partial}), we re-build + * the list of procedures modified in that WAL because we need it for log cleaning purposes. If + * all procedures modified in a WAL are found to be obsolete, it can be safely deleted. (see + * {@link WALProcedureStore#removeInactiveLogs()}). + *

+ * Notice that, the deleted part for this tracker will not be global valid as we can only count + * the deletes in the current file, but it is not big problem as finally, the above tracker will + * have the global state of deleted, and it will also be used to build the cleanup tracker. */ private ProcedureStoreTracker localTracker; - // private long compactionLogId; private long maxProcId = 0; public ProcedureWALFormatReader(final ProcedureStoreTracker tracker, ProcedureWALFormat.Loader loader) { this.tracker = tracker; this.loader = loader; - // we support fast-start only if we have a clean shutdown. - // this.hasFastStartSupport = !tracker.isEmpty(); } - public void read(final ProcedureWALFile log) throws IOException { - localTracker = log.getTracker().isPartial() ? log.getTracker() : null; - if (localTracker != null) { - LOG.info("Rebuilding tracker for " + log); + public void read(ProcedureWALFile log) throws IOException { + localTracker = log.getTracker(); + if (localTracker.isPartial()) { + LOG.info("Rebuilding tracker for {}", log); } long count = 0; @@ -147,7 +94,7 @@ public void read(final ProcedureWALFile log) throws IOException { while (hasMore) { ProcedureWALEntry entry = ProcedureWALFormat.readEntry(stream); if (entry == null) { - LOG.warn("Nothing left to decode. Exiting with missing EOF, log=" + log); + LOG.warn("Nothing left to decode. Exiting with missing EOF, log={}", log); break; } count++; @@ -178,61 +125,58 @@ public void read(final ProcedureWALFile log) throws IOException { loader.markCorruptedWAL(log, e); } - if (localTracker != null) { - localTracker.setPartialFlag(false); - } if (!localProcedureMap.isEmpty()) { - log.setProcIds(localProcedureMap.getMinProcId(), localProcedureMap.getMaxProcId()); - procedureMap.mergeTail(localProcedureMap); - - //if (hasFastStartSupport) { - // TODO: Some procedure may be already runnables (see readInitEntry()) - // (we can also check the "update map" in the log trackers) - // -------------------------------------------------- - //EntryIterator iter = procedureMap.fetchReady(); - //if (iter != null) loader.load(iter); - // -------------------------------------------------- - //} + log.setProcIds(localProcedureMap.getMinModifiedProcId(), + localProcedureMap.getMaxModifiedProcId()); + if (localTracker.isPartial()) { + localTracker.setMinMaxModifiedProcIds(localProcedureMap.getMinModifiedProcId(), + localProcedureMap.getMaxModifiedProcId()); + } + procedureMap.merge(localProcedureMap); } + // Do not reset the partial flag for local tracker, as here the local tracker only know the + // procedures which are modified in this file. } public void finish() throws IOException { // notify the loader about the max proc ID loader.setMaxProcId(maxProcId); - // fetch the procedure ready to run. - ProcedureIterator procIter = procedureMap.fetchReady(); - if (procIter != null) loader.load(procIter); + // build the procedure execution tree. When building we will verify that whether a procedure is + // valid. + WALProcedureTree tree = WALProcedureTree.build(procedureMap.getProcedures()); + loader.load(tree.getValidProcs()); + loader.handleCorrupted(tree.getCorruptedProcs()); + } - // remaining procedures have missing link or dependencies - // consider them as corrupted, manual fix is probably required. - procIter = procedureMap.fetchAll(); - if (procIter != null) loader.handleCorrupted(procIter); + private void setDeletedIfPartial(ProcedureStoreTracker tracker, long procId) { + if (tracker.isPartial()) { + tracker.setDeleted(procId, true); + } } - private void loadProcedure(final ProcedureWALEntry entry, final ProcedureProtos.Procedure proc) { + private void insertIfPartial(ProcedureStoreTracker tracker, ProcedureProtos.Procedure proc) { + if (tracker.isPartial()) { + tracker.insert(proc.getProcId()); + } + } + + private void loadProcedure(ProcedureWALEntry entry, ProcedureProtos.Procedure proc) { maxProcId = Math.max(maxProcId, proc.getProcId()); if (isRequired(proc.getProcId())) { - if (LOG.isTraceEnabled()) { - LOG.trace("Read " + entry.getType() + " entry " + proc.getProcId()); - } + LOG.trace("Read {} entry {}", entry.getType(), proc.getProcId()); localProcedureMap.add(proc); - if (tracker.isPartial()) { - tracker.insert(proc.getProcId()); - } - } - if (localTracker != null) { - localTracker.insert(proc.getProcId()); + insertIfPartial(tracker, proc); } + insertIfPartial(localTracker, proc); } - private void readInitEntry(final ProcedureWALEntry entry) - throws IOException { + private void readInitEntry(ProcedureWALEntry entry) { assert entry.getProcedureCount() == 1 : "Expected only one procedure"; loadProcedure(entry, entry.getProcedure(0)); } - private void readInsertEntry(final ProcedureWALEntry entry) throws IOException { + private void readInsertEntry(ProcedureWALEntry entry) { assert entry.getProcedureCount() >= 1 : "Expected one or more procedures"; loadProcedure(entry, entry.getProcedure(0)); for (int i = 1; i < entry.getProcedureCount(); ++i) { @@ -240,12 +184,12 @@ private void readInsertEntry(final ProcedureWALEntry entry) throws IOException { } } - private void readUpdateEntry(final ProcedureWALEntry entry) throws IOException { + private void readUpdateEntry(ProcedureWALEntry entry) { assert entry.getProcedureCount() == 1 : "Expected only one procedure"; loadProcedure(entry, entry.getProcedure(0)); } - private void readDeleteEntry(final ProcedureWALEntry entry) throws IOException { + private void readDeleteEntry(ProcedureWALEntry entry) { assert entry.hasProcId() : "expected ProcID"; if (entry.getChildIdCount() > 0) { @@ -267,598 +211,19 @@ private void readDeleteEntry(final ProcedureWALEntry entry) throws IOException { } private void deleteEntry(final long procId) { - if (LOG.isTraceEnabled()) { - LOG.trace("delete entry " + procId); - } + LOG.trace("delete entry {}", procId); maxProcId = Math.max(maxProcId, procId); localProcedureMap.remove(procId); assert !procedureMap.contains(procId); - if (tracker.isPartial()) { - tracker.setDeleted(procId, true); - } - if (localTracker != null) { - // In case there is only delete entry for this procedure in current log. - localTracker.setDeleted(procId, true); - } + setDeletedIfPartial(tracker, procId); + setDeletedIfPartial(localTracker, procId); } - private boolean isDeleted(final long procId) { + private boolean isDeleted(long procId) { return tracker.isDeleted(procId) == ProcedureStoreTracker.DeleteState.YES; } - private boolean isRequired(final long procId) { + private boolean isRequired(long procId) { return !isDeleted(procId) && !procedureMap.contains(procId); } - - // ========================================================================== - // We keep an in-memory map of the procedures sorted by replay order. - // (see the details in the beginning of the file) - // _______________________________________________ - // procedureMap = | A | | E | | C | | | | | G | | | - // D B - // replayOrderHead = C <-> B <-> E <-> D <-> A <-> G - // - // We also have a lazy grouping by "root procedure", and a list of - // unlinked procedures. If after reading all the WALs we have unlinked - // procedures it means that we had a missing WAL or a corruption. - // rootHead = A <-> D <-> G - // B E - // C - // unlinkFromLinkList = None - // ========================================================================== - private static class Entry { - // For bucketed linked lists in hash-table. - protected Entry hashNext; - // child head - protected Entry childHead; - // double-link for rootHead or childHead - protected Entry linkNext; - protected Entry linkPrev; - // replay double-linked-list - protected Entry replayNext; - protected Entry replayPrev; - // procedure-infos - protected Procedure procedure; - protected ProcedureProtos.Procedure proto; - protected boolean ready = false; - - public Entry(Entry hashNext) { - this.hashNext = hashNext; - } - - public long getProcId() { - return proto.getProcId(); - } - - public long getParentId() { - return proto.getParentId(); - } - - public boolean hasParent() { - return proto.hasParentId(); - } - - public boolean isReady() { - return ready; - } - - public boolean isFinished() { - if (!hasParent()) { - // we only consider 'root' procedures. because for the user 'finished' - // means when everything up to the 'root' is finished. - switch (proto.getState()) { - case ROLLEDBACK: - case SUCCESS: - return true; - default: - break; - } - } - return false; - } - - public Procedure convert() throws IOException { - if (procedure == null) { - procedure = ProcedureUtil.convertToProcedure(proto); - } - return procedure; - } - - @Override - public String toString() { - final StringBuilder sb = new StringBuilder(); - sb.append("Entry("); - sb.append(getProcId()); - sb.append(", parentId="); - sb.append(getParentId()); - sb.append(", class="); - sb.append(proto.getClassName()); - sb.append(")"); - return sb.toString(); - } - } - - private static class EntryIterator implements ProcedureIterator { - private final Entry replayHead; - private Entry current; - - public EntryIterator(Entry replayHead) { - this.replayHead = replayHead; - this.current = replayHead; - } - - @Override - public void reset() { - this.current = replayHead; - } - - @Override - public boolean hasNext() { - return current != null; - } - - @Override - public boolean isNextFinished() { - return current != null && current.isFinished(); - } - - @Override - public void skipNext() { - current = current.replayNext; - } - - @Override - public Procedure next() throws IOException { - try { - return current.convert(); - } finally { - current = current.replayNext; - } - } - } - - private static class WalProcedureMap { - // procedure hash table - private Entry[] procedureMap; - - // replay-order double-linked-list - private Entry replayOrderHead; - private Entry replayOrderTail; - - // root linked-list - private Entry rootHead; - - // pending unlinked children (root not present yet) - private Entry childUnlinkedHead; - - // Track ProcId range - private long minProcId = Long.MAX_VALUE; - private long maxProcId = Long.MIN_VALUE; - - public WalProcedureMap(int size) { - procedureMap = new Entry[size]; - replayOrderHead = null; - replayOrderTail = null; - rootHead = null; - childUnlinkedHead = null; - } - - public void add(ProcedureProtos.Procedure procProto) { - trackProcIds(procProto.getProcId()); - Entry entry = addToMap(procProto.getProcId(), procProto.hasParentId()); - boolean newEntry = entry.proto == null; - // We have seen procedure WALs where the entries are out of order; see HBASE-18152. - // To compensate, only replace the Entry procedure if for sure this new procedure - // is indeed an entry that came later. TODO: Fix the writing of procedure info so - // it does not violate basic expectation, that WALs contain procedure changes going - // from start to finish in sequence. - if (newEntry || isIncreasing(entry.proto, procProto)) { - entry.proto = procProto; - } - addToReplayList(entry); - if(newEntry) { - if (procProto.hasParentId()) { - childUnlinkedHead = addToLinkList(entry, childUnlinkedHead); - } else { - rootHead = addToLinkList(entry, rootHead); - } - } - } - - /** - * @return True if this new procedure is 'richer' than the current one else - * false and we log this incidence where it appears that the WAL has older entries - * appended after newer ones. See HBASE-18152. - */ - private static boolean isIncreasing(ProcedureProtos.Procedure current, - ProcedureProtos.Procedure candidate) { - // Check that the procedures we see are 'increasing'. We used to compare - // procedure id first and then update time but it can legitimately go backwards if the - // procedure is failed or rolled back so that was unreliable. Was going to compare - // state but lets see if comparing update time enough (unfortunately this issue only - // seen under load...) - boolean increasing = current.getLastUpdate() <= candidate.getLastUpdate(); - if (!increasing) { - LOG.warn("NOT INCREASING! current=" + current + ", candidate=" + candidate); - } - return increasing; - } - - public boolean remove(long procId) { - trackProcIds(procId); - Entry entry = removeFromMap(procId); - if (entry != null) { - unlinkFromReplayList(entry); - unlinkFromLinkList(entry); - return true; - } - return false; - } - - private void trackProcIds(long procId) { - minProcId = Math.min(minProcId, procId); - maxProcId = Math.max(maxProcId, procId); - } - - public long getMinProcId() { - return minProcId; - } - - public long getMaxProcId() { - return maxProcId; - } - - public boolean contains(long procId) { - return getProcedure(procId) != null; - } - - public boolean isEmpty() { - return replayOrderHead == null; - } - - public void clear() { - for (int i = 0; i < procedureMap.length; ++i) { - procedureMap[i] = null; - } - replayOrderHead = null; - replayOrderTail = null; - rootHead = null; - childUnlinkedHead = null; - minProcId = Long.MAX_VALUE; - maxProcId = Long.MIN_VALUE; - } - - /* - * Merges two WalProcedureMap, - * the target is the "global" map, the source is the "local" map. - * - The entries in the hashtables are guaranteed to be unique. - * On replay we don't load procedures that already exist in the "global" - * map (the one we are merging the "local" in to). - * - The replayOrderList of the "local" nao will be appended to the "global" - * map replay list. - * - The "local" map will be cleared at the end of the operation. - */ - public void mergeTail(WalProcedureMap other) { - for (Entry p = other.replayOrderHead; p != null; p = p.replayNext) { - int slotIndex = getMapSlot(p.getProcId()); - p.hashNext = procedureMap[slotIndex]; - procedureMap[slotIndex] = p; - } - - if (replayOrderHead == null) { - replayOrderHead = other.replayOrderHead; - replayOrderTail = other.replayOrderTail; - rootHead = other.rootHead; - childUnlinkedHead = other.childUnlinkedHead; - } else { - // append replay list - assert replayOrderTail.replayNext == null; - assert other.replayOrderHead.replayPrev == null; - replayOrderTail.replayNext = other.replayOrderHead; - other.replayOrderHead.replayPrev = replayOrderTail; - replayOrderTail = other.replayOrderTail; - - // merge rootHead - if (rootHead == null) { - rootHead = other.rootHead; - } else if (other.rootHead != null) { - Entry otherTail = findLinkListTail(other.rootHead); - otherTail.linkNext = rootHead; - rootHead.linkPrev = otherTail; - rootHead = other.rootHead; - } - - // merge childUnlinkedHead - if (childUnlinkedHead == null) { - childUnlinkedHead = other.childUnlinkedHead; - } else if (other.childUnlinkedHead != null) { - Entry otherTail = findLinkListTail(other.childUnlinkedHead); - otherTail.linkNext = childUnlinkedHead; - childUnlinkedHead.linkPrev = otherTail; - childUnlinkedHead = other.childUnlinkedHead; - } - } - maxProcId = Math.max(maxProcId, other.maxProcId); - minProcId = Math.max(minProcId, other.minProcId); - - other.clear(); - } - - /* - * Returns an EntryIterator with the list of procedures ready - * to be added to the executor. - * A Procedure is ready if its children and parent are ready. - */ - public EntryIterator fetchReady() { - buildGraph(); - - Entry readyHead = null; - Entry readyTail = null; - Entry p = replayOrderHead; - while (p != null) { - Entry next = p.replayNext; - if (p.isReady()) { - unlinkFromReplayList(p); - if (readyTail != null) { - readyTail.replayNext = p; - p.replayPrev = readyTail; - } else { - p.replayPrev = null; - readyHead = p; - } - readyTail = p; - p.replayNext = null; - } - p = next; - } - // we need the hash-table lookups for parents, so this must be done - // out of the loop where we check isReadyToRun() - for (p = readyHead; p != null; p = p.replayNext) { - removeFromMap(p.getProcId()); - unlinkFromLinkList(p); - } - return readyHead != null ? new EntryIterator(readyHead) : null; - } - - /* - * Drain this map and return all procedures in it. - */ - public EntryIterator fetchAll() { - Entry head = replayOrderHead; - for (Entry p = head; p != null; p = p.replayNext) { - removeFromMap(p.getProcId()); - } - for (int i = 0; i < procedureMap.length; ++i) { - assert procedureMap[i] == null : "map not empty i=" + i; - } - replayOrderHead = null; - replayOrderTail = null; - childUnlinkedHead = null; - rootHead = null; - return head != null ? new EntryIterator(head) : null; - } - - private void buildGraph() { - Entry p = childUnlinkedHead; - while (p != null) { - Entry next = p.linkNext; - Entry rootProc = getRootProcedure(p); - if (rootProc != null) { - rootProc.childHead = addToLinkList(p, rootProc.childHead); - } - p = next; - } - - for (p = rootHead; p != null; p = p.linkNext) { - checkReadyToRun(p); - } - } - - private Entry getRootProcedure(Entry entry) { - while (entry != null && entry.hasParent()) { - entry = getProcedure(entry.getParentId()); - } - return entry; - } - - /* - * (see the comprehensive explanation in the beginning of the file) - * A Procedure is ready when parent and children are ready. - * "ready" means that we all the information that we need in-memory. - * - * Example-1: - * We have two WALs, we start reading from the newest (wal-2) - * wal-2 | C B | - * wal-1 | A B C | - * - * If C and B don't depend on A (A is not the parent), we can start them - * before reading wal-1. If B is the only one with parent A we can start C. - * We have to read one more WAL before being able to start B. - * - * How do we know with the only information in B that we are not ready. - * - easy case, the parent is missing from the global map - * - more complex case we look at the Stack IDs. - * - * The Stack-IDs are added to the procedure order as an incremental index - * tracking how many times that procedure was executed, which is equivalent - * to the number of times we wrote the procedure to the WAL. - * In the example above: - * wal-2: B has stackId = [1, 2] - * wal-1: B has stackId = [1] - * wal-1: A has stackId = [0] - * - * Since we know that the Stack-IDs are incremental for a Procedure, - * we notice that there is a gap in the stackIds of B, so something was - * executed before. - * To identify when a Procedure is ready we do the sum of the stackIds of - * the procedure and the parent. if the stackIdSum is equal to the - * sum of {1..maxStackId} then everything we need is available. - * - * Example-2 - * wal-2 | A | A stackIds = [0, 2] - * wal-1 | A B | B stackIds = [1] - * - * There is a gap between A stackIds so something was executed in between. - */ - private boolean checkReadyToRun(Entry rootEntry) { - assert !rootEntry.hasParent() : "expected root procedure, got " + rootEntry; - - if (rootEntry.isFinished()) { - // If the root procedure is finished, sub-procedures should be gone - if (rootEntry.childHead != null) { - LOG.error("unexpected active children for root-procedure: " + rootEntry); - for (Entry p = rootEntry.childHead; p != null; p = p.linkNext) { - LOG.error("unexpected active children: " + p); - } - } - - assert rootEntry.childHead == null : "unexpected children on root completion. " + rootEntry; - rootEntry.ready = true; - return true; - } - - int stackIdSum = 0; - int maxStackId = 0; - for (int i = 0; i < rootEntry.proto.getStackIdCount(); ++i) { - int stackId = 1 + rootEntry.proto.getStackId(i); - maxStackId = Math.max(maxStackId, stackId); - stackIdSum += stackId; - if (LOG.isTraceEnabled()) { - LOG.trace("stackId=" + stackId + " stackIdSum=" + stackIdSum + - " maxStackid=" + maxStackId + " " + rootEntry); - } - } - - for (Entry p = rootEntry.childHead; p != null; p = p.linkNext) { - for (int i = 0; i < p.proto.getStackIdCount(); ++i) { - int stackId = 1 + p.proto.getStackId(i); - maxStackId = Math.max(maxStackId, stackId); - stackIdSum += stackId; - if (LOG.isTraceEnabled()) { - LOG.trace("stackId=" + stackId + " stackIdSum=" + stackIdSum + - " maxStackid=" + maxStackId + " " + p); - } - } - } - // The cmpStackIdSum is this formula for finding the sum of a series of numbers: - // http://www.wikihow.com/Sum-the-Integers-from-1-to-N#/Image:Sum-the-Integers-from-1-to-N-Step-2-Version-3.jpg - final int cmpStackIdSum = (maxStackId * (maxStackId + 1) / 2); - if (cmpStackIdSum == stackIdSum) { - rootEntry.ready = true; - for (Entry p = rootEntry.childHead; p != null; p = p.linkNext) { - p.ready = true; - } - return true; - } - return false; - } - - private void unlinkFromReplayList(Entry entry) { - if (replayOrderHead == entry) { - replayOrderHead = entry.replayNext; - } - if (replayOrderTail == entry) { - replayOrderTail = entry.replayPrev; - } - if (entry.replayPrev != null) { - entry.replayPrev.replayNext = entry.replayNext; - } - if (entry.replayNext != null) { - entry.replayNext.replayPrev = entry.replayPrev; - } - } - - private void addToReplayList(final Entry entry) { - unlinkFromReplayList(entry); - entry.replayNext = replayOrderHead; - entry.replayPrev = null; - if (replayOrderHead != null) { - replayOrderHead.replayPrev = entry; - } else { - replayOrderTail = entry; - } - replayOrderHead = entry; - } - - private void unlinkFromLinkList(Entry entry) { - if (entry == rootHead) { - rootHead = entry.linkNext; - } else if (entry == childUnlinkedHead) { - childUnlinkedHead = entry.linkNext; - } - if (entry.linkPrev != null) { - entry.linkPrev.linkNext = entry.linkNext; - } - if (entry.linkNext != null) { - entry.linkNext.linkPrev = entry.linkPrev; - } - } - - private Entry addToLinkList(Entry entry, Entry linkHead) { - unlinkFromLinkList(entry); - entry.linkNext = linkHead; - entry.linkPrev = null; - if (linkHead != null) { - linkHead.linkPrev = entry; - } - return entry; - } - - private Entry findLinkListTail(Entry linkHead) { - Entry tail = linkHead; - while (tail.linkNext != null) { - tail = tail.linkNext; - } - return tail; - } - - private Entry addToMap(final long procId, final boolean hasParent) { - int slotIndex = getMapSlot(procId); - Entry entry = getProcedure(slotIndex, procId); - if (entry != null) return entry; - - entry = new Entry(procedureMap[slotIndex]); - procedureMap[slotIndex] = entry; - return entry; - } - - private Entry removeFromMap(final long procId) { - int slotIndex = getMapSlot(procId); - Entry prev = null; - Entry entry = procedureMap[slotIndex]; - while (entry != null) { - if (procId == entry.getProcId()) { - if (prev != null) { - prev.hashNext = entry.hashNext; - } else { - procedureMap[slotIndex] = entry.hashNext; - } - entry.hashNext = null; - return entry; - } - prev = entry; - entry = entry.hashNext; - } - return null; - } - - private Entry getProcedure(final long procId) { - return getProcedure(getMapSlot(procId), procId); - } - - private Entry getProcedure(final int slotIndex, final long procId) { - Entry entry = procedureMap[slotIndex]; - while (entry != null) { - if (procId == entry.getProcId()) { - return entry; - } - entry = entry.hashNext; - } - return null; - } - - private int getMapSlot(final long procId) { - return (int)(Procedure.getProcIdHashCode(procId) % procedureMap.length); - } - } } diff --git a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/ProcedureWALPrettyPrinter.java b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/ProcedureWALPrettyPrinter.java index 582db77beaf4..3afcd16411ca 100644 --- a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/ProcedureWALPrettyPrinter.java +++ b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/ProcedureWALPrettyPrinter.java @@ -21,7 +21,6 @@ import java.io.PrintStream; import java.util.ArrayList; import java.util.List; - import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.conf.Configured; import org.apache.hadoop.fs.FSDataInputStream; @@ -30,22 +29,23 @@ import org.apache.hadoop.fs.Path; import org.apache.hadoop.hbase.HBaseConfiguration; import org.apache.hadoop.hbase.HBaseInterfaceAudience; -import org.apache.yetus.audience.InterfaceAudience; -import org.apache.yetus.audience.InterfaceStability; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.apache.hadoop.hbase.procedure2.Procedure; import org.apache.hadoop.hbase.procedure2.ProcedureUtil; -import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos.ProcedureWALEntry; -import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos.ProcedureWALHeader; import org.apache.hadoop.util.Tool; import org.apache.hadoop.util.ToolRunner; +import org.apache.yetus.audience.InterfaceAudience; +import org.apache.yetus.audience.InterfaceStability; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.apache.hbase.thirdparty.org.apache.commons.cli.CommandLine; +import org.apache.hbase.thirdparty.org.apache.commons.cli.DefaultParser; import org.apache.hbase.thirdparty.org.apache.commons.cli.HelpFormatter; import org.apache.hbase.thirdparty.org.apache.commons.cli.Options; import org.apache.hbase.thirdparty.org.apache.commons.cli.ParseException; -import org.apache.hbase.thirdparty.org.apache.commons.cli.PosixParser; + +import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos.ProcedureWALEntry; +import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos.ProcedureWALHeader; /** * ProcedureWALPrettyPrinter prints the contents of a given ProcedureWAL file @@ -164,7 +164,7 @@ public int run(final String[] args) throws IOException { final List files = new ArrayList<>(); try { - CommandLine cmd = new PosixParser().parse(options, args); + CommandLine cmd = new DefaultParser().parse(options, args); if (cmd.hasOption("f")) { files.add(new Path(cmd.getOptionValue("f"))); diff --git a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/WALProcedureMap.java b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/WALProcedureMap.java new file mode 100644 index 000000000000..9cda1bcc555c --- /dev/null +++ b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/WALProcedureMap.java @@ -0,0 +1,132 @@ +/** + * 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.hadoop.hbase.procedure2.store.wal; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos; + +/** + * This class is used to track the active procedures when loading procedures from proc wal file. + *

+ * We will read proc wal files from new to old, but when reading a proc wal file, we will still read + * from top to bottom, so there are two groups of methods for this class. + *

+ * The first group is {@link #add(ProcedureProtos.Procedure)} and {@link #remove(long)}. It is used + * when reading a proc wal file. In these methods, for the same procedure, typically the one comes + * later should win, please see the comment for + * {@link #isIncreasing(ProcedureProtos.Procedure, ProcedureProtos.Procedure)} to see the + * exceptions. + *

+ * The second group is {@link #merge(WALProcedureMap)}. We will have a global + * {@link WALProcedureMap} to hold global the active procedures, and a local {@link WALProcedureMap} + * to hold the active procedures for the current proc wal file. And when we finish reading a proc + * wal file, we will merge the local one into the global one, by calling the + * {@link #merge(WALProcedureMap)} method of the global one and pass the local one in. In this + * method, for the same procedure, the one comes earlier will win, as we read the proc wal files + * from new to old(the reverse order). + */ +@InterfaceAudience.Private +class WALProcedureMap { + + private static final Logger LOG = LoggerFactory.getLogger(WALProcedureMap.class); + + private final Map procMap = new HashMap<>(); + + private long minModifiedProcId = Long.MAX_VALUE; + + private long maxModifiedProcId = Long.MIN_VALUE; + + private void trackProcId(long procId) { + minModifiedProcId = Math.min(minModifiedProcId, procId); + maxModifiedProcId = Math.max(maxModifiedProcId, procId); + } + + /** + * @return True if this new procedure is 'richer' than the current one else false and we log this + * incidence where it appears that the WAL has older entries appended after newer ones. + * See HBASE-18152. + */ + private static boolean isIncreasing(ProcedureProtos.Procedure current, + ProcedureProtos.Procedure candidate) { + // Check that the procedures we see are 'increasing'. We used to compare + // procedure id first and then update time but it can legitimately go backwards if the + // procedure is failed or rolled back so that was unreliable. Was going to compare + // state but lets see if comparing update time enough (unfortunately this issue only + // seen under load...) + boolean increasing = current.getLastUpdate() <= candidate.getLastUpdate(); + if (!increasing) { + LOG.warn("NOT INCREASING! current=" + current + ", candidate=" + candidate); + } + return increasing; + } + + public void add(ProcedureProtos.Procedure proc) { + procMap.compute(proc.getProcId(), (procId, existingProc) -> { + if (existingProc == null || isIncreasing(existingProc, proc)) { + return proc; + } else { + return existingProc; + } + }); + trackProcId(proc.getProcId()); + } + + public void remove(long procId) { + procMap.remove(procId); + } + + public boolean isEmpty() { + return procMap.isEmpty(); + } + + public boolean contains(long procId) { + return procMap.containsKey(procId); + } + + /** + * Merge the given {@link WALProcedureMap} into this one. The {@link WALProcedureMap} passed in + * will be cleared after merging. + */ + public void merge(WALProcedureMap other) { + other.procMap.forEach(procMap::putIfAbsent); + maxModifiedProcId = Math.max(maxModifiedProcId, other.maxModifiedProcId); + minModifiedProcId = Math.max(minModifiedProcId, other.minModifiedProcId); + other.procMap.clear(); + other.maxModifiedProcId = Long.MIN_VALUE; + other.minModifiedProcId = Long.MAX_VALUE; + } + + public Collection getProcedures() { + return Collections.unmodifiableCollection(procMap.values()); + } + + public long getMinModifiedProcId() { + return minModifiedProcId; + } + + public long getMaxModifiedProcId() { + return maxModifiedProcId; + } +} diff --git a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/WALProcedureStore.java b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/WALProcedureStore.java index 7d5d6d244888..afde41bdfc8f 100644 --- a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/WALProcedureStore.java +++ b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/WALProcedureStore.java @@ -15,14 +15,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.apache.hadoop.hbase.procedure2.store.wal; import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; @@ -35,7 +33,6 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; - import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FSDataOutputStream; import org.apache.hadoop.fs.FileAlreadyExistsException; @@ -48,29 +45,69 @@ import org.apache.hadoop.hbase.log.HBaseMarkers; import org.apache.hadoop.hbase.procedure2.Procedure; import org.apache.hadoop.hbase.procedure2.ProcedureExecutor; +import org.apache.hadoop.hbase.procedure2.store.ProcedureStore; import org.apache.hadoop.hbase.procedure2.store.ProcedureStoreBase; import org.apache.hadoop.hbase.procedure2.store.ProcedureStoreTracker; import org.apache.hadoop.hbase.procedure2.util.ByteSlot; import org.apache.hadoop.hbase.procedure2.util.StringUtils; -import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos.ProcedureWALHeader; import org.apache.hadoop.hbase.util.CommonFSUtils; import org.apache.hadoop.hbase.util.Threads; import org.apache.hadoop.ipc.RemoteException; import org.apache.yetus.audience.InterfaceAudience; -import org.apache.yetus.audience.InterfaceStability; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting; import org.apache.hbase.thirdparty.org.apache.commons.collections4.queue.CircularFifoQueue; +import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos.ProcedureWALHeader; /** * WAL implementation of the ProcedureStore. + *

+ * When starting, the upper layer will first call {@link #start(int)}, then {@link #recoverLease()}, + * then {@link #load(ProcedureLoader)}. + *

+ * In {@link #recoverLease()}, we will get the lease by closing all the existing wal files(by + * calling recoverFileLease), and creating a new wal writer. And we will also get the list of all + * the old wal files. + *

+ * FIXME: notice that the current recover lease implementation is problematic, it can not deal with + * the races if there are two master both wants to acquire the lease... + *

+ * In {@link #load(ProcedureLoader)} method, we will load all the active procedures. See the + * comments of this method for more details. + *

+ * The actual logging way is a bit like our FileSystem based WAL implementation as RS side. There is + * a {@link #slots}, which is more like the ring buffer, and in the insert, update and delete + * methods we will put thing into the {@link #slots} and wait. And there is a background sync + * thread(see the {@link #syncLoop()} method) which get data from the {@link #slots} and write them + * to the FileSystem, and notify the caller that we have finished. + *

+ * TODO: try using disruptor to increase performance and simplify the logic? + *

+ * The {@link #storeTracker} keeps track of the modified procedures in the newest wal file, which is + * also the one being written currently. And the deleted bits in it are for all the procedures, not + * only the ones in the newest wal file. And when rolling a log, we will first store it in the + * trailer of the current wal file, and then reset its modified bits, so that it can start to track + * the modified procedures for the new wal file. + *

+ * The {@link #holdingCleanupTracker} is used to test whether we are safe to delete the oldest wal + * file. When there are log rolling and there are more than 1 wal files, we will make use of it. It + * will first be initialized to the oldest file's tracker(which is stored in the trailer), using the + * method {@link ProcedureStoreTracker#resetTo(ProcedureStoreTracker, boolean)}, and then merge it + * with the tracker of every newer wal files, using the + * {@link ProcedureStoreTracker#setDeletedIfModifiedInBoth(ProcedureStoreTracker)}. + * If we find out + * that all the modified procedures for the oldest wal file are modified or deleted in newer wal + * files, then we can delete it. This is because that, every time we call + * {@link ProcedureStore#insert(Procedure[])} or {@link ProcedureStore#update(Procedure)}, we will + * persist the full state of a Procedure, so the earlier wal records for this procedure can all be + * deleted. * @see ProcedureWALPrettyPrinter for printing content of a single WAL. * @see #main(String[]) to parse a directory of MasterWALProcs. */ @InterfaceAudience.Private -@InterfaceStability.Evolving public class WALProcedureStore extends ProcedureStoreBase { private static final Logger LOG = LoggerFactory.getLogger(WALProcedureStore.class); public static final String LOG_PREFIX = "pv2-"; @@ -84,7 +121,7 @@ public interface LeaseRecovery { public static final String WAL_COUNT_WARN_THRESHOLD_CONF_KEY = "hbase.procedure.store.wal.warn.threshold"; - private static final int DEFAULT_WAL_COUNT_WARN_THRESHOLD = 64; + private static final int DEFAULT_WAL_COUNT_WARN_THRESHOLD = 10; public static final String EXEC_WAL_CLEANUP_ON_LOAD_CONF_KEY = "hbase.procedure.store.wal.exec.cleanup.on.load"; @@ -166,7 +203,7 @@ public interface LeaseRecovery { private int syncWaitMsec; // Variables used for UI display - private CircularFifoQueue syncMetricsQueue; + private CircularFifoQueue syncMetricsQueue; public static class SyncMetrics { private long timestamp; @@ -228,11 +265,9 @@ public WALProcedureStore(final Configuration conf, final Path walDir, final Path // Create archive dir up front. Rename won't work w/o it up on HDFS. if (this.walArchiveDir != null && !this.fs.exists(this.walArchiveDir)) { if (this.fs.mkdirs(this.walArchiveDir)) { - if (LOG.isDebugEnabled()) { - LOG.debug("Created Procedure Store WAL archive dir " + this.walArchiveDir); - } + LOG.debug("Created Procedure Store WAL archive dir {}", this.walArchiveDir); } else { - LOG.warn("Failed create of " + this.walArchiveDir); + LOG.warn("Failed create of {}", this.walArchiveDir); } } } @@ -248,7 +283,7 @@ public void start(int numSlots) throws IOException { runningProcCount = numSlots; syncMaxSlot = numSlots; slots = new ByteSlot[numSlots]; - slotsCache = new LinkedTransferQueue(); + slotsCache = new LinkedTransferQueue<>(); while (slotsCache.size() < numSlots) { slotsCache.offer(new ByteSlot()); } @@ -267,7 +302,7 @@ public void start(int numSlots) throws IOException { useHsync = conf.getBoolean(USE_HSYNC_CONF_KEY, DEFAULT_USE_HSYNC); // WebUI - syncMetricsQueue = new CircularFifoQueue( + syncMetricsQueue = new CircularFifoQueue<>( conf.getInt(STORE_WAL_SYNC_STATS_COUNT, DEFAULT_SYNC_STATS_COUNT)); // Init sync thread @@ -309,7 +344,7 @@ public void stop(final boolean abort) { } // Close the writer - closeCurrentLogStream(); + closeCurrentLogStream(abort); // Close the old logs // they should be already closed, this is just in case the load fails @@ -364,7 +399,7 @@ public Set getCorruptedLogs() { public void recoverLease() throws IOException { lock.lock(); try { - LOG.trace("Starting WAL Procedure Store lease recovery"); + LOG.debug("Starting WAL Procedure Store lease recovery"); boolean afterFirstAttempt = false; while (isRunning()) { // Don't sleep before first attempt @@ -394,14 +429,12 @@ public void recoverLease() throws IOException { // We have the lease on the log oldLogs = getLogFiles(); if (getMaxLogId(oldLogs) > flushLogId) { - if (LOG.isDebugEnabled()) { - LOG.debug("Someone else created new logs. Expected maxLogId < " + flushLogId); - } + LOG.debug("Someone else created new logs. Expected maxLogId < {}", flushLogId); logs.getLast().removeFile(this.walArchiveDir); continue; } - LOG.trace("Lease acquired for flushLogId={}", flushLogId); + LOG.debug("Lease acquired for flushLogId={}", flushLogId); break; } } finally { @@ -410,7 +443,7 @@ public void recoverLease() throws IOException { } @Override - public void load(final ProcedureLoader loader) throws IOException { + public void load(ProcedureLoader loader) throws IOException { lock.lock(); try { if (logs.isEmpty()) { @@ -419,13 +452,13 @@ public void load(final ProcedureLoader loader) throws IOException { // Nothing to do, If we have only the current log. if (logs.size() == 1) { - LOG.trace("No state logs to replay."); + LOG.debug("No state logs to replay."); loader.setMaxProcId(0); return; } // Load the old logs - final Iterator it = logs.descendingIterator(); + Iterator it = logs.descendingIterator(); it.next(); // Skip the current log ProcedureWALFormat.load(it, storeTracker, new ProcedureWALFormat.Loader() { @@ -468,7 +501,9 @@ public void markCorruptedWAL(ProcedureWALFile log, IOException e) { private void tryCleanupLogsOnLoad() { // nothing to cleanup. - if (logs.size() <= 1) return; + if (logs.size() <= 1) { + return; + } // the config says to not cleanup wals on load. if (!conf.getBoolean(EXEC_WAL_CLEANUP_ON_LOAD_CONF_KEY, @@ -485,7 +520,7 @@ private void tryCleanupLogsOnLoad() { } @Override - public void insert(final Procedure proc, final Procedure[] subprocs) { + public void insert(Procedure proc, Procedure[] subprocs) { if (LOG.isTraceEnabled()) { LOG.trace("Insert " + proc + ", subproc=" + Arrays.toString(subprocs)); } @@ -519,7 +554,7 @@ public void insert(final Procedure proc, final Procedure[] subprocs) { } @Override - public void insert(final Procedure[] procs) { + public void insert(Procedure[] procs) { if (LOG.isTraceEnabled()) { LOG.trace("Insert " + Arrays.toString(procs)); } @@ -548,7 +583,7 @@ public void insert(final Procedure[] procs) { } @Override - public void update(final Procedure proc) { + public void update(Procedure proc) { if (LOG.isTraceEnabled()) { LOG.trace("Update " + proc); } @@ -571,11 +606,8 @@ public void update(final Procedure proc) { } @Override - public void delete(final long procId) { - if (LOG.isTraceEnabled()) { - LOG.trace("Delete " + procId); - } - + public void delete(long procId) { + LOG.trace("Delete {}", procId); ByteSlot slot = acquireSlot(); try { // Serialize the delete @@ -594,7 +626,7 @@ public void delete(final long procId) { } @Override - public void delete(final Procedure proc, final long[] subProcIds) { + public void delete(Procedure proc, long[] subProcIds) { assert proc != null : "expected a non-null procedure"; assert subProcIds != null && subProcIds.length > 0 : "expected subProcIds"; if (LOG.isTraceEnabled()) { @@ -630,7 +662,7 @@ public void delete(final long[] procIds, final int offset, final int count) { } } - private void delete(final long[] procIds) { + private void delete(long[] procIds) { if (LOG.isTraceEnabled()) { LOG.trace("Delete " + Arrays.toString(procIds)); } @@ -736,20 +768,20 @@ private void updateStoreTracker(final PushType type, storeTracker.insert(subProcIds); } else { storeTracker.insert(procId, subProcIds); - holdingCleanupTracker.setDeletedIfSet(procId); + holdingCleanupTracker.setDeletedIfModified(procId); } break; case UPDATE: storeTracker.update(procId); - holdingCleanupTracker.setDeletedIfSet(procId); + holdingCleanupTracker.setDeletedIfModified(procId); break; case DELETE: if (subProcIds != null && subProcIds.length > 0) { storeTracker.delete(subProcIds); - holdingCleanupTracker.setDeletedIfSet(subProcIds); + holdingCleanupTracker.setDeletedIfModified(subProcIds); } else { storeTracker.delete(procId); - holdingCleanupTracker.setDeletedIfSet(procId); + holdingCleanupTracker.setDeletedIfModified(procId); } break; default: @@ -942,7 +974,7 @@ public long getMillisFromLastRoll() { } @VisibleForTesting - protected void periodicRollForTesting() throws IOException { + void periodicRollForTesting() throws IOException { lock.lock(); try { periodicRoll(); @@ -952,7 +984,7 @@ protected void periodicRollForTesting() throws IOException { } @VisibleForTesting - protected boolean rollWriterForTesting() throws IOException { + public boolean rollWriterForTesting() throws IOException { lock.lock(); try { return rollWriter(); @@ -962,7 +994,7 @@ protected boolean rollWriterForTesting() throws IOException { } @VisibleForTesting - protected void removeInactiveLogsForTesting() throws Exception { + void removeInactiveLogsForTesting() throws Exception { lock.lock(); try { removeInactiveLogs(); @@ -973,17 +1005,13 @@ protected void removeInactiveLogsForTesting() throws Exception { private void periodicRoll() throws IOException { if (storeTracker.isEmpty()) { - if (LOG.isTraceEnabled()) { - LOG.trace("no active procedures"); - } + LOG.trace("no active procedures"); tryRollWriter(); - removeAllLogs(flushLogId - 1); + removeAllLogs(flushLogId - 1, "no active procedures"); } else { - if (storeTracker.isUpdated()) { - if (LOG.isTraceEnabled()) { - LOG.trace("all the active procedures are in the latest log"); - } - removeAllLogs(flushLogId - 1); + if (storeTracker.isAllModified()) { + LOG.trace("all the active procedures are in the latest log"); + removeAllLogs(flushLogId - 1, "all the active procedures are in the latest log"); } // if the log size has exceeded the roll threshold @@ -997,18 +1025,20 @@ private void periodicRoll() throws IOException { } private boolean rollWriter() throws IOException { - if (!isRunning()) return false; + if (!isRunning()) { + return false; + } // Create new state-log if (!rollWriter(flushLogId + 1)) { - LOG.warn("someone else has already created log " + flushLogId); + LOG.warn("someone else has already created log {}", flushLogId); return false; } // We have the lease on the log, // but we should check if someone else has created new files if (getMaxLogId(getLogFiles()) > flushLogId) { - LOG.warn("Someone else created new logs. Expected maxLogId < " + flushLogId); + LOG.warn("Someone else created new logs. Expected maxLogId < {}", flushLogId); logs.getLast().removeFile(this.walArchiveDir); return false; } @@ -1018,7 +1048,7 @@ private boolean rollWriter() throws IOException { } @VisibleForTesting - boolean rollWriter(final long logId) throws IOException { + boolean rollWriter(long logId) throws IOException { assert logId > flushLogId : "logId=" + logId + " flushLogId=" + flushLogId; assert lock.isHeldByCurrentThread() : "expected to be the lock owner. " + lock.isLocked(); @@ -1036,10 +1066,10 @@ boolean rollWriter(final long logId) throws IOException { try { newStream = CommonFSUtils.createForWal(fs, newLogFile, false); } catch (FileAlreadyExistsException e) { - LOG.error("Log file with id=" + logId + " already exists", e); + LOG.error("Log file with id={} already exists", logId, e); return false; } catch (RemoteException re) { - LOG.warn("failed to create log file with id=" + logId, re); + LOG.warn("failed to create log file with id={}", logId, re); return false; } // After we create the stream but before we attempt to use it at all @@ -1062,9 +1092,9 @@ boolean rollWriter(final long logId) throws IOException { return false; } - closeCurrentLogStream(); + closeCurrentLogStream(false); - storeTracker.resetUpdates(); + storeTracker.resetModified(); stream = newStream; flushLogId = logId; totalSynced.set(0); @@ -1076,28 +1106,40 @@ boolean rollWriter(final long logId) throws IOException { if (logs.size() == 2) { buildHoldingCleanupTracker(); } else if (logs.size() > walCountWarnThreshold) { - LOG.warn("procedure WALs count=" + logs.size() + - " above the warning threshold " + walCountWarnThreshold + - ". check running procedures to see if something is stuck."); + LOG.warn("procedure WALs count={} above the warning threshold {}. check running procedures" + + " to see if something is stuck.", logs.size(), walCountWarnThreshold); + // This is just like what we have done at RS side when there are too many wal files. For RS, + // if there are too many wal files, we will find out the wal entries in the oldest file, and + // tell the upper layer to flush these regions so the wal entries will be useless and then we + // can delete the wal file. For WALProcedureStore, the assumption is that, if all the + // procedures recorded in a proc wal file are modified or deleted in a new proc wal file, then + // we are safe to delete it. So here if there are too many proc wal files, we will find out + // the procedure ids in the oldest file, which are neither modified nor deleted in newer proc + // wal files, and tell upper layer to update the state of these procedures to the newest proc + // wal file(by calling ProcedureStore.update), then we are safe to delete the oldest proc wal + // file. + sendForceUpdateSignal(holdingCleanupTracker.getAllActiveProcIds()); } LOG.info("Rolled new Procedure Store WAL, id={}", logId); return true; } - private void closeCurrentLogStream() { + private void closeCurrentLogStream(boolean abort) { if (stream == null || logs.isEmpty()) { return; } try { ProcedureWALFile log = logs.getLast(); - log.setProcIds(storeTracker.getUpdatedMinProcId(), storeTracker.getUpdatedMaxProcId()); + log.setProcIds(storeTracker.getModifiedMinProcId(), storeTracker.getModifiedMaxProcId()); log.updateLocalTracker(storeTracker); - long trailerSize = ProcedureWALFormat.writeTrailer(stream, storeTracker); - log.addToSize(trailerSize); + if (!abort) { + long trailerSize = ProcedureWALFormat.writeTrailer(stream, storeTracker); + log.addToSize(trailerSize); + } } catch (IOException e) { - LOG.warn("Unable to write the trailer: " + e.getMessage()); + LOG.warn("Unable to write the trailer", e); } try { stream.close(); @@ -1114,6 +1156,7 @@ private void removeInactiveLogs() throws IOException { // We keep track of which procedures are holding the oldest WAL in 'holdingCleanupTracker'. // once there is nothing olding the oldest WAL we can remove it. while (logs.size() > 1 && holdingCleanupTracker.isEmpty()) { + LOG.info("Remove the oldest log {}", logs.getFirst()); removeLogFile(logs.getFirst(), walArchiveDir); buildHoldingCleanupTracker(); } @@ -1130,26 +1173,39 @@ private void buildHoldingCleanupTracker() { } // compute the holding tracker. - // - the first WAL is used for the 'updates' - // - the other WALs are scanned to remove procs already in other wals. - // TODO: exit early if holdingCleanupTracker.isEmpty() + // - the first WAL is used for the 'updates' + // - the global tracker will be used to determine whether a procedure has been deleted + // - other trackers will be used to determine whether a procedure has been updated, as a deleted + // procedure can always be detected by checking the global tracker, we can save the deleted + // checks when applying other trackers holdingCleanupTracker.resetTo(logs.getFirst().getTracker(), true); - holdingCleanupTracker.setDeletedIfSet(storeTracker); - for (int i = 1, size = logs.size() - 1; i < size; ++i) { - holdingCleanupTracker.setDeletedIfSet(logs.get(i).getTracker()); + holdingCleanupTracker.setDeletedIfDeletedByThem(storeTracker); + // the logs is a linked list, so avoid calling get(index) on it. + Iterator iter = logs.iterator(); + // skip the tracker for the first file when creating the iterator. + iter.next(); + ProcedureStoreTracker tracker = iter.next().getTracker(); + // testing iter.hasNext after calling iter.next to skip applying the tracker for last file, + // which is just the storeTracker above. + while (iter.hasNext()) { + holdingCleanupTracker.setDeletedIfModifiedInBoth(tracker); + if (holdingCleanupTracker.isEmpty()) { + break; + } + iter.next(); } } /** * Remove all logs with logId <= {@code lastLogId}. */ - private void removeAllLogs(long lastLogId) { - if (logs.size() <= 1) return; - - if (LOG.isTraceEnabled()) { - LOG.trace("Remove all state logs with ID less than " + lastLogId); + private void removeAllLogs(long lastLogId, String why) { + if (logs.size() <= 1) { + return; } + LOG.info("Remove all state logs with ID less than {}, since {}", lastLogId, why); + boolean removed = false; while (logs.size() > 1) { ProcedureWALFile log = logs.getFirst(); @@ -1167,14 +1223,10 @@ private void removeAllLogs(long lastLogId) { private boolean removeLogFile(final ProcedureWALFile log, final Path walArchiveDir) { try { - if (LOG.isTraceEnabled()) { - LOG.trace("Removing log=" + log); - } + LOG.trace("Removing log={}", log); log.removeFile(walArchiveDir); logs.remove(log); - if (LOG.isDebugEnabled()) { - LOG.info("Removed log=" + log + ", activeLogs=" + logs); - } + LOG.debug("Removed log={}, activeLogs={}", log, logs); assert logs.size() > 0 : "expected at least one log"; } catch (IOException e) { LOG.error("Unable to remove log: " + log, e); @@ -1238,50 +1290,53 @@ private FileStatus[] getLogFiles() throws IOException { } } - private static long getMaxLogId(final FileStatus[] logFiles) { - long maxLogId = 0; - if (logFiles != null && logFiles.length > 0) { - for (int i = 0; i < logFiles.length; ++i) { - maxLogId = Math.max(maxLogId, getLogIdFromName(logFiles[i].getPath().getName())); - } + /** + * Make sure that the file set are gotten by calling {@link #getLogFiles()}, where we will sort + * the file set by log id. + * @return Max-LogID of the specified log file set + */ + private static long getMaxLogId(FileStatus[] logFiles) { + if (logFiles == null || logFiles.length == 0) { + return 0L; } - return maxLogId; + return getLogIdFromName(logFiles[logFiles.length - 1].getPath().getName()); } /** + * Make sure that the file set are gotten by calling {@link #getLogFiles()}, where we will sort + * the file set by log id. * @return Max-LogID of the specified log file set */ - private long initOldLogs(final FileStatus[] logFiles) throws IOException { - this.logs.clear(); - + private long initOldLogs(FileStatus[] logFiles) throws IOException { + if (logFiles == null || logFiles.length == 0) { + return 0L; + } long maxLogId = 0; - if (logFiles != null && logFiles.length > 0) { - for (int i = 0; i < logFiles.length; ++i) { - final Path logPath = logFiles[i].getPath(); - leaseRecovery.recoverFileLease(fs, logPath); - if (!isRunning()) { - throw new IOException("wal aborting"); - } + for (int i = 0; i < logFiles.length; ++i) { + final Path logPath = logFiles[i].getPath(); + leaseRecovery.recoverFileLease(fs, logPath); + if (!isRunning()) { + throw new IOException("wal aborting"); + } - maxLogId = Math.max(maxLogId, getLogIdFromName(logPath.getName())); - ProcedureWALFile log = initOldLog(logFiles[i], this.walArchiveDir); - if (log != null) { - this.logs.add(log); - } + maxLogId = Math.max(maxLogId, getLogIdFromName(logPath.getName())); + ProcedureWALFile log = initOldLog(logFiles[i], this.walArchiveDir); + if (log != null) { + this.logs.add(log); } - Collections.sort(this.logs); - initTrackerFromOldLogs(); } + initTrackerFromOldLogs(); return maxLogId; } /** - * If last log's tracker is not null, use it as {@link #storeTracker}. - * Otherwise, set storeTracker as partial, and let {@link ProcedureWALFormatReader} rebuild - * it using entries in the log. + * If last log's tracker is not null, use it as {@link #storeTracker}. Otherwise, set storeTracker + * as partial, and let {@link ProcedureWALFormatReader} rebuild it using entries in the log. */ private void initTrackerFromOldLogs() { - if (logs.isEmpty() || !isRunning()) return; + if (logs.isEmpty() || !isRunning()) { + return; + } ProcedureWALFile log = logs.getLast(); if (!log.getTracker().isPartial()) { storeTracker.resetTo(log.getTracker()); @@ -1295,20 +1350,18 @@ private void initTrackerFromOldLogs() { * Loads given log file and it's tracker. */ private ProcedureWALFile initOldLog(final FileStatus logFile, final Path walArchiveDir) - throws IOException { + throws IOException { final ProcedureWALFile log = new ProcedureWALFile(fs, logFile); if (logFile.getLen() == 0) { - LOG.warn("Remove uninitialized log: " + logFile); + LOG.warn("Remove uninitialized log: {}", logFile); log.removeFile(walArchiveDir); return null; } - if (LOG.isDebugEnabled()) { - LOG.debug("Opening Pv2 " + logFile); - } + LOG.debug("Opening Pv2 {}", logFile); try { log.open(); } catch (ProcedureWALFormat.InvalidWALDataException e) { - LOG.warn("Remove uninitialized log: " + logFile, e); + LOG.warn("Remove uninitialized log: {}", logFile, e); log.removeFile(walArchiveDir); return null; } catch (IOException e) { @@ -1322,7 +1375,7 @@ private ProcedureWALFile initOldLog(final FileStatus logFile, final Path walArch } catch (IOException e) { log.getTracker().reset(); log.getTracker().setPartialFlag(true); - LOG.warn("Unable to read tracker for " + log + " - " + e.getMessage()); + LOG.warn("Unable to read tracker for {}", log, e); } log.close(); @@ -1350,7 +1403,7 @@ public void recoverFileLease(FileSystem fs, Path path) throws IOException { }); try { store.start(16); - ProcedureExecutor pe = new ProcedureExecutor(conf, new Object()/*Pass anything*/, store); + ProcedureExecutor pe = new ProcedureExecutor<>(conf, new Object()/*Pass anything*/, store); pe.init(1, true); } finally { store.stop(true); diff --git a/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/WALProcedureTree.java b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/WALProcedureTree.java new file mode 100644 index 000000000000..6e624b4fca2f --- /dev/null +++ b/hbase-procedure/src/main/java/org/apache/hadoop/hbase/procedure2/store/wal/WALProcedureTree.java @@ -0,0 +1,302 @@ +/** + * 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.hadoop.hbase.procedure2.store.wal; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import org.apache.commons.lang3.mutable.MutableInt; +import org.apache.hadoop.hbase.procedure2.Procedure; +import org.apache.hadoop.hbase.procedure2.ProcedureUtil; +import org.apache.hadoop.hbase.procedure2.store.ProcedureStore.ProcedureIterator; +import org.apache.yetus.audience.InterfaceAudience; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos; + +/** + * Used to build the tree for procedures. + *

+ * We will group the procedures with the root procedure, and then validate each group. For each + * group of procedures(with the same root procedure), we will collect all the stack ids, if the max + * stack id is n, then all the stack ids should be from 0 to n, non-repetition and non-omission. If + * not, we will consider all the procedures in this group as corrupted. Please see the code in + * {@link #checkReady(Entry, Map)} method. + *

+ * For the procedures not in any group, i.e, can not find the root procedure for these procedures, + * we will also consider them as corrupted. Please see the code in {@link #checkOrphan(Map)} method. + */ +@InterfaceAudience.Private +public final class WALProcedureTree { + + private static final Logger LOG = LoggerFactory.getLogger(WALProcedureTree.class); + + private static final class Entry { + + private final ProcedureProtos.Procedure proc; + + private final List subProcs = new ArrayList<>(); + + public Entry(ProcedureProtos.Procedure proc) { + this.proc = proc; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Procedure(pid="); + sb.append(proc.getProcId()); + sb.append(", ppid="); + sb.append(proc.hasParentId() ? proc.getParentId() : Procedure.NO_PROC_ID); + sb.append(", class="); + sb.append(proc.getClassName()); + sb.append(")"); + return sb.toString(); + } + } + + // when loading we will iterator the procedures twice, so use this class to cache the deserialized + // result to prevent deserializing multiple times. + private static final class ProtoAndProc { + private final ProcedureProtos.Procedure proto; + + private Procedure proc; + + public ProtoAndProc(ProcedureProtos.Procedure proto) { + this.proto = proto; + } + + public Procedure getProc() throws IOException { + if (proc == null) { + proc = ProcedureUtil.convertToProcedure(proto); + } + return proc; + } + } + + private final List validProcs = new ArrayList<>(); + + private final List corruptedProcs = new ArrayList<>(); + + private static boolean isFinished(ProcedureProtos.Procedure proc) { + if (!proc.hasParentId()) { + switch (proc.getState()) { + case ROLLEDBACK: + case SUCCESS: + return true; + default: + break; + } + } + return false; + } + + private WALProcedureTree(Map procMap) { + List rootEntries = buildTree(procMap); + for (Entry rootEntry : rootEntries) { + checkReady(rootEntry, procMap); + } + checkOrphan(procMap); + Comparator cmp = + (p1, p2) -> Long.compare(p1.proto.getProcId(), p2.proto.getProcId()); + Collections.sort(validProcs, cmp); + Collections.sort(corruptedProcs, cmp); + } + + private List buildTree(Map procMap) { + List rootEntries = new ArrayList<>(); + procMap.values().forEach(entry -> { + if (!entry.proc.hasParentId()) { + rootEntries.add(entry); + } else { + Entry parentEntry = procMap.get(entry.proc.getParentId()); + // For a valid procedure this should not be null. We will log the error later if it is null, + // as it will not be referenced by any root procedures. + if (parentEntry != null) { + parentEntry.subProcs.add(entry); + } + } + }); + return rootEntries; + } + + private void collectStackId(Entry entry, Map> stackId2Proc, + MutableInt maxStackId) { + if (LOG.isDebugEnabled()) { + LOG.debug("Procedure {} stack ids={}", entry, entry.proc.getStackIdList()); + } + for (int i = 0, n = entry.proc.getStackIdCount(); i < n; i++) { + int stackId = entry.proc.getStackId(i); + if (stackId > maxStackId.intValue()) { + maxStackId.setValue(stackId); + } + stackId2Proc.computeIfAbsent(stackId, k -> new ArrayList<>()).add(entry); + } + entry.subProcs.forEach(e -> collectStackId(e, stackId2Proc, maxStackId)); + } + + private void addAllToCorruptedAndRemoveFromProcMap(Entry entry, + Map remainingProcMap) { + corruptedProcs.add(new ProtoAndProc(entry.proc)); + remainingProcMap.remove(entry.proc.getProcId()); + for (Entry e : entry.subProcs) { + addAllToCorruptedAndRemoveFromProcMap(e, remainingProcMap); + } + } + + private void addAllToValidAndRemoveFromProcMap(Entry entry, Map remainingProcMap) { + validProcs.add(new ProtoAndProc(entry.proc)); + remainingProcMap.remove(entry.proc.getProcId()); + for (Entry e : entry.subProcs) { + addAllToValidAndRemoveFromProcMap(e, remainingProcMap); + } + } + + // In this method first we will check whether the given root procedure and all its sub procedures + // are valid, through the procedure stack. And we will also remove all these procedures from the + // remainingProcMap, so at last, if there are still procedures in the map, we know that there are + // orphan procedures. + private void checkReady(Entry rootEntry, Map remainingProcMap) { + if (isFinished(rootEntry.proc)) { + if (!rootEntry.subProcs.isEmpty()) { + LOG.error("unexpected active children for root-procedure: {}", rootEntry); + rootEntry.subProcs.forEach(e -> LOG.error("unexpected active children: {}", e)); + addAllToCorruptedAndRemoveFromProcMap(rootEntry, remainingProcMap); + } else { + addAllToValidAndRemoveFromProcMap(rootEntry, remainingProcMap); + } + return; + } + Map> stackId2Proc = new HashMap<>(); + MutableInt maxStackId = new MutableInt(Integer.MIN_VALUE); + collectStackId(rootEntry, stackId2Proc, maxStackId); + // the stack ids should start from 0 and increase by one every time + boolean valid = true; + for (int i = 0; i <= maxStackId.intValue(); i++) { + List entries = stackId2Proc.get(i); + if (entries == null) { + LOG.error("Missing stack id {}, max stack id is {}, root procedure is {}", i, maxStackId, + rootEntry); + valid = false; + } else if (entries.size() > 1) { + LOG.error("Multiple procedures {} have the same stack id {}, max stack id is {}," + + " root procedure is {}", entries, i, maxStackId, rootEntry); + valid = false; + } + } + if (valid) { + addAllToValidAndRemoveFromProcMap(rootEntry, remainingProcMap); + } else { + addAllToCorruptedAndRemoveFromProcMap(rootEntry, remainingProcMap); + } + } + + private void checkOrphan(Map procMap) { + procMap.values().forEach(entry -> { + LOG.error("Orphan procedure: {}", entry); + corruptedProcs.add(new ProtoAndProc(entry.proc)); + }); + } + + private static final class Iter implements ProcedureIterator { + + private final List procs; + + private Iterator iter; + + private ProtoAndProc current; + + public Iter(List procs) { + this.procs = procs; + reset(); + } + + @Override + public void reset() { + iter = procs.iterator(); + if (iter.hasNext()) { + current = iter.next(); + } else { + current = null; + } + } + + @Override + public boolean hasNext() { + return current != null; + } + + private void checkNext() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + } + + @Override + public boolean isNextFinished() { + checkNext(); + return isFinished(current.proto); + } + + private void moveToNext() { + if (iter.hasNext()) { + current = iter.next(); + } else { + current = null; + } + } + + @Override + public void skipNext() { + checkNext(); + moveToNext(); + } + + @Override + public Procedure next() throws IOException { + checkNext(); + Procedure proc = current.getProc(); + moveToNext(); + return proc; + } + } + + public ProcedureIterator getValidProcs() { + return new Iter(validProcs); + } + + public ProcedureIterator getCorruptedProcs() { + return new Iter(corruptedProcs); + } + + public static WALProcedureTree build(Collection procedures) { + Map procMap = new HashMap<>(); + for (ProcedureProtos.Procedure proc : procedures) { + procMap.put(proc.getProcId(), new Entry(proc)); + } + return new WALProcedureTree(procMap); + } +} diff --git a/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/ProcedureTestingUtility.java b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/ProcedureTestingUtility.java index d52b6bb2e5ab..95e032043fb4 100644 --- a/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/ProcedureTestingUtility.java +++ b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/ProcedureTestingUtility.java @@ -67,20 +67,44 @@ public void recoverFileLease(FileSystem fs, Path path) throws IOException { }); } + public static void restart(final ProcedureExecutor procExecutor, + boolean abort, boolean startWorkers) throws Exception { + restart(procExecutor, false, true, null, null, null, abort, startWorkers); + } + + public static void restart(final ProcedureExecutor procExecutor, + boolean abort) throws Exception { + restart(procExecutor, false, true, null, null, null, abort, true); + } + public static void restart(final ProcedureExecutor procExecutor) throws Exception { - restart(procExecutor, false, true, null, null, null); + restart(procExecutor, false, true, null, null, null, false, true); } public static void initAndStartWorkers(ProcedureExecutor procExecutor, int numThreads, boolean abortOnCorruption) throws IOException { + initAndStartWorkers(procExecutor, numThreads, abortOnCorruption, true); + } + + public static void initAndStartWorkers(ProcedureExecutor procExecutor, int numThreads, + boolean abortOnCorruption, boolean startWorkers) throws IOException { procExecutor.init(numThreads, abortOnCorruption); - procExecutor.startWorkers(); + if (startWorkers) { + procExecutor.startWorkers(); + } } public static void restart(ProcedureExecutor procExecutor, boolean avoidTestKillDuringRestart, boolean failOnCorrupted, Callable stopAction, - Callable actionBeforeStartWorker, Callable startAction) - throws Exception { + Callable actionBeforeStartWorker, Callable startAction) throws Exception { + restart(procExecutor, avoidTestKillDuringRestart, failOnCorrupted, stopAction, + actionBeforeStartWorker, startAction, false, true); + } + + public static void restart(ProcedureExecutor procExecutor, + boolean avoidTestKillDuringRestart, boolean failOnCorrupted, Callable stopAction, + Callable actionBeforeStartWorker, Callable startAction, boolean abort, + boolean startWorkers) throws Exception { final ProcedureStore procStore = procExecutor.getStore(); final int storeThreads = procExecutor.getCorePoolSize(); final int execThreads = procExecutor.getCorePoolSize(); @@ -93,7 +117,7 @@ public static void restart(ProcedureExecutor procExecutor, // stop LOG.info("RESTART - Stop"); procExecutor.stop(); - procStore.stop(false); + procStore.stop(abort); if (stopAction != null) { stopAction.call(); } @@ -109,7 +133,9 @@ public static void restart(ProcedureExecutor procExecutor, if (actionBeforeStartWorker != null) { actionBeforeStartWorker.call(); } - procExecutor.startWorkers(); + if (startWorkers) { + procExecutor.startWorkers(); + } if (startAction != null) { startAction.call(); } @@ -207,7 +233,7 @@ public static long submitAndWait(Configuration conf, TEnv env, Procedure< NoopProcedureStore procStore = new NoopProcedureStore(); ProcedureExecutor procExecutor = new ProcedureExecutor<>(conf, env, procStore); procStore.start(1); - initAndStartWorkers(procExecutor, 1, false); + initAndStartWorkers(procExecutor, 1, false, true); try { return submitAndWait(procExecutor, proc, HConstants.NO_NONCE, HConstants.NO_NONCE); } finally { @@ -400,6 +426,46 @@ protected void deserializeStateData(ProcedureStateSerializer serializer) } } + public static class NoopStateMachineProcedure + extends StateMachineProcedure { + private TState initialState; + private TEnv env; + + public NoopStateMachineProcedure() { + } + + public NoopStateMachineProcedure(TEnv env, TState initialState) { + this.env = env; + this.initialState = initialState; + } + + @Override + protected Flow executeFromState(TEnv env, TState tState) + throws ProcedureSuspendedException, ProcedureYieldException, InterruptedException { + return null; + } + + @Override + protected void rollbackState(TEnv env, TState tState) throws IOException, InterruptedException { + + } + + @Override + protected TState getState(int stateId) { + return null; + } + + @Override + protected int getStateId(TState tState) { + return 0; + } + + @Override + protected TState getInitialState() { + return initialState; + } + } + public static class TestProcedure extends NoopProcedure { private byte[] data = null; diff --git a/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestProcedureBypass.java b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestProcedureBypass.java index d58d57e76e8b..7d587fd0ab51 100644 --- a/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestProcedureBypass.java +++ b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestProcedureBypass.java @@ -17,8 +17,10 @@ */ package org.apache.hadoop.hbase.procedure2; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import org.apache.hadoop.fs.FileSystem; @@ -87,7 +89,7 @@ public void testBypassSuspendProcedure() throws Exception { long id = procExecutor.submitProcedure(proc); Thread.sleep(500); //bypass the procedure - assertTrue(procExecutor.bypassProcedure(id, 30000, false)); + assertTrue(procExecutor.bypassProcedure(id, 30000, false, false)); htu.waitFor(5000, () -> proc.isSuccess() && proc.isBypass()); LOG.info("{} finished", proc); } @@ -98,7 +100,7 @@ public void testStuckProcedure() throws Exception { long id = procExecutor.submitProcedure(proc); Thread.sleep(500); //bypass the procedure - assertTrue(procExecutor.bypassProcedure(id, 1000, true)); + assertTrue(procExecutor.bypassProcedure(id, 1000, true, false)); //Since the procedure is stuck there, we need to restart the executor to recovery. ProcedureTestingUtility.restart(procExecutor); htu.waitFor(5000, () -> proc.isSuccess() && proc.isBypass()); @@ -114,12 +116,38 @@ public void testBypassingProcedureWithParent() throws Exception { .size() > 0); SuspendProcedure suspendProcedure = (SuspendProcedure)procExecutor.getProcedures().stream() .filter(p -> p.getParentProcId() == rootId).collect(Collectors.toList()).get(0); - assertTrue(procExecutor.bypassProcedure(suspendProcedure.getProcId(), 1000, false)); + assertTrue(procExecutor.bypassProcedure(suspendProcedure.getProcId(), 1000, false, false)); htu.waitFor(5000, () -> proc.isSuccess() && proc.isBypass()); LOG.info("{} finished", proc); } + @Test + public void testBypassingStuckStateMachineProcedure() throws Exception { + final StuckStateMachineProcedure proc = + new StuckStateMachineProcedure(procEnv, StuckStateMachineState.START); + long id = procExecutor.submitProcedure(proc); + Thread.sleep(500); + // bypass the procedure + assertFalse(procExecutor.bypassProcedure(id, 1000, false, false)); + assertTrue(procExecutor.bypassProcedure(id, 1000, true, false)); + htu.waitFor(5000, () -> proc.isSuccess() && proc.isBypass()); + LOG.info("{} finished", proc); + } + + @Test + public void testBypassingProcedureWithParentRecursive() throws Exception { + final RootProcedure proc = new RootProcedure(); + long rootId = procExecutor.submitProcedure(proc); + htu.waitFor(5000, () -> procExecutor.getProcedures().stream() + .filter(p -> p.getParentProcId() == rootId).collect(Collectors.toList()) + .size() > 0); + SuspendProcedure suspendProcedure = (SuspendProcedure)procExecutor.getProcedures().stream() + .filter(p -> p.getParentProcId() == rootId).collect(Collectors.toList()).get(0); + assertTrue(procExecutor.bypassProcedure(rootId, 1000, false, true)); + htu.waitFor(5000, () -> proc.isSuccess() && proc.isBypass()); + LOG.info("{} finished", proc); + } @AfterClass public static void tearDown() throws Exception { @@ -181,5 +209,52 @@ protected Procedure[] execute(final TestProcEnv env) } + public enum StuckStateMachineState { + START, THEN, END + } + + public static class StuckStateMachineProcedure extends + ProcedureTestingUtility.NoopStateMachineProcedure { + private AtomicBoolean stop = new AtomicBoolean(false); + + public StuckStateMachineProcedure() { + super(); + } + + public StuckStateMachineProcedure(TestProcEnv env, StuckStateMachineState initialState) { + super(env, initialState); + } + + @Override + protected Flow executeFromState(TestProcEnv env, StuckStateMachineState tState) + throws ProcedureSuspendedException, ProcedureYieldException, InterruptedException { + switch (tState) { + case START: + LOG.info("PHASE 1: START"); + setNextState(StuckStateMachineState.THEN); + return Flow.HAS_MORE_STATE; + case THEN: + if (stop.get()) { + setNextState(StuckStateMachineState.END); + } + return Flow.HAS_MORE_STATE; + case END: + return Flow.NO_MORE_STATE; + default: + throw new UnsupportedOperationException("unhandled state=" + tState); + } + } + + @Override + protected StuckStateMachineState getState(int stateId) { + return StuckStateMachineState.values()[stateId]; + } + + @Override + protected int getStateId(StuckStateMachineState tState) { + return tState.ordinal(); + } + } + } diff --git a/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestProcedureCleanup.java b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestProcedureCleanup.java new file mode 100644 index 000000000000..82917ea5315c --- /dev/null +++ b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestProcedureCleanup.java @@ -0,0 +1,286 @@ +/** + * 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.hadoop.hbase.procedure2; + +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Exchanger; +import org.apache.hadoop.fs.FSDataInputStream; +import org.apache.hadoop.fs.FSDataOutputStream; +import org.apache.hadoop.fs.FileStatus; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.HBaseCommonTestingUtility; +import org.apache.hadoop.hbase.procedure2.store.wal.WALProcedureStore; +import org.apache.hadoop.hbase.testclassification.MasterTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.rules.TestName; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hbase.thirdparty.com.google.common.io.ByteStreams; + +@Category({ MasterTests.class, SmallTests.class }) +public class TestProcedureCleanup { + + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestProcedureCleanup.class); + + + private static final Logger LOG = LoggerFactory.getLogger(TestProcedureCleanup.class); + + private static final int PROCEDURE_EXECUTOR_SLOTS = 2; + + private static WALProcedureStore procStore; + + private static ProcedureExecutor procExecutor; + + private static HBaseCommonTestingUtility htu; + + private static FileSystem fs; + private static Path testDir; + private static Path logDir; + + @Rule + public final TestName name = new TestName(); + + private void createProcExecutor() throws Exception { + logDir = new Path(testDir, name.getMethodName()); + procStore = ProcedureTestingUtility.createWalStore(htu.getConfiguration(), logDir); + procExecutor = new ProcedureExecutor<>(htu.getConfiguration(), null, procStore); + procStore.start(PROCEDURE_EXECUTOR_SLOTS); + ProcedureTestingUtility.initAndStartWorkers(procExecutor, PROCEDURE_EXECUTOR_SLOTS, true, true); + } + + @BeforeClass + public static void setUp() throws Exception { + htu = new HBaseCommonTestingUtility(); + htu.getConfiguration().setBoolean(WALProcedureStore.EXEC_WAL_CLEANUP_ON_LOAD_CONF_KEY, true); + // NOTE: The executor will be created by each test + testDir = htu.getDataTestDir(); + fs = testDir.getFileSystem(htu.getConfiguration()); + assertTrue(testDir.depth() > 1); + } + + @Test + public void testProcedureShouldNotCleanOnLoad() throws Exception { + createProcExecutor(); + final RootProcedure proc = new RootProcedure(); + long rootProc = procExecutor.submitProcedure(proc); + LOG.info("Begin to execute " + rootProc); + // wait until the child procedure arrival + htu.waitFor(10000, () -> procExecutor.getProcedures().size() >= 2); + SuspendProcedure suspendProcedure = (SuspendProcedure) procExecutor + .getProcedures().get(1); + // wait until the suspendProcedure executed + suspendProcedure.latch.countDown(); + Thread.sleep(100); + // roll the procedure log + LOG.info("Begin to roll log "); + procStore.rollWriterForTesting(); + LOG.info("finish to roll log "); + Thread.sleep(500); + LOG.info("begin to restart1 "); + ProcedureTestingUtility.restart(procExecutor, true); + LOG.info("finish to restart1 "); + assertTrue(procExecutor.getProcedure(rootProc) != null); + Thread.sleep(500); + LOG.info("begin to restart2 "); + ProcedureTestingUtility.restart(procExecutor, true); + LOG.info("finish to restart2 "); + assertTrue(procExecutor.getProcedure(rootProc) != null); + } + + @Test + public void testProcedureUpdatedShouldClean() throws Exception { + createProcExecutor(); + SuspendProcedure suspendProcedure = new SuspendProcedure(); + long suspendProc = procExecutor.submitProcedure(suspendProcedure); + LOG.info("Begin to execute " + suspendProc); + suspendProcedure.latch.countDown(); + Thread.sleep(500); + LOG.info("begin to restart1 "); + ProcedureTestingUtility.restart(procExecutor, true); + LOG.info("finish to restart1 "); + htu.waitFor(10000, () -> procExecutor.getProcedure(suspendProc) != null); + // Wait until the suspendProc executed after restart + suspendProcedure = (SuspendProcedure) procExecutor.getProcedure(suspendProc); + suspendProcedure.latch.countDown(); + Thread.sleep(500); + // Should be 1 log since the suspendProcedure is updated in the new log + assertTrue(procStore.getActiveLogs().size() == 1); + // restart procExecutor + LOG.info("begin to restart2"); + // Restart the executor but do not start the workers. + // Otherwise, the suspendProcedure will soon be executed and the oldest log + // will be cleaned, leaving only the newest log. + ProcedureTestingUtility.restart(procExecutor, true, false); + LOG.info("finish to restart2"); + // There should be two active logs + assertTrue(procStore.getActiveLogs().size() == 2); + procExecutor.startWorkers(); + + } + + @Test + public void testProcedureDeletedShouldClean() throws Exception { + createProcExecutor(); + WaitProcedure waitProcedure = new WaitProcedure(); + long waitProce = procExecutor.submitProcedure(waitProcedure); + LOG.info("Begin to execute " + waitProce); + Thread.sleep(500); + LOG.info("begin to restart1 "); + ProcedureTestingUtility.restart(procExecutor, true); + LOG.info("finish to restart1 "); + htu.waitFor(10000, () -> procExecutor.getProcedure(waitProce) != null); + // Wait until the suspendProc executed after restart + waitProcedure = (WaitProcedure) procExecutor.getProcedure(waitProce); + waitProcedure.latch.countDown(); + Thread.sleep(500); + // Should be 1 log since the suspendProcedure is updated in the new log + assertTrue(procStore.getActiveLogs().size() == 1); + // restart procExecutor + LOG.info("begin to restart2"); + // Restart the executor but do not start the workers. + // Otherwise, the suspendProcedure will soon be executed and the oldest log + // will be cleaned, leaving only the newest log. + ProcedureTestingUtility.restart(procExecutor, true, false); + LOG.info("finish to restart2"); + // There should be two active logs + assertTrue(procStore.getActiveLogs().size() == 2); + procExecutor.startWorkers(); + } + + private void corrupt(FileStatus file) throws IOException { + LOG.info("Corrupt " + file); + Path tmpFile = file.getPath().suffix(".tmp"); + // remove the last byte to make the trailer corrupted + try (FSDataInputStream in = fs.open(file.getPath()); + FSDataOutputStream out = fs.create(tmpFile)) { + ByteStreams.copy(ByteStreams.limit(in, file.getLen() - 1), out); + } + fs.delete(file.getPath(), false); + fs.rename(tmpFile, file.getPath()); + } + + + public static final class ExchangeProcedure extends ProcedureTestingUtility.NoopProcedure { + + private final Exchanger exchanger = new Exchanger<>(); + + @SuppressWarnings("unchecked") + @Override + protected Procedure[] execute(Void env) + throws ProcedureYieldException, ProcedureSuspendedException, InterruptedException { + if (exchanger.exchange(Boolean.TRUE)) { + return new Procedure[] { this }; + } else { + return null; + } + } + } + + @Test + public void testResetDeleteWhenBuildingHoldingCleanupTracker() throws Exception { + createProcExecutor(); + ExchangeProcedure proc1 = new ExchangeProcedure(); + ExchangeProcedure proc2 = new ExchangeProcedure(); + procExecutor.submitProcedure(proc1); + long procId2 = procExecutor.submitProcedure(proc2); + Thread.sleep(500); + procStore.rollWriterForTesting(); + proc1.exchanger.exchange(Boolean.TRUE); + Thread.sleep(500); + + FileStatus[] walFiles = fs.listStatus(logDir); + Arrays.sort(walFiles, (f1, f2) -> f1.getPath().getName().compareTo(f2.getPath().getName())); + // corrupt the first proc wal file, so we will have a partial tracker for it after restarting + corrupt(walFiles[0]); + ProcedureTestingUtility.restart(procExecutor, false, true); + // also update proc2, which means that all the procedures in the first proc wal have been + // updated and it should be deleted. + proc2 = (ExchangeProcedure) procExecutor.getProcedure(procId2); + proc2.exchanger.exchange(Boolean.TRUE); + htu.waitFor(10000, () -> !fs.exists(walFiles[0].getPath())); + } + + public static class WaitProcedure extends ProcedureTestingUtility.NoopProcedure { + public WaitProcedure() { + super(); + } + + private CountDownLatch latch = new CountDownLatch(1); + + @Override + protected Procedure[] execute(Void env) throws ProcedureSuspendedException { + // Always wait here + LOG.info("wait here"); + try { + latch.await(); + } catch (Throwable t) { + + } + LOG.info("finished"); + return null; + } + } + + public static class SuspendProcedure extends ProcedureTestingUtility.NoopProcedure { + public SuspendProcedure() { + super(); + } + + private CountDownLatch latch = new CountDownLatch(1); + + @Override + protected Procedure[] execute(Void env) throws ProcedureSuspendedException { + // Always suspend the procedure + LOG.info("suspend here"); + latch.countDown(); + throw new ProcedureSuspendedException(); + } + } + + public static class RootProcedure extends ProcedureTestingUtility.NoopProcedure { + private boolean childSpwaned = false; + + public RootProcedure() { + super(); + } + + @Override + protected Procedure[] execute(Void env) throws ProcedureSuspendedException { + if (!childSpwaned) { + childSpwaned = true; + return new Procedure[] { new SuspendProcedure() }; + } else { + return null; + } + } + } +} diff --git a/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestProcedureExecution.java b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestProcedureExecution.java index a3cff582fd9f..e734bcb93d4b 100644 --- a/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestProcedureExecution.java +++ b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestProcedureExecution.java @@ -52,7 +52,7 @@ public class TestProcedureExecution { private static final Logger LOG = LoggerFactory.getLogger(TestProcedureExecution.class); private static final int PROCEDURE_EXECUTOR_SLOTS = 1; - private static final Procedure NULL_PROC = null; + private static final Procedure NULL_PROC = null; private ProcedureExecutor procExecutor; private ProcedureStore procStore; @@ -84,11 +84,16 @@ public void tearDown() throws IOException { } private static class TestProcedureException extends IOException { - public TestProcedureException(String msg) { super(msg); } + + private static final long serialVersionUID = 8798565784658913798L; + + public TestProcedureException(String msg) { + super(msg); + } } public static class TestSequentialProcedure extends SequentialProcedure { - private final Procedure[] subProcs; + private final Procedure[] subProcs; private final List state; private final Exception failure; private final String name; @@ -112,7 +117,7 @@ public TestSequentialProcedure(String name, List state, Exception failur } @Override - protected Procedure[] execute(Void env) { + protected Procedure[] execute(Void env) { state.add(name + "-execute"); if (failure != null) { setFailure(new RemoteProcedureException(name + "-failure", failure)); @@ -136,9 +141,9 @@ protected boolean abort(Void env) { @Test public void testBadSubprocList() { List state = new ArrayList<>(); - Procedure subProc2 = new TestSequentialProcedure("subProc2", state); - Procedure subProc1 = new TestSequentialProcedure("subProc1", state, subProc2, NULL_PROC); - Procedure rootProc = new TestSequentialProcedure("rootProc", state, subProc1); + Procedure subProc2 = new TestSequentialProcedure("subProc2", state); + Procedure subProc1 = new TestSequentialProcedure("subProc1", state, subProc2, NULL_PROC); + Procedure rootProc = new TestSequentialProcedure("rootProc", state, subProc1); long rootId = ProcedureTestingUtility.submitAndWait(procExecutor, rootProc); // subProc1 has a "null" subprocedure which is catched as InvalidArgument @@ -158,9 +163,9 @@ public void testBadSubprocList() { @Test public void testSingleSequentialProc() { List state = new ArrayList<>(); - Procedure subProc2 = new TestSequentialProcedure("subProc2", state); - Procedure subProc1 = new TestSequentialProcedure("subProc1", state, subProc2); - Procedure rootProc = new TestSequentialProcedure("rootProc", state, subProc1); + Procedure subProc2 = new TestSequentialProcedure("subProc2", state); + Procedure subProc1 = new TestSequentialProcedure("subProc1", state, subProc2); + Procedure rootProc = new TestSequentialProcedure("rootProc", state, subProc1); long rootId = ProcedureTestingUtility.submitAndWait(procExecutor, rootProc); // successful state, with 3 execute @@ -173,10 +178,10 @@ public void testSingleSequentialProc() { @Test public void testSingleSequentialProcRollback() { List state = new ArrayList<>(); - Procedure subProc2 = new TestSequentialProcedure("subProc2", state, - new TestProcedureException("fail test")); - Procedure subProc1 = new TestSequentialProcedure("subProc1", state, subProc2); - Procedure rootProc = new TestSequentialProcedure("rootProc", state, subProc1); + Procedure subProc2 = + new TestSequentialProcedure("subProc2", state, new TestProcedureException("fail test")); + Procedure subProc1 = new TestSequentialProcedure("subProc1", state, subProc2); + Procedure rootProc = new TestSequentialProcedure("rootProc", state, subProc1); long rootId = ProcedureTestingUtility.submitAndWait(procExecutor, rootProc); // the 3rd proc fail, rollback after 2 successful execution @@ -203,7 +208,7 @@ public static class TestFaultyRollback extends SequentialProcedure { public TestFaultyRollback() { } @Override - protected Procedure[] execute(Void env) { + protected Procedure[] execute(Void env) { setFailure("faulty-rollback-test", new TestProcedureException("test faulty rollback")); return null; } @@ -249,7 +254,7 @@ public TestWaitingProcedure(String name, List state, boolean hasChild) { } @Override - protected Procedure[] execute(Void env) { + protected Procedure[] execute(Void env) { state.add(name + "-execute"); setState(ProcedureState.WAITING_TIMEOUT); return hasChild ? new Procedure[] { new TestWaitChild(name, state) } : null; @@ -280,14 +285,14 @@ public TestWaitChild(String name, List state) { } @Override - protected Procedure[] execute(Void env) { + protected Procedure[] execute(Void env) { state.add(name + "-child-execute"); return null; } @Override protected void rollback(Void env) { - state.add(name + "-child-rollback"); + throw new UnsupportedOperationException("should not rollback a successful child procedure"); } @Override @@ -302,7 +307,7 @@ protected boolean abort(Void env) { public void testAbortTimeout() { final int PROC_TIMEOUT_MSEC = 2500; List state = new ArrayList<>(); - Procedure proc = new TestWaitingProcedure("wproc", state, false); + Procedure proc = new TestWaitingProcedure("wproc", state, false); proc.setTimeout(PROC_TIMEOUT_MSEC); long startTime = EnvironmentEdgeManager.currentTime(); long rootId = ProcedureTestingUtility.submitAndWait(procExecutor, proc); @@ -320,17 +325,16 @@ public void testAbortTimeout() { @Test public void testAbortTimeoutWithChildren() { List state = new ArrayList<>(); - Procedure proc = new TestWaitingProcedure("wproc", state, true); + Procedure proc = new TestWaitingProcedure("wproc", state, true); proc.setTimeout(2500); long rootId = ProcedureTestingUtility.submitAndWait(procExecutor, proc); LOG.info(Objects.toString(state)); Procedure result = procExecutor.getResult(rootId); assertTrue(state.toString(), result.isFailed()); ProcedureTestingUtility.assertIsTimeoutException(result); - assertEquals(state.toString(), 4, state.size()); + assertEquals(state.toString(), 3, state.size()); assertEquals("wproc-execute", state.get(0)); assertEquals("wproc-child-execute", state.get(1)); - assertEquals("wproc-child-rollback", state.get(2)); - assertEquals("wproc-rollback", state.get(3)); + assertEquals("wproc-rollback", state.get(2)); } } diff --git a/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestProcedureRollbackAIOOB.java b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestProcedureRollbackAIOOB.java new file mode 100644 index 000000000000..fc12924244e1 --- /dev/null +++ b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestProcedureRollbackAIOOB.java @@ -0,0 +1,117 @@ +/** + * 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.hadoop.hbase.procedure2; + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.HBaseCommonTestingUtility; +import org.apache.hadoop.hbase.procedure2.ProcedureTestingUtility.NoopProcedure; +import org.apache.hadoop.hbase.procedure2.store.wal.WALProcedureStore; +import org.apache.hadoop.hbase.testclassification.MasterTests; +import org.apache.hadoop.hbase.testclassification.MediumTests; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.rules.TestName; + +/** + * Testcase for HBASE-20973 + */ +@Category({ MasterTests.class, MediumTests.class }) +public class TestProcedureRollbackAIOOB { + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestProcedureRollbackAIOOB.class); + + private static final HBaseCommonTestingUtility UTIL = new HBaseCommonTestingUtility(); + + public static final class ParentProcedure extends NoopProcedure { + + private final CountDownLatch latch = new CountDownLatch(1); + + private boolean scheduled; + + @Override + protected Procedure[] execute(Void env) + throws ProcedureYieldException, ProcedureSuspendedException, InterruptedException { + latch.await(); + if (scheduled) { + return null; + } + scheduled = true; + return new Procedure[] { new SubProcedure() }; + } + } + + public static final class SubProcedure extends NoopProcedure { + + @Override + protected Procedure[] execute(Void env) + throws ProcedureYieldException, ProcedureSuspendedException, InterruptedException { + setFailure("Inject error", new RuntimeException("Inject error")); + return null; + } + } + + private WALProcedureStore procStore; + + private ProcedureExecutor procExec; + + @Rule + public final TestName name = new TestName(); + + @Before + public void setUp() throws IOException { + procStore = ProcedureTestingUtility.createWalStore(UTIL.getConfiguration(), + UTIL.getDataTestDir(name.getMethodName())); + procStore.start(2); + procExec = new ProcedureExecutor(UTIL.getConfiguration(), null, procStore); + ProcedureTestingUtility.initAndStartWorkers(procExec, 2, true); + } + + @After + public void tearDown() { + procExec.stop(); + procStore.stop(false); + } + + @AfterClass + public static void tearDownAfterClass() throws IOException { + UTIL.cleanupTestDir(); + } + + @Test + public void testArrayIndexOutOfBounds() { + ParentProcedure proc = new ParentProcedure(); + long procId = procExec.submitProcedure(proc); + long noopProcId = -1L; + // make sure that the sub procedure will have a new BitSetNode + for (int i = 0; i < Long.SIZE - 2; i++) { + noopProcId = procExec.submitProcedure(new NoopProcedure<>()); + } + final long lastNoopProcId = noopProcId; + UTIL.waitFor(30000, () -> procExec.isFinished(lastNoopProcId)); + proc.latch.countDown(); + UTIL.waitFor(10000, () -> procExec.isFinished(procId)); + } +} diff --git a/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestProcedureSkipPersistence.java b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestProcedureSkipPersistence.java new file mode 100644 index 000000000000..8293dcb4ed34 --- /dev/null +++ b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestProcedureSkipPersistence.java @@ -0,0 +1,163 @@ +/** + * 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.hadoop.hbase.procedure2; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.HBaseCommonTestingUtility; +import org.apache.hadoop.hbase.procedure2.store.ProcedureStore; +import org.apache.hadoop.hbase.testclassification.MasterTests; +import org.apache.hadoop.hbase.testclassification.MediumTests; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos; + +@Category({ MasterTests.class, MediumTests.class }) +public class TestProcedureSkipPersistence { + + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestProcedureSkipPersistence.class); + private ProcedureExecutor procExecutor; + private ProcedureStore procStore; + + private HBaseCommonTestingUtility htu; + private FileSystem fs; + private Path testDir; + private Path logDir; + + private static volatile int STEP = 0; + + public class ProcEnv { + + public ProcedureExecutor getProcedureExecutor() { + return procExecutor; + } + } + + public static class TestProcedure extends Procedure { + + // need to override this method, otherwise we will persist the release lock operation and the + // test will fail. + @Override + protected boolean holdLock(ProcEnv env) { + return true; + } + + @Override + protected Procedure[] execute(ProcEnv env) + throws ProcedureYieldException, ProcedureSuspendedException, InterruptedException { + if (STEP == 0) { + STEP = 1; + setTimeout(60 * 60 * 1000); + setState(ProcedureProtos.ProcedureState.WAITING_TIMEOUT); + skipPersistence(); + throw new ProcedureSuspendedException(); + } else if (STEP == 1) { + STEP = 2; + if (hasTimeout()) { + setFailure("Should not persist the timeout value", + new IOException("Should not persist the timeout value")); + return null; + } + setTimeout(2 * 1000); + setState(ProcedureProtos.ProcedureState.WAITING_TIMEOUT); + // used to confirm that we reset the persist flag before execution + throw new ProcedureSuspendedException(); + } else { + if (!hasTimeout()) { + setFailure("Should have persisted the timeout value", + new IOException("Should have persisted the timeout value")); + } + return null; + } + } + + @Override + protected synchronized boolean setTimeoutFailure(ProcEnv env) { + setState(ProcedureProtos.ProcedureState.RUNNABLE); + env.getProcedureExecutor().getProcedureScheduler().addFront(this); + return false; + } + + @Override + protected void rollback(ProcEnv env) throws IOException, InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + protected boolean abort(ProcEnv env) { + return false; + } + + @Override + protected void serializeStateData(ProcedureStateSerializer serializer) throws IOException { + } + + @Override + protected void deserializeStateData(ProcedureStateSerializer serializer) throws IOException { + } + } + + @Before + public void setUp() throws IOException { + htu = new HBaseCommonTestingUtility(); + testDir = htu.getDataTestDir(); + fs = testDir.getFileSystem(htu.getConfiguration()); + assertTrue(testDir.depth() > 1); + + logDir = new Path(testDir, "proc-logs"); + procStore = ProcedureTestingUtility.createWalStore(htu.getConfiguration(), logDir); + procExecutor = new ProcedureExecutor<>(htu.getConfiguration(), new ProcEnv(), procStore); + procStore.start(1); + ProcedureTestingUtility.initAndStartWorkers(procExecutor, 1, true); + } + + @After + public void tearDown() throws IOException { + procExecutor.stop(); + procStore.stop(false); + fs.delete(logDir, true); + } + + @Test + public void test() throws Exception { + TestProcedure proc = new TestProcedure(); + long procId = procExecutor.submitProcedure(proc); + htu.waitFor(30000, () -> proc.isWaiting()); + ProcedureTestingUtility.restart(procExecutor); + htu.waitFor(30000, () -> { + Procedure p = procExecutor.getProcedure(procId); + return p.isWaiting() || p.isFinished(); + }); + assertFalse(procExecutor.isFinished(procId)); + ProcedureTestingUtility.restart(procExecutor); + htu.waitFor(30000, () -> procExecutor.isFinished(procId)); + Procedure p = procExecutor.getResult(procId); + assertTrue(p.isSuccess()); + } +} diff --git a/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestProcedureUtil.java b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestProcedureUtil.java index 6342beccae01..3629fb74bc6c 100644 --- a/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestProcedureUtil.java +++ b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestProcedureUtil.java @@ -18,6 +18,7 @@ package org.apache.hadoop.hbase.procedure2; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import java.util.concurrent.TimeUnit; import org.apache.hadoop.hbase.HBaseClassTestRule; @@ -63,9 +64,17 @@ public void testGetBackoffTimeMs() { for (int i = 30; i < 1000; i++) { assertEquals(TimeUnit.MINUTES.toMillis(10), ProcedureUtil.getBackoffTimeMs(30)); } - assertEquals(1000, ProcedureUtil.getBackoffTimeMs(0)); - assertEquals(2000, ProcedureUtil.getBackoffTimeMs(1)); - assertEquals(32000, ProcedureUtil.getBackoffTimeMs(5)); + long backoffTimeMs = ProcedureUtil.getBackoffTimeMs(0); + assertTrue(backoffTimeMs >= 1000); + assertTrue(backoffTimeMs <= 1000 * 1.01f); + + backoffTimeMs = ProcedureUtil.getBackoffTimeMs(1); + assertTrue(backoffTimeMs >= 2000); + assertTrue(backoffTimeMs <= 2000 * 1.01f); + + backoffTimeMs = ProcedureUtil.getBackoffTimeMs(5); + assertTrue(backoffTimeMs >= 32000); + assertTrue(backoffTimeMs <= 32000 * 1.01f); } public static class TestProcedureNoDefaultConstructor extends TestProcedure { diff --git a/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestStateMachineProcedure.java b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestStateMachineProcedure.java index b1ed5fbff0be..a2c385705a66 100644 --- a/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestStateMachineProcedure.java +++ b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/TestStateMachineProcedure.java @@ -48,6 +48,9 @@ public class TestStateMachineProcedure { private static final Logger LOG = LoggerFactory.getLogger(TestStateMachineProcedure.class); private static final Exception TEST_FAILURE_EXCEPTION = new Exception("test failure") { + + private static final long serialVersionUID = 2147942238987041310L; + @Override public boolean equals(final Object other) { if (this == other) return true; @@ -192,6 +195,11 @@ protected Flow executeFromState(TestProcEnv env, TestSMProcedureState state) { return Flow.HAS_MORE_STATE; } + @Override + protected boolean isRollbackSupported(TestSMProcedureState state) { + return true; + } + @Override protected void rollbackState(TestProcEnv env, TestSMProcedureState state) { LOG.info("ROLLBACK " + state + " " + this); @@ -274,7 +282,7 @@ protected void rollback(final TestProcEnv env) public static class SimpleChildProcedure extends NoopProcedure { @Override - protected Procedure[] execute(TestProcEnv env) { + protected Procedure[] execute(TestProcEnv env) { LOG.info("EXEC " + this); env.execCount.incrementAndGet(); if (env.triggerChildRollback) { diff --git a/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/store/TestBitSetNode.java b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/store/TestBitSetNode.java new file mode 100644 index 000000000000..8d193d029b4c --- /dev/null +++ b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/store/TestBitSetNode.java @@ -0,0 +1,59 @@ +/** + * 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.hadoop.hbase.procedure2.store; + +import static org.junit.Assert.assertEquals; + +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.procedure2.Procedure; +import org.apache.hadoop.hbase.testclassification.MasterTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +@Category({ MasterTests.class, SmallTests.class }) +public class TestBitSetNode { + + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestBitSetNode.class); + + @Test + public void testGetActiveMaxMinProcId() { + BitSetNode node = new BitSetNode(5L, false); + assertEquals(5L, node.getActiveMinProcId()); + assertEquals(5L, node.getActiveMaxProcId()); + node.insertOrUpdate(10L); + assertEquals(5L, node.getActiveMinProcId()); + assertEquals(10L, node.getActiveMaxProcId()); + node.insertOrUpdate(1L); + assertEquals(1L, node.getActiveMinProcId()); + assertEquals(10L, node.getActiveMaxProcId()); + + node.delete(10L); + assertEquals(1L, node.getActiveMinProcId()); + assertEquals(5L, node.getActiveMaxProcId()); + node.delete(1L); + assertEquals(5L, node.getActiveMinProcId()); + assertEquals(5L, node.getActiveMaxProcId()); + node.delete(5L); + assertEquals(Procedure.NO_PROC_ID, node.getActiveMinProcId()); + assertEquals(Procedure.NO_PROC_ID, node.getActiveMaxProcId()); + } +} diff --git a/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/store/TestProcedureStoreTracker.java b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/store/TestProcedureStoreTracker.java index ffc6ab8de0da..ab448be89734 100644 --- a/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/store/TestProcedureStoreTracker.java +++ b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/store/TestProcedureStoreTracker.java @@ -23,7 +23,7 @@ import java.util.Random; import org.apache.hadoop.hbase.HBaseClassTestRule; -import org.apache.hadoop.hbase.procedure2.store.ProcedureStoreTracker.BitSetNode; +import org.apache.hadoop.hbase.procedure2.Procedure; import org.apache.hadoop.hbase.testclassification.MasterTests; import org.apache.hadoop.hbase.testclassification.MediumTests; import org.junit.ClassRule; @@ -119,29 +119,29 @@ public void testBasicCRUD() { tracker.insert(procs[0]); tracker.insert(procs[1], new long[] { procs[2], procs[3], procs[4] }); assertFalse(tracker.isEmpty()); - assertTrue(tracker.isUpdated()); + assertTrue(tracker.isAllModified()); - tracker.resetUpdates(); - assertFalse(tracker.isUpdated()); + tracker.resetModified(); + assertFalse(tracker.isAllModified()); for (int i = 0; i < 4; ++i) { tracker.update(procs[i]); assertFalse(tracker.isEmpty()); - assertFalse(tracker.isUpdated()); + assertFalse(tracker.isAllModified()); } tracker.update(procs[4]); assertFalse(tracker.isEmpty()); - assertTrue(tracker.isUpdated()); + assertTrue(tracker.isAllModified()); tracker.update(procs[5]); assertFalse(tracker.isEmpty()); - assertTrue(tracker.isUpdated()); + assertTrue(tracker.isAllModified()); for (int i = 0; i < 5; ++i) { tracker.delete(procs[i]); assertFalse(tracker.isEmpty()); - assertTrue(tracker.isUpdated()); + assertTrue(tracker.isAllModified()); } tracker.delete(procs[5]); assertTrue(tracker.isEmpty()); @@ -218,54 +218,8 @@ public void testDelete() { } } - boolean isDeleted(ProcedureStoreTracker n, long procId) { - return n.isDeleted(procId) == ProcedureStoreTracker.DeleteState.YES; - } - - boolean isDeleted(BitSetNode n, long procId) { - return n.isDeleted(procId) == ProcedureStoreTracker.DeleteState.YES; - } - - /** - * @param active list of active proc ids. To mark them as non-deleted, since by default a proc - * id is always marked deleted. - */ - ProcedureStoreTracker buildTracker(long[] active, long[] updated, long[] deleted) { - ProcedureStoreTracker tracker = new ProcedureStoreTracker(); - for (long i : active) { - tracker.insert(i); - } - tracker.resetUpdates(); - for (long i : updated) { - tracker.update(i); - } - for (long i : deleted) { - tracker.delete(i); - } - return tracker; - } - - /** - * @param active list of active proc ids. To mark them as non-deleted, since by default a proc - * id is always marked deleted. - */ - BitSetNode buildBitSetNode(long[] active, long[] updated, long[] deleted) { - BitSetNode bitSetNode = new BitSetNode(0L, false); - for (long i : active) { - bitSetNode.update(i); - } - bitSetNode.resetUpdates(); - for (long i : updated) { - bitSetNode.update(i); - } - for (long i : deleted) { - bitSetNode.delete(i); - } - return bitSetNode; - } - @Test - public void testSetDeletedIfSet() { + public void testSetDeletedIfModified() { final ProcedureStoreTracker tracker = new ProcedureStoreTracker(); final long[] procIds = new long[] { 1, 3, 7, 152, 512, 1024, 1025 }; @@ -276,9 +230,9 @@ public void testSetDeletedIfSet() { assertEquals(false, tracker.isEmpty()); for (int i = 0; i < procIds.length; ++i) { - tracker.setDeletedIfSet(procIds[i] - 1); - tracker.setDeletedIfSet(procIds[i]); - tracker.setDeletedIfSet(procIds[i] + 1); + tracker.setDeletedIfModified(procIds[i] - 1); + tracker.setDeletedIfModified(procIds[i]); + tracker.setDeletedIfModified(procIds[i] + 1); } assertEquals(true, tracker.isEmpty()); @@ -289,7 +243,37 @@ public void testSetDeletedIfSet() { } assertEquals(false, tracker.isEmpty()); - tracker.setDeletedIfSet(procIds); + tracker.setDeletedIfModified(procIds); assertEquals(true, tracker.isEmpty()); } + + @Test + public void testGetActiveProcIds() { + ProcedureStoreTracker tracker = new ProcedureStoreTracker(); + for (int i = 0; i < 10000; i++) { + tracker.insert(i * 10); + } + for (int i = 0; i < 10000; i += 2) { + tracker.delete(i * 10); + } + long[] activeProcIds = tracker.getAllActiveProcIds(); + assertEquals(5000, activeProcIds.length); + for (int i = 0; i < 5000; i++) { + assertEquals((2 * i + 1) * 10, activeProcIds[i]); + } + } + + @Test + public void testGetActiveMinProcId() { + ProcedureStoreTracker tracker = new ProcedureStoreTracker(); + assertEquals(Procedure.NO_PROC_ID, tracker.getActiveMinProcId()); + for (int i = 100; i < 1000; i = 2 * i + 1) { + tracker.insert(i); + } + for (int i = 100; i < 1000; i = 2 * i + 1) { + assertEquals(i, tracker.getActiveMinProcId()); + tracker.delete(i); + } + assertEquals(Procedure.NO_PROC_ID, tracker.getActiveMinProcId()); + } } diff --git a/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/store/wal/TestForceUpdateProcedure.java b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/store/wal/TestForceUpdateProcedure.java new file mode 100644 index 000000000000..df6ee51efe3f --- /dev/null +++ b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/store/wal/TestForceUpdateProcedure.java @@ -0,0 +1,246 @@ +/** + * 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.hadoop.hbase.procedure2.store.wal; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Exchanger; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.HBaseCommonTestingUtility; +import org.apache.hadoop.hbase.procedure2.Procedure; +import org.apache.hadoop.hbase.procedure2.ProcedureExecutor; +import org.apache.hadoop.hbase.procedure2.ProcedureStateSerializer; +import org.apache.hadoop.hbase.procedure2.ProcedureSuspendedException; +import org.apache.hadoop.hbase.procedure2.ProcedureTestingUtility; +import org.apache.hadoop.hbase.procedure2.ProcedureYieldException; +import org.apache.hadoop.hbase.testclassification.MasterTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos.ProcedureState; + +@Category({ MasterTests.class, SmallTests.class }) +public class TestForceUpdateProcedure { + + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestForceUpdateProcedure.class); + + private static HBaseCommonTestingUtility UTIL = new HBaseCommonTestingUtility(); + + private static WALProcedureStore STORE; + + private static ProcedureExecutor EXEC; + + private static Exchanger EXCHANGER = new Exchanger<>(); + + private static int WAL_COUNT = 5; + + private static void createStoreAndExecutor() throws IOException { + Path logDir = UTIL.getDataTestDir("proc-wals"); + STORE = ProcedureTestingUtility.createWalStore(UTIL.getConfiguration(), logDir); + STORE.start(1); + EXEC = new ProcedureExecutor(UTIL.getConfiguration(), null, STORE); + ProcedureTestingUtility.initAndStartWorkers(EXEC, 1, true); + } + + @BeforeClass + public static void setUp() throws IOException { + UTIL.getConfiguration().setInt(WALProcedureStore.WAL_COUNT_WARN_THRESHOLD_CONF_KEY, WAL_COUNT); + createStoreAndExecutor(); + } + + private static void stopStoreAndExecutor() { + EXEC.stop(); + STORE.stop(false); + EXEC = null; + STORE = null; + } + + @AfterClass + public static void tearDown() throws IOException { + stopStoreAndExecutor(); + UTIL.cleanupTestDir(); + } + + public static final class WaitingProcedure extends Procedure { + + @Override + protected Procedure[] execute(Void env) + throws ProcedureYieldException, ProcedureSuspendedException, InterruptedException { + EXCHANGER.exchange(Boolean.TRUE); + setState(ProcedureState.WAITING_TIMEOUT); + setTimeout(Integer.MAX_VALUE); + throw new ProcedureSuspendedException(); + } + + @Override + protected void rollback(Void env) throws IOException, InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + protected boolean abort(Void env) { + return false; + } + + @Override + protected void serializeStateData(ProcedureStateSerializer serializer) throws IOException { + } + + @Override + protected void deserializeStateData(ProcedureStateSerializer serializer) throws IOException { + } + } + + public static final class ParentProcedure extends Procedure { + + @SuppressWarnings("unchecked") + @Override + protected Procedure[] execute(Void env) + throws ProcedureYieldException, ProcedureSuspendedException, InterruptedException { + return new Procedure[] { new DummyProcedure(), new WaitingProcedure() }; + } + + @Override + protected void rollback(Void env) throws IOException, InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + protected boolean abort(Void env) { + return false; + } + + @Override + protected void serializeStateData(ProcedureStateSerializer serializer) throws IOException { + } + + @Override + protected void deserializeStateData(ProcedureStateSerializer serializer) throws IOException { + } + } + + public static final class DummyProcedure extends Procedure { + + @Override + protected Procedure[] execute(Void env) + throws ProcedureYieldException, ProcedureSuspendedException, InterruptedException { + return null; + } + + @Override + protected void rollback(Void env) throws IOException, InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + protected boolean abort(Void env) { + return false; + } + + @Override + protected void serializeStateData(ProcedureStateSerializer serializer) throws IOException { + } + + @Override + protected void deserializeStateData(ProcedureStateSerializer serializer) throws IOException { + } + } + + public static final class ExchangeProcedure extends Procedure { + + @SuppressWarnings("unchecked") + @Override + protected Procedure[] execute(Void env) + throws ProcedureYieldException, ProcedureSuspendedException, InterruptedException { + if (EXCHANGER.exchange(Boolean.TRUE)) { + return new Procedure[] { this }; + } else { + return null; + } + } + + @Override + protected void rollback(Void env) throws IOException, InterruptedException { + throw new UnsupportedOperationException(); + } + + @Override + protected boolean abort(Void env) { + return false; + } + + @Override + protected void serializeStateData(ProcedureStateSerializer serializer) throws IOException { + } + + @Override + protected void deserializeStateData(ProcedureStateSerializer serializer) throws IOException { + } + } + + @Test + public void test() throws IOException, InterruptedException { + EXEC.submitProcedure(new ParentProcedure()); + EXCHANGER.exchange(Boolean.TRUE); + UTIL.waitFor(10000, () -> EXEC.getActiveExecutorCount() == 0); + // The above operations are used to make sure that we have persist the states of the two + // procedures. + long procId = EXEC.submitProcedure(new ExchangeProcedure()); + assertEquals(1, STORE.getActiveLogs().size()); + for (int i = 0; i < WAL_COUNT - 1; i++) { + assertTrue(STORE.rollWriterForTesting()); + // The WaitinProcedure never gets updated so we can not delete the oldest wal file, so the + // number of wal files will increase + assertEquals(2 + i, STORE.getActiveLogs().size()); + EXCHANGER.exchange(Boolean.TRUE); + Thread.sleep(1000); + } + STORE.rollWriterForTesting(); + // Finish the ExchangeProcedure + EXCHANGER.exchange(Boolean.FALSE); + // Make sure that we can delete several wal files because we force update the state of + // WaitingProcedure. Notice that the last closed wal files can not be deleted, as when rolling + // the newest wal file does not have anything in it, and in the closed file we still have the + // state for the ExchangeProcedure so it can not be deleted + UTIL.waitFor(10000, () -> STORE.getActiveLogs().size() <= 2); + UTIL.waitFor(10000, () -> EXEC.isFinished(procId)); + // Make sure that after the force update we could still load the procedures + stopStoreAndExecutor(); + createStoreAndExecutor(); + Map, Procedure> procMap = new HashMap<>(); + EXEC.getActiveProceduresNoCopy().forEach(p -> procMap.put(p.getClass(), p)); + assertEquals(3, procMap.size()); + ParentProcedure parentProc = (ParentProcedure) procMap.get(ParentProcedure.class); + assertEquals(ProcedureState.WAITING, parentProc.getState()); + WaitingProcedure waitingProc = (WaitingProcedure) procMap.get(WaitingProcedure.class); + assertEquals(ProcedureState.WAITING_TIMEOUT, waitingProc.getState()); + DummyProcedure dummyProc = (DummyProcedure) procMap.get(DummyProcedure.class); + assertEquals(ProcedureState.SUCCESS, dummyProc.getState()); + } +} diff --git a/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/store/wal/TestStressWALProcedureStore.java b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/store/wal/TestStressWALProcedureStore.java index 443386d0845f..da53fa5d0bb7 100644 --- a/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/store/wal/TestStressWALProcedureStore.java +++ b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/store/wal/TestStressWALProcedureStore.java @@ -18,9 +18,7 @@ package org.apache.hadoop.hbase.procedure2.store.wal; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import java.io.IOException; import java.util.Random; diff --git a/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/store/wal/TestWALProcedureStore.java b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/store/wal/TestWALProcedureStore.java index b1bd254b8006..0f598b0df8ea 100644 --- a/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/store/wal/TestWALProcedureStore.java +++ b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/store/wal/TestWALProcedureStore.java @@ -20,7 +20,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import java.io.FileNotFoundException; import java.io.IOException; @@ -44,7 +43,6 @@ import org.apache.hadoop.hbase.procedure2.ProcedureTestingUtility.TestProcedure; import org.apache.hadoop.hbase.procedure2.SequentialProcedure; import org.apache.hadoop.hbase.procedure2.store.ProcedureStore; -import org.apache.hadoop.hbase.procedure2.store.ProcedureStore.ProcedureIterator; import org.apache.hadoop.hbase.procedure2.store.ProcedureStoreTracker; import org.apache.hadoop.hbase.testclassification.MasterTests; import org.apache.hadoop.hbase.testclassification.SmallTests; @@ -72,7 +70,6 @@ public class TestWALProcedureStore { private static final Logger LOG = LoggerFactory.getLogger(TestWALProcedureStore.class); private static final int PROCEDURE_STORE_SLOTS = 1; - private static final Procedure NULL_PROC = null; private WALProcedureStore procStore; @@ -153,7 +150,7 @@ public void trackersLoadedForAllOldLogs() throws Exception { @Test public void testWalCleanerSequentialClean() throws Exception { - final Procedure[] procs = new Procedure[5]; + final Procedure[] procs = new Procedure[5]; ArrayList logs = null; // Insert procedures and roll wal after every insert. @@ -182,7 +179,7 @@ public void testWalCleanerSequentialClean() throws Exception { // they are in the starting of the list. @Test public void testWalCleanerNoHoles() throws Exception { - final Procedure[] procs = new Procedure[5]; + final Procedure[] procs = new Procedure[5]; ArrayList logs = null; // Insert procedures and roll wal after every insert. for (int i = 0; i < procs.length; i++) { @@ -242,7 +239,7 @@ public void testWalCleanerUpdatesDontLeaveHoles() throws Exception { @Test public void testWalCleanerWithEmptyRolls() throws Exception { - final Procedure[] procs = new Procedure[3]; + final Procedure[] procs = new Procedure[3]; for (int i = 0; i < procs.length; ++i) { procs[i] = new TestSequentialProcedure(); procStore.insert(procs[i], null); @@ -284,12 +281,12 @@ public void testLoad() throws Exception { Set procIds = new HashSet<>(); // Insert something in the log - Procedure proc1 = new TestSequentialProcedure(); + Procedure proc1 = new TestSequentialProcedure(); procIds.add(proc1.getProcId()); procStore.insert(proc1, null); - Procedure proc2 = new TestSequentialProcedure(); - Procedure[] child2 = new Procedure[2]; + Procedure proc2 = new TestSequentialProcedure(); + Procedure[] child2 = new Procedure[2]; child2[0] = new TestSequentialProcedure(); child2[1] = new TestSequentialProcedure(); @@ -323,11 +320,11 @@ public void testLoad() throws Exception { @Test public void testNoTrailerDoubleRestart() throws Exception { // log-0001: proc 0, 1 and 2 are inserted - Procedure proc0 = new TestSequentialProcedure(); + Procedure proc0 = new TestSequentialProcedure(); procStore.insert(proc0, null); - Procedure proc1 = new TestSequentialProcedure(); + Procedure proc1 = new TestSequentialProcedure(); procStore.insert(proc1, null); - Procedure proc2 = new TestSequentialProcedure(); + Procedure proc2 = new TestSequentialProcedure(); procStore.insert(proc2, null); procStore.rollWriterForTesting(); @@ -420,19 +417,19 @@ public void testCorruptedTrailer() throws Exception { } private static void assertUpdated(final ProcedureStoreTracker tracker, - final Procedure[] procs, final int[] updatedProcs, final int[] nonUpdatedProcs) { + final Procedure[] procs, final int[] updatedProcs, final int[] nonUpdatedProcs) { for (int index : updatedProcs) { long procId = procs[index].getProcId(); - assertTrue("Procedure id : " + procId, tracker.isUpdated(procId)); + assertTrue("Procedure id : " + procId, tracker.isModified(procId)); } for (int index : nonUpdatedProcs) { long procId = procs[index].getProcId(); - assertFalse("Procedure id : " + procId, tracker.isUpdated(procId)); + assertFalse("Procedure id : " + procId, tracker.isModified(procId)); } } private static void assertDeleted(final ProcedureStoreTracker tracker, - final Procedure[] procs, final int[] deletedProcs, final int[] nonDeletedProcs) { + final Procedure[] procs, final int[] deletedProcs, final int[] nonDeletedProcs) { for (int index : deletedProcs) { long procId = procs[index].getProcId(); assertEquals("Procedure id : " + procId, @@ -447,7 +444,7 @@ private static void assertDeleted(final ProcedureStoreTracker tracker, @Test public void testCorruptedTrailersRebuild() throws Exception { - final Procedure[] procs = new Procedure[6]; + final Procedure[] procs = new Procedure[6]; for (int i = 0; i < procs.length; ++i) { procs[i] = new TestSequentialProcedure(); } @@ -575,127 +572,20 @@ public int compare(FileStatus o1, FileStatus o2) { storeRestart(loader); assertEquals(0, loader.getLoadedCount()); assertEquals(rootProcs.length, loader.getCorruptedCount()); - for (Procedure proc: loader.getCorrupted()) { + for (Procedure proc : loader.getCorrupted()) { assertTrue(proc.toString(), proc.getParentProcId() <= rootProcs.length); assertTrue(proc.toString(), - proc.getProcId() > rootProcs.length && - proc.getProcId() <= (rootProcs.length * 2)); + proc.getProcId() > rootProcs.length && proc.getProcId() <= (rootProcs.length * 2)); } } - @Test - public void testWalReplayOrder_AB_A() throws Exception { - /* - * | A B | -> | A | - */ - TestProcedure a = new TestProcedure(1, 0); - TestProcedure b = new TestProcedure(2, 1); - - procStore.insert(a, null); - a.addStackId(0); - procStore.update(a); - - procStore.insert(a, new Procedure[] { b }); - b.addStackId(1); - procStore.update(b); - - procStore.rollWriterForTesting(); - - a.addStackId(2); - procStore.update(a); - - storeRestart(new ProcedureStore.ProcedureLoader() { - @Override - public void setMaxProcId(long maxProcId) { - assertEquals(2, maxProcId); - } - - @Override - public void load(ProcedureIterator procIter) throws IOException { - assertTrue(procIter.hasNext()); - assertEquals(1, procIter.next().getProcId()); - assertTrue(procIter.hasNext()); - assertEquals(2, procIter.next().getProcId()); - assertFalse(procIter.hasNext()); - } - - @Override - public void handleCorrupted(ProcedureIterator procIter) throws IOException { - assertFalse(procIter.hasNext()); - } - }); - } - - @Test - public void testWalReplayOrder_ABC_BAD() throws Exception { - /* - * | A B C | -> | B A D | - */ - TestProcedure a = new TestProcedure(1, 0); - TestProcedure b = new TestProcedure(2, 1); - TestProcedure c = new TestProcedure(3, 2); - TestProcedure d = new TestProcedure(4, 0); - - procStore.insert(a, null); - a.addStackId(0); - procStore.update(a); - - procStore.insert(a, new Procedure[] { b }); - b.addStackId(1); - procStore.update(b); - - procStore.insert(b, new Procedure[] { c }); - b.addStackId(2); - procStore.update(b); - - procStore.rollWriterForTesting(); - - b.addStackId(3); - procStore.update(b); - - a.addStackId(4); - procStore.update(a); - - procStore.insert(d, null); - d.addStackId(0); - procStore.update(d); - - storeRestart(new ProcedureStore.ProcedureLoader() { - @Override - public void setMaxProcId(long maxProcId) { - assertEquals(4, maxProcId); - } - - @Override - public void load(ProcedureIterator procIter) throws IOException { - assertTrue(procIter.hasNext()); - assertEquals(4, procIter.next().getProcId()); - // TODO: This will be multiple call once we do fast-start - //assertFalse(procIter.hasNext()); - - assertTrue(procIter.hasNext()); - assertEquals(1, procIter.next().getProcId()); - assertTrue(procIter.hasNext()); - assertEquals(2, procIter.next().getProcId()); - assertTrue(procIter.hasNext()); - assertEquals(3, procIter.next().getProcId()); - assertFalse(procIter.hasNext()); - } - - @Override - public void handleCorrupted(ProcedureIterator procIter) throws IOException { - assertFalse(procIter.hasNext()); - } - }); - } - @Test public void testRollAndRemove() throws IOException { // Insert something in the log - Procedure proc1 = new TestSequentialProcedure(); + Procedure proc1 = new TestSequentialProcedure(); procStore.insert(proc1, null); - Procedure proc2 = new TestSequentialProcedure(); + Procedure proc2 = new TestSequentialProcedure(); procStore.insert(proc2, null); // roll the log, now we have 2 @@ -942,17 +832,6 @@ private void verifyProcIdsOnRestart(final Set procIds) throws Exception { assertEquals(0, loader.getCorruptedCount()); } - private void assertEmptyLogDir() { - try { - FileStatus[] status = fs.listStatus(logDir); - assertTrue("expected empty state-log dir", status == null || status.length == 0); - } catch (FileNotFoundException e) { - fail("expected the state-log dir to be present: " + logDir); - } catch (IOException e) { - fail("got en exception on state-log dir list: " + e.getMessage()); - } - } - public static class TestSequentialProcedure extends SequentialProcedure { private static long seqid = 0; @@ -961,13 +840,18 @@ public TestSequentialProcedure() { } @Override - protected Procedure[] execute(Void env) { return null; } + protected Procedure[] execute(Void env) { + return null; + } @Override - protected void rollback(Void env) { } + protected void rollback(Void env) { + } @Override - protected boolean abort(Void env) { return false; } + protected boolean abort(Void env) { + return false; + } @Override protected void serializeStateData(ProcedureStateSerializer serializer) diff --git a/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/store/wal/TestWALProcedureTree.java b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/store/wal/TestWALProcedureTree.java new file mode 100644 index 000000000000..890d0e3e9c0e --- /dev/null +++ b/hbase-procedure/src/test/java/org/apache/hadoop/hbase/procedure2/store/wal/TestWALProcedureTree.java @@ -0,0 +1,173 @@ +/** + * 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.hadoop.hbase.procedure2.store.wal; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.procedure2.Procedure; +import org.apache.hadoop.hbase.procedure2.ProcedureStateSerializer; +import org.apache.hadoop.hbase.procedure2.ProcedureSuspendedException; +import org.apache.hadoop.hbase.procedure2.ProcedureUtil; +import org.apache.hadoop.hbase.procedure2.ProcedureYieldException; +import org.apache.hadoop.hbase.procedure2.store.ProcedureStore.ProcedureIterator; +import org.apache.hadoop.hbase.testclassification.MasterTests; +import org.apache.hadoop.hbase.testclassification.SmallTests; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import org.apache.hadoop.hbase.shaded.protobuf.generated.ProcedureProtos; + +@Category({ MasterTests.class, SmallTests.class }) +public class TestWALProcedureTree { + + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestWALProcedureTree.class); + + public static final class TestProcedure extends Procedure { + + @Override + public void setProcId(long procId) { + super.setProcId(procId); + } + + @Override + public void setParentProcId(long parentProcId) { + super.setParentProcId(parentProcId); + } + + @Override + public synchronized void addStackIndex(int index) { + super.addStackIndex(index); + } + + @Override + protected Procedure[] execute(Void env) + throws ProcedureYieldException, ProcedureSuspendedException, InterruptedException { + return null; + } + + @Override + protected void rollback(Void env) throws IOException, InterruptedException { + } + + @Override + protected boolean abort(Void env) { + return false; + } + + @Override + protected void serializeStateData(ProcedureStateSerializer serializer) throws IOException { + } + + @Override + protected void deserializeStateData(ProcedureStateSerializer serializer) throws IOException { + } + } + + private TestProcedure createProc(long procId, long parentProcId) { + TestProcedure proc = new TestProcedure(); + proc.setProcId(procId); + if (parentProcId != Procedure.NO_PROC_ID) { + proc.setParentProcId(parentProcId); + } + return proc; + } + + private List toProtos(TestProcedure... procs) { + return Arrays.stream(procs).map(p -> { + try { + return ProcedureUtil.convertToProtoProcedure(p); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }).collect(Collectors.toList()); + } + + private List getProcs(ProcedureIterator iter) throws IOException { + List procs = new ArrayList<>(); + while (iter.hasNext()) { + procs.add((TestProcedure) iter.next()); + } + return procs; + } + + @Test + public void testMissingStackId() throws IOException { + TestProcedure proc0 = createProc(1, Procedure.NO_PROC_ID); + proc0.addStackIndex(0); + TestProcedure proc1 = createProc(2, 1); + proc1.addStackIndex(1); + TestProcedure proc2 = createProc(3, 2); + proc2.addStackIndex(3); + WALProcedureTree tree = WALProcedureTree.build(toProtos(proc0, proc1, proc2)); + List validProcs = getProcs(tree.getValidProcs()); + assertEquals(0, validProcs.size()); + List corruptedProcs = getProcs(tree.getCorruptedProcs()); + assertEquals(3, corruptedProcs.size()); + assertEquals(1, corruptedProcs.get(0).getProcId()); + assertEquals(2, corruptedProcs.get(1).getProcId()); + assertEquals(3, corruptedProcs.get(2).getProcId()); + } + + @Test + public void testDuplicatedStackId() throws IOException { + TestProcedure proc0 = createProc(1, Procedure.NO_PROC_ID); + proc0.addStackIndex(0); + TestProcedure proc1 = createProc(2, 1); + proc1.addStackIndex(1); + TestProcedure proc2 = createProc(3, 2); + proc2.addStackIndex(1); + WALProcedureTree tree = WALProcedureTree.build(toProtos(proc0, proc1, proc2)); + List validProcs = getProcs(tree.getValidProcs()); + assertEquals(0, validProcs.size()); + List corruptedProcs = getProcs(tree.getCorruptedProcs()); + assertEquals(3, corruptedProcs.size()); + assertEquals(1, corruptedProcs.get(0).getProcId()); + assertEquals(2, corruptedProcs.get(1).getProcId()); + assertEquals(3, corruptedProcs.get(2).getProcId()); + } + + @Test + public void testOrphan() throws IOException { + TestProcedure proc0 = createProc(1, Procedure.NO_PROC_ID); + proc0.addStackIndex(0); + TestProcedure proc1 = createProc(2, 1); + proc1.addStackIndex(1); + TestProcedure proc2 = createProc(3, Procedure.NO_PROC_ID); + proc2.addStackIndex(0); + TestProcedure proc3 = createProc(5, 4); + proc3.addStackIndex(1); + WALProcedureTree tree = WALProcedureTree.build(toProtos(proc0, proc1, proc2, proc3)); + List validProcs = getProcs(tree.getValidProcs()); + assertEquals(3, validProcs.size()); + List corruptedProcs = getProcs(tree.getCorruptedProcs()); + assertEquals(1, corruptedProcs.size()); + assertEquals(5, corruptedProcs.get(0).getProcId()); + assertEquals(4, corruptedProcs.get(0).getParentProcId()); + } + +} diff --git a/hbase-protocol-shaded/src/main/protobuf/Cell.proto b/hbase-protocol-shaded/src/main/protobuf/Cell.proto index 0e9eb94b5e8e..aada35329aaf 100644 --- a/hbase-protocol-shaded/src/main/protobuf/Cell.proto +++ b/hbase-protocol-shaded/src/main/protobuf/Cell.proto @@ -32,6 +32,7 @@ enum CellType { PUT = 4; DELETE = 8; + DELETE_FAMILY_VERSION = 10; DELETE_COLUMN = 12; DELETE_FAMILY = 14; diff --git a/hbase-protocol-shaded/src/main/protobuf/Master.proto b/hbase-protocol-shaded/src/main/protobuf/Master.proto index 69e0f32d07e4..8318da39c2c8 100644 --- a/hbase-protocol-shaded/src/main/protobuf/Master.proto +++ b/hbase-protocol-shaded/src/main/protobuf/Master.proto @@ -99,6 +99,7 @@ message MergeTableRegionsResponse { message AssignRegionRequest { required RegionSpecifier region = 1; + optional bool override = 2 [default = false]; } message AssignRegionResponse { @@ -485,10 +486,6 @@ message GetTableStateResponse { required TableState table_state = 1; } -message SetTableStateInMetaRequest { - required TableName table_name = 1; - required TableState table_state = 2; -} message GetClusterStatusRequest { repeated Option options = 1; @@ -997,8 +994,82 @@ service MasterService { } +// HBCK Service definitions. + +message SetTableStateInMetaRequest { + required TableName table_name = 1; + required TableState table_state = 2; +} + +/** Like Admin's AssignRegionRequest except it can + * take one or more Regions at a time. + */ +// NOTE: In hbck.proto, there is a define for +// AssignRegionRequest -- singular 'Region'. This +// is plural to convey it can carry more than one +// Region at a time. +message AssignsRequest { + repeated RegionSpecifier region = 1; + optional bool override = 2 [default = false]; +} + +/** Like Admin's AssignRegionResponse except it can + * return one or more pids as result -- one per assign. + */ +message AssignsResponse { + repeated uint64 pid = 1; +} + +/** Like Admin's UnassignRegionRequest except it can + * take one or more Regions at a time. + */ +message UnassignsRequest { + repeated RegionSpecifier region = 1; + optional bool override = 2 [default = false]; +} + +/** Like Admin's UnassignRegionResponse except it can + * return one or more pids as result -- one per unassign. + */ +message UnassignsResponse { + repeated uint64 pid = 1; +} + +message BypassProcedureRequest { + repeated uint64 proc_id = 1; + optional uint64 waitTime = 2; // wait time in ms to acquire lock on a procedure + optional bool override = 3 [default = false]; // if true, procedure is marked for bypass even if its executing + optional bool recursive = 4; +} + +message BypassProcedureResponse { + repeated bool bypassed = 1; +} + service HbckService { /** Update state of the table in meta only*/ rpc SetTableStateInMeta(SetTableStateInMetaRequest) returns(GetTableStateResponse); + + /** + * Assign regions. + * Like Admin's assign but works even if the + * Master is initializing. Also allows bulk'ing up + * assigns rather than one region at a time. + */ + rpc Assigns(AssignsRequest) + returns(AssignsResponse); + + /** + * Unassign regions + * Like Admin's unssign but works even if the + * Master is initializing. Also allows bulk'ing up + * assigns rather than one region at a time. + */ + rpc Unassigns(UnassignsRequest) + returns(UnassignsResponse); + + /** Bypass a procedure to completion, procedure is completed but no actual work is done*/ + rpc BypassProcedure(BypassProcedureRequest) + returns(BypassProcedureResponse); } diff --git a/hbase-protocol-shaded/src/main/protobuf/MasterProcedure.proto b/hbase-protocol-shaded/src/main/protobuf/MasterProcedure.proto index e50a9137bb77..07d1a55af5bd 100644 --- a/hbase-protocol-shaded/src/main/protobuf/MasterProcedure.proto +++ b/hbase-protocol-shaded/src/main/protobuf/MasterProcedure.proto @@ -533,6 +533,7 @@ message RegionStateTransitionStateData { required RegionStateTransitionState lastState = 2; optional ServerName assign_candidate = 3; required bool force_new_plan = 4; + optional bool override = 5 [default = false]; } message RegionRemoteProcedureBaseStateData { diff --git a/hbase-protocol/src/main/protobuf/Cell.proto b/hbase-protocol/src/main/protobuf/Cell.proto index 2c6103540fb8..e518e658f63b 100644 --- a/hbase-protocol/src/main/protobuf/Cell.proto +++ b/hbase-protocol/src/main/protobuf/Cell.proto @@ -32,6 +32,7 @@ enum CellType { PUT = 4; DELETE = 8; + DELETE_FAMILY_VERSION = 10; DELETE_COLUMN = 12; DELETE_FAMILY = 14; diff --git a/hbase-replication/src/main/java/org/apache/hadoop/hbase/replication/ReplicationQueueStorage.java b/hbase-replication/src/main/java/org/apache/hadoop/hbase/replication/ReplicationQueueStorage.java index 84653adbf6d1..59278e9807d5 100644 --- a/hbase-replication/src/main/java/org/apache/hadoop/hbase/replication/ReplicationQueueStorage.java +++ b/hbase-replication/src/main/java/org/apache/hadoop/hbase/replication/ReplicationQueueStorage.java @@ -202,4 +202,11 @@ Pair> claimQueue(ServerName sourceServerName, String q * created hfile references during the call may not be included. */ Set getAllHFileRefs() throws ReplicationException; + + /** + * Get full znode name for given region server + * @param serverName the name of the region server + * @return full znode name + */ + String getRsNode(ServerName serverName); } diff --git a/hbase-replication/src/main/java/org/apache/hadoop/hbase/replication/ZKReplicationQueueStorage.java b/hbase-replication/src/main/java/org/apache/hadoop/hbase/replication/ZKReplicationQueueStorage.java index cca8bfcab3da..68f2adc2e8b0 100644 --- a/hbase-replication/src/main/java/org/apache/hadoop/hbase/replication/ZKReplicationQueueStorage.java +++ b/hbase-replication/src/main/java/org/apache/hadoop/hbase/replication/ZKReplicationQueueStorage.java @@ -118,7 +118,8 @@ public ZKReplicationQueueStorage(ZKWatcher zookeeper, Configuration conf) { .get(ZOOKEEPER_ZNODE_REPLICATION_REGIONS_KEY, ZOOKEEPER_ZNODE_REPLICATION_REGIONS_DEFAULT)); } - private String getRsNode(ServerName serverName) { + @Override + public String getRsNode(ServerName serverName) { return ZNodePaths.joinZNode(queuesZNode, serverName.getServerName()); } diff --git a/hbase-rsgroup/src/main/java/org/apache/hadoop/hbase/rsgroup/RSGroupBasedLoadBalancer.java b/hbase-rsgroup/src/main/java/org/apache/hadoop/hbase/rsgroup/RSGroupBasedLoadBalancer.java index 5c70ecf34fe3..500650026d24 100644 --- a/hbase-rsgroup/src/main/java/org/apache/hadoop/hbase/rsgroup/RSGroupBasedLoadBalancer.java +++ b/hbase-rsgroup/src/main/java/org/apache/hadoop/hbase/rsgroup/RSGroupBasedLoadBalancer.java @@ -478,4 +478,8 @@ public void setRsGroupInfoManager(RSGroupInfoManager rsGroupInfoManager) { public void postMasterStartupInitialize() { this.internalBalancer.postMasterStartupInitialize(); } + + public void updateBalancerStatus(boolean status) { + internalBalancer.updateBalancerStatus(status); + } } diff --git a/hbase-rsgroup/src/main/java/org/apache/hadoop/hbase/rsgroup/RSGroupInfoManagerImpl.java b/hbase-rsgroup/src/main/java/org/apache/hadoop/hbase/rsgroup/RSGroupInfoManagerImpl.java index ee0651b4918f..4a881d30d928 100644 --- a/hbase-rsgroup/src/main/java/org/apache/hadoop/hbase/rsgroup/RSGroupInfoManagerImpl.java +++ b/hbase-rsgroup/src/main/java/org/apache/hadoop/hbase/rsgroup/RSGroupInfoManagerImpl.java @@ -65,6 +65,7 @@ import org.apache.hadoop.hbase.master.ServerListener; import org.apache.hadoop.hbase.master.TableStateManager; import org.apache.hadoop.hbase.master.assignment.RegionStateNode; +import org.apache.hadoop.hbase.master.procedure.MasterProcedureUtil; import org.apache.hadoop.hbase.net.Address; import org.apache.hadoop.hbase.procedure2.Procedure; import org.apache.hadoop.hbase.protobuf.ProtobufMagic; @@ -874,7 +875,7 @@ private void createRSGroupTable() throws IOException { Procedure result = masterServices.getMasterProcedureExecutor().getResult(procId); if (result != null && result.isFailed()) { throw new IOException("Failed to create group table. " + - result.getException().unwrapRemoteIOException()); + MasterProcedureUtil.unwrapRemoteIOException(result)); } } } diff --git a/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestEnableRSGroup.java b/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestEnableRSGroups.java similarity index 93% rename from hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestEnableRSGroup.java rename to hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestEnableRSGroups.java index 77faec70f4fc..d15c7a89f864 100644 --- a/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestEnableRSGroup.java +++ b/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestEnableRSGroups.java @@ -40,13 +40,13 @@ * Test enable RSGroup */ @Category({ MediumTests.class }) -public class TestEnableRSGroup { +public class TestEnableRSGroups { @ClassRule public static final HBaseClassTestRule CLASS_RULE = - HBaseClassTestRule.forClass(TestEnableRSGroup.class); + HBaseClassTestRule.forClass(TestEnableRSGroups.class); - protected static final Logger LOG = LoggerFactory.getLogger(TestEnableRSGroup.class); + protected static final Logger LOG = LoggerFactory.getLogger(TestEnableRSGroups.class); private static final HBaseTestingUtility TEST_UTIL = new HBaseTestingUtility(); @@ -71,7 +71,8 @@ public void testEnableRSGroup() throws IOException, InterruptedException { LOG.info("stopped master..."); final Configuration conf = TEST_UTIL.getMiniHBaseCluster().getConfiguration(); conf.set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY, RSGroupAdminEndpoint.class.getName()); - conf.set(HConstants.HBASE_MASTER_LOADBALANCER_CLASS, RSGroupBasedLoadBalancer.class.getName()); + conf.set(HConstants.HBASE_MASTER_LOADBALANCER_CLASS, + RSGroupBasedLoadBalancer.class.getName()); TEST_UTIL.getMiniHBaseCluster().startMaster(); TEST_UTIL.getMiniHBaseCluster().waitForActiveAndReadyMaster(60000); diff --git a/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroups.java b/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroups.java deleted file mode 100644 index 2fac94c6c7ac..000000000000 --- a/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroups.java +++ /dev/null @@ -1,616 +0,0 @@ -/** - * 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.hadoop.hbase.rsgroup; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -import java.io.IOException; -import java.util.Iterator; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import org.apache.hadoop.hbase.HBaseClassTestRule; -import org.apache.hadoop.hbase.HBaseTestingUtility; -import org.apache.hadoop.hbase.HColumnDescriptor; -import org.apache.hadoop.hbase.HConstants; -import org.apache.hadoop.hbase.HTableDescriptor; -import org.apache.hadoop.hbase.MiniHBaseCluster; -import org.apache.hadoop.hbase.NamespaceDescriptor; -import org.apache.hadoop.hbase.ServerName; -import org.apache.hadoop.hbase.TableName; -import org.apache.hadoop.hbase.Waiter; -import org.apache.hadoop.hbase.Waiter.Predicate; -import org.apache.hadoop.hbase.client.ClusterConnection; -import org.apache.hadoop.hbase.coprocessor.CoprocessorHost; -import org.apache.hadoop.hbase.coprocessor.MasterCoprocessor; -import org.apache.hadoop.hbase.coprocessor.MasterCoprocessorEnvironment; -import org.apache.hadoop.hbase.coprocessor.MasterObserver; -import org.apache.hadoop.hbase.coprocessor.ObserverContext; -import org.apache.hadoop.hbase.master.MasterCoprocessorHost; -import org.apache.hadoop.hbase.master.ServerManager; -import org.apache.hadoop.hbase.master.TableNamespaceManager; -import org.apache.hadoop.hbase.master.snapshot.SnapshotManager; -import org.apache.hadoop.hbase.net.Address; -import org.apache.hadoop.hbase.quotas.QuotaTableUtil; -import org.apache.hadoop.hbase.quotas.QuotaUtil; -import org.apache.hadoop.hbase.testclassification.MediumTests; -import org.apache.hadoop.hbase.util.Bytes; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Assert; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.ClassRule; -import org.junit.Test; -import org.junit.experimental.categories.Category; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.hbase.thirdparty.com.google.common.collect.Sets; - -import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil; -import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos; - -@Category({MediumTests.class}) -public class TestRSGroups extends TestRSGroupsBase { - - @ClassRule - public static final HBaseClassTestRule CLASS_RULE = - HBaseClassTestRule.forClass(TestRSGroups.class); - - protected static final Logger LOG = LoggerFactory.getLogger(TestRSGroups.class); - private static boolean INIT = false; - private static RSGroupAdminEndpoint rsGroupAdminEndpoint; - private static CPMasterObserver observer; - - @BeforeClass - public static void setUp() throws Exception { - TEST_UTIL = new HBaseTestingUtility(); - TEST_UTIL.getConfiguration().setFloat( - "hbase.master.balancer.stochastic.tableSkewCost", 6000); - TEST_UTIL.getConfiguration().set( - HConstants.HBASE_MASTER_LOADBALANCER_CLASS, - RSGroupBasedLoadBalancer.class.getName()); - TEST_UTIL.getConfiguration().set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY, - RSGroupAdminEndpoint.class.getName() + "," + CPMasterObserver.class.getName()); - TEST_UTIL.startMiniCluster(NUM_SLAVES_BASE - 1); - TEST_UTIL.getConfiguration().setInt( - ServerManager.WAIT_ON_REGIONSERVERS_MINTOSTART, - NUM_SLAVES_BASE - 1); - TEST_UTIL.getConfiguration().setBoolean(SnapshotManager.HBASE_SNAPSHOT_ENABLED, true); - initialize(); - } - - private static void initialize() throws Exception { - admin = TEST_UTIL.getAdmin(); - cluster = TEST_UTIL.getHBaseCluster(); - master = ((MiniHBaseCluster)cluster).getMaster(); - - //wait for balancer to come online - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - return master.isInitialized() && - ((RSGroupBasedLoadBalancer) master.getLoadBalancer()).isOnline(); - } - }); - admin.setBalancerRunning(false,true); - rsGroupAdmin = new VerifyingRSGroupAdminClient( - new RSGroupAdminClient(TEST_UTIL.getConnection()), TEST_UTIL.getConfiguration()); - MasterCoprocessorHost host = master.getMasterCoprocessorHost(); - observer = (CPMasterObserver) host.findCoprocessor(CPMasterObserver.class.getName()); - rsGroupAdminEndpoint = (RSGroupAdminEndpoint) - host.findCoprocessor(RSGroupAdminEndpoint.class.getName()); - } - - @AfterClass - public static void tearDown() throws Exception { - TEST_UTIL.shutdownMiniCluster(); - } - - @Before - public void beforeMethod() throws Exception { - if (!INIT) { - INIT = true; - afterMethod(); - } - - } - - @After - public void afterMethod() throws Exception { - deleteTableIfNecessary(); - deleteNamespaceIfNecessary(); - deleteGroups(); - - for(ServerName sn : admin.listDecommissionedRegionServers()){ - admin.recommissionRegionServer(sn, null); - } - assertTrue(admin.listDecommissionedRegionServers().isEmpty()); - - int missing = NUM_SLAVES_BASE - getNumServers(); - LOG.info("Restoring servers: "+missing); - for(int i=0; i() { - @Override - public boolean evaluate() throws Exception { - LOG.info("Waiting for cleanup to finish " + rsGroupAdmin.listRSGroups()); - //Might be greater since moving servers back to default - //is after starting a server - - return rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP).getServers().size() - == NUM_SLAVES_BASE; - } - }); - } - - @Test - public void testBasicStartUp() throws IOException { - RSGroupInfo defaultInfo = rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP); - assertEquals(4, defaultInfo.getServers().size()); - // Assignment of root and meta regions. - int count = master.getAssignmentManager().getRegionStates().getRegionAssignments().size(); - //3 meta,namespace, group - assertEquals(3, count); - } - - @Test - public void testNamespaceCreateAndAssign() throws Exception { - LOG.info("testNamespaceCreateAndAssign"); - String nsName = tablePrefix+"_foo"; - final TableName tableName = TableName.valueOf(nsName, tablePrefix + "_testCreateAndAssign"); - RSGroupInfo appInfo = addGroup("appInfo", 1); - admin.createNamespace(NamespaceDescriptor.create(nsName) - .addConfiguration(RSGroupInfo.NAMESPACE_DESC_PROP_GROUP, "appInfo").build()); - final HTableDescriptor desc = new HTableDescriptor(tableName); - desc.addFamily(new HColumnDescriptor("f")); - admin.createTable(desc); - //wait for created table to be assigned - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - return getTableRegionMap().get(desc.getTableName()) != null; - } - }); - ServerName targetServer = - ServerName.parseServerName(appInfo.getServers().iterator().next().toString()); - AdminProtos.AdminService.BlockingInterface rs = - ((ClusterConnection) admin.getConnection()).getAdmin(targetServer); - //verify it was assigned to the right group - Assert.assertEquals(1, ProtobufUtil.getOnlineRegions(rs).size()); - } - - @Test - public void testDefaultNamespaceCreateAndAssign() throws Exception { - LOG.info("testDefaultNamespaceCreateAndAssign"); - String tableName = tablePrefix + "_testCreateAndAssign"; - admin.modifyNamespace(NamespaceDescriptor.create("default") - .addConfiguration(RSGroupInfo.NAMESPACE_DESC_PROP_GROUP, "default").build()); - final HTableDescriptor desc = new HTableDescriptor(TableName.valueOf(tableName)); - desc.addFamily(new HColumnDescriptor("f")); - admin.createTable(desc); - //wait for created table to be assigned - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - return getTableRegionMap().get(desc.getTableName()) != null; - } - }); - } - - @Test - public void testNamespaceConstraint() throws Exception { - String nsName = tablePrefix+"_foo"; - String groupName = tablePrefix+"_foo"; - LOG.info("testNamespaceConstraint"); - rsGroupAdmin.addRSGroup(groupName); - assertTrue(observer.preAddRSGroupCalled); - assertTrue(observer.postAddRSGroupCalled); - - admin.createNamespace(NamespaceDescriptor.create(nsName) - .addConfiguration(RSGroupInfo.NAMESPACE_DESC_PROP_GROUP, groupName) - .build()); - //test removing a referenced group - try { - rsGroupAdmin.removeRSGroup(groupName); - fail("Expected a constraint exception"); - } catch (IOException ex) { - } - //test modify group - //changing with the same name is fine - admin.modifyNamespace( - NamespaceDescriptor.create(nsName) - .addConfiguration(RSGroupInfo.NAMESPACE_DESC_PROP_GROUP, groupName) - .build()); - String anotherGroup = tablePrefix+"_anotherGroup"; - rsGroupAdmin.addRSGroup(anotherGroup); - //test add non-existent group - admin.deleteNamespace(nsName); - rsGroupAdmin.removeRSGroup(groupName); - assertTrue(observer.preRemoveRSGroupCalled); - assertTrue(observer.postRemoveRSGroupCalled); - try { - admin.createNamespace(NamespaceDescriptor.create(nsName) - .addConfiguration(RSGroupInfo.NAMESPACE_DESC_PROP_GROUP, "foo") - .build()); - fail("Expected a constraint exception"); - } catch (IOException ex) { - } - } - - @Test - public void testGroupInfoMultiAccessing() throws Exception { - RSGroupInfoManager manager = rsGroupAdminEndpoint.getGroupInfoManager(); - RSGroupInfo defaultGroup = manager.getRSGroup("default"); - // getRSGroup updates default group's server list - // this process must not affect other threads iterating the list - Iterator

it = defaultGroup.getServers().iterator(); - manager.getRSGroup("default"); - it.next(); - } - - public static class CPMasterObserver implements MasterCoprocessor, MasterObserver { - boolean preBalanceRSGroupCalled = false; - boolean postBalanceRSGroupCalled = false; - boolean preMoveServersCalled = false; - boolean postMoveServersCalled = false; - boolean preMoveTablesCalled = false; - boolean postMoveTablesCalled = false; - boolean preAddRSGroupCalled = false; - boolean postAddRSGroupCalled = false; - boolean preRemoveRSGroupCalled = false; - boolean postRemoveRSGroupCalled = false; - boolean preRemoveServersCalled = false; - boolean postRemoveServersCalled = false; - boolean preMoveServersAndTables = false; - boolean postMoveServersAndTables = false; - boolean preGetRSGroupInfoCalled = false; - boolean postGetRSGroupInfoCalled = false; - boolean preGetRSGroupInfoOfTableCalled = false; - boolean postGetRSGroupInfoOfTableCalled = false; - boolean preListRSGroupsCalled = false; - boolean postListRSGroupsCalled = false; - boolean preGetRSGroupInfoOfServerCalled = false; - boolean postGetRSGroupInfoOfServerCalled = false; - - @Override - public Optional getMasterObserver() { - return Optional.of(this); - } - @Override - public void preMoveServersAndTables(final ObserverContext ctx, - Set
servers, Set tables, String targetGroup) throws IOException { - preMoveServersAndTables = true; - } - @Override - public void postMoveServersAndTables(final ObserverContext ctx, - Set
servers, Set tables, String targetGroup) throws IOException { - postMoveServersAndTables = true; - } - @Override - public void preRemoveServers( - final ObserverContext ctx, - Set
servers) throws IOException { - preRemoveServersCalled = true; - } - @Override - public void postRemoveServers( - final ObserverContext ctx, - Set
servers) throws IOException { - postRemoveServersCalled = true; - } - @Override - public void preRemoveRSGroup(final ObserverContext ctx, - String name) throws IOException { - preRemoveRSGroupCalled = true; - } - @Override - public void postRemoveRSGroup(final ObserverContext ctx, - String name) throws IOException { - postRemoveRSGroupCalled = true; - } - @Override - public void preAddRSGroup(final ObserverContext ctx, - String name) throws IOException { - preAddRSGroupCalled = true; - } - @Override - public void postAddRSGroup(final ObserverContext ctx, - String name) throws IOException { - postAddRSGroupCalled = true; - } - @Override - public void preMoveTables(final ObserverContext ctx, - Set tables, String targetGroup) throws IOException { - preMoveTablesCalled = true; - } - @Override - public void postMoveTables(final ObserverContext ctx, - Set tables, String targetGroup) throws IOException { - postMoveTablesCalled = true; - } - @Override - public void preMoveServers(final ObserverContext ctx, - Set
servers, String targetGroup) throws IOException { - preMoveServersCalled = true; - } - - @Override - public void postMoveServers(final ObserverContext ctx, - Set
servers, String targetGroup) throws IOException { - postMoveServersCalled = true; - } - @Override - public void preBalanceRSGroup(final ObserverContext ctx, - String groupName) throws IOException { - preBalanceRSGroupCalled = true; - } - @Override - public void postBalanceRSGroup(final ObserverContext ctx, - String groupName, boolean balancerRan) throws IOException { - postBalanceRSGroupCalled = true; - } - - @Override - public void preGetRSGroupInfo(final ObserverContext ctx, - final String groupName) throws IOException { - preGetRSGroupInfoCalled = true; - } - - @Override - public void postGetRSGroupInfo(final ObserverContext ctx, - final String groupName) throws IOException { - postGetRSGroupInfoCalled = true; - } - - @Override - public void preGetRSGroupInfoOfTable(final ObserverContext ctx, - final TableName tableName) throws IOException { - preGetRSGroupInfoOfTableCalled = true; - } - - @Override - public void postGetRSGroupInfoOfTable(final ObserverContext ctx, - final TableName tableName) throws IOException { - postGetRSGroupInfoOfTableCalled = true; - } - - @Override - public void preListRSGroups(final ObserverContext ctx) - throws IOException { - preListRSGroupsCalled = true; - } - - @Override - public void postListRSGroups(final ObserverContext ctx) - throws IOException { - postListRSGroupsCalled = true; - } - - @Override - public void preGetRSGroupInfoOfServer(final ObserverContext ctx, - final Address server) throws IOException { - preGetRSGroupInfoOfServerCalled = true; - } - - @Override - public void postGetRSGroupInfoOfServer(final ObserverContext ctx, - final Address server) throws IOException { - postGetRSGroupInfoOfServerCalled = true; - } - } - - @Test - public void testGetRSGroupInfoCPHookCalled() throws Exception { - rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP); - assertTrue(observer.preGetRSGroupInfoCalled); - assertTrue(observer.postGetRSGroupInfoCalled); - } - - @Test - public void testGetRSGroupInfoOfTableCPHookCalled() throws Exception { - rsGroupAdmin.getRSGroupInfoOfTable(TableName.META_TABLE_NAME); - assertTrue(observer.preGetRSGroupInfoOfTableCalled); - assertTrue(observer.postGetRSGroupInfoOfTableCalled); - } - - @Test - public void testListRSGroupsCPHookCalled() throws Exception { - rsGroupAdmin.listRSGroups(); - assertTrue(observer.preListRSGroupsCalled); - assertTrue(observer.postListRSGroupsCalled); - } - - @Test - public void testGetRSGroupInfoOfServerCPHookCalled() throws Exception { - ServerName masterServerName = ((MiniHBaseCluster) cluster).getMaster().getServerName(); - rsGroupAdmin.getRSGroupOfServer(masterServerName.getAddress()); - assertTrue(observer.preGetRSGroupInfoOfServerCalled); - assertTrue(observer.postGetRSGroupInfoOfServerCalled); - } - - @Test - public void testMoveServersAndTables() throws Exception { - super.testMoveServersAndTables(); - assertTrue(observer.preMoveServersAndTables); - assertTrue(observer.postMoveServersAndTables); - } - @Test - public void testTableMoveTruncateAndDrop() throws Exception { - super.testTableMoveTruncateAndDrop(); - assertTrue(observer.preMoveTablesCalled); - assertTrue(observer.postMoveTablesCalled); - } - - @Test - public void testRemoveServers() throws Exception { - super.testRemoveServers(); - assertTrue(observer.preRemoveServersCalled); - assertTrue(observer.postRemoveServersCalled); - } - - @Test - public void testMisplacedRegions() throws Exception { - final TableName tableName = TableName.valueOf(tablePrefix+"_testMisplacedRegions"); - LOG.info("testMisplacedRegions"); - - final RSGroupInfo RSGroupInfo = addGroup("testMisplacedRegions", 1); - - TEST_UTIL.createMultiRegionTable(tableName, new byte[]{'f'}, 15); - TEST_UTIL.waitUntilAllRegionsAssigned(tableName); - - rsGroupAdminEndpoint.getGroupInfoManager() - .moveTables(Sets.newHashSet(tableName), RSGroupInfo.getName()); - - admin.setBalancerRunning(true,true); - assertTrue(rsGroupAdmin.balanceRSGroup(RSGroupInfo.getName())); - admin.setBalancerRunning(false,true); - assertTrue(observer.preBalanceRSGroupCalled); - assertTrue(observer.postBalanceRSGroupCalled); - - TEST_UTIL.waitFor(60000, new Predicate() { - @Override - public boolean evaluate() throws Exception { - ServerName serverName = - ServerName.valueOf(RSGroupInfo.getServers().iterator().next().toString(), 1); - return admin.getConnection().getAdmin() - .getOnlineRegions(serverName).size() == 15; - } - }); - } - - @Test - public void testCloneSnapshot() throws Exception { - byte[] FAMILY = Bytes.toBytes("test"); - String snapshotName = tableName.getNameAsString() + "_snap"; - TableName clonedTableName = TableName.valueOf(tableName.getNameAsString() + "_clone"); - - // create base table - TEST_UTIL.createTable(tableName, FAMILY); - - // create snapshot - admin.snapshot(snapshotName, tableName); - - // clone - admin.cloneSnapshot(snapshotName, clonedTableName); - } - - @Test - public void testRSGroupsWithHBaseQuota() throws Exception { - TEST_UTIL.getConfiguration().setBoolean(QuotaUtil.QUOTA_CONF_KEY, true); - restartHBaseCluster(); - try { - TEST_UTIL.waitFor(90000, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - return admin.isTableAvailable(QuotaTableUtil.QUOTA_TABLE_NAME); - } - }); - } finally { - TEST_UTIL.getConfiguration().setBoolean(QuotaUtil.QUOTA_CONF_KEY, false); - restartHBaseCluster(); - } - } - - @Test - public void testRSGroupListDoesNotContainFailedTableCreation() throws Exception { - toggleQuotaCheckAndRestartMiniCluster(true); - String nsp = "np1"; - NamespaceDescriptor nspDesc = - NamespaceDescriptor.create(nsp).addConfiguration(TableNamespaceManager.KEY_MAX_REGIONS, "5") - .addConfiguration(TableNamespaceManager.KEY_MAX_TABLES, "2").build(); - admin.createNamespace(nspDesc); - assertEquals(3, admin.listNamespaceDescriptors().length); - HColumnDescriptor fam1 = new HColumnDescriptor("fam1"); - HTableDescriptor tableDescOne = - new HTableDescriptor(TableName.valueOf(nsp + TableName.NAMESPACE_DELIM + "table1")); - tableDescOne.addFamily(fam1); - admin.createTable(tableDescOne); - - HTableDescriptor tableDescTwo = - new HTableDescriptor(TableName.valueOf(nsp + TableName.NAMESPACE_DELIM + "table2")); - tableDescTwo.addFamily(fam1); - boolean constraintViolated = false; - - try { - admin.createTable(tableDescTwo, Bytes.toBytes("AAA"), Bytes.toBytes("ZZZ"), - 6); - Assert.fail("Creation table should fail because of quota violation."); - } catch (Exception exp) { - assertTrue(exp instanceof IOException); - constraintViolated = true; - } finally { - assertTrue("Constraint not violated for table " + tableDescTwo.getTableName(), - constraintViolated); - } - List rsGroupInfoList = rsGroupAdmin.listRSGroups(); - boolean foundTable2 = false; - boolean foundTable1 = false; - for(int i = 0; i < rsGroupInfoList.size(); i++){ - if(rsGroupInfoList.get(i).getTables().contains(tableDescTwo.getTableName())){ - foundTable2 = true; - } - if(rsGroupInfoList.get(i).getTables().contains(tableDescOne.getTableName())){ - foundTable1 = true; - } - } - assertFalse("Found table2 in rsgroup list.", foundTable2); - assertTrue("Did not find table1 in rsgroup list", foundTable1); - - TEST_UTIL.deleteTable(tableDescOne.getTableName()); - admin.deleteNamespace(nspDesc.getName()); - toggleQuotaCheckAndRestartMiniCluster(false); - - } - private void toggleQuotaCheckAndRestartMiniCluster(boolean enable) throws Exception{ - TEST_UTIL.shutdownMiniCluster(); - TEST_UTIL.getConfiguration().setBoolean(QuotaUtil.QUOTA_CONF_KEY, enable); - TEST_UTIL.startMiniCluster(NUM_SLAVES_BASE - 1); - TEST_UTIL.getConfiguration().setInt( - ServerManager.WAIT_ON_REGIONSERVERS_MINTOSTART, - NUM_SLAVES_BASE - 1); - TEST_UTIL.getConfiguration().setBoolean(SnapshotManager.HBASE_SNAPSHOT_ENABLED, true); - initialize(); - } - - private void restartHBaseCluster() throws Exception { - LOG.info("\n\nShutting down cluster"); - TEST_UTIL.shutdownMiniHBaseCluster(); - LOG.info("\n\nSleeping a bit"); - Thread.sleep(2000); - TEST_UTIL.restartHBaseCluster(NUM_SLAVES_BASE - 1); - initialize(); - } -} diff --git a/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsAdmin1.java b/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsAdmin1.java new file mode 100644 index 000000000000..89d1930730fc --- /dev/null +++ b/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsAdmin1.java @@ -0,0 +1,502 @@ +/** + * 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.hadoop.hbase.rsgroup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.HColumnDescriptor; +import org.apache.hadoop.hbase.HTableDescriptor; +import org.apache.hadoop.hbase.MiniHBaseCluster; +import org.apache.hadoop.hbase.NamespaceDescriptor; +import org.apache.hadoop.hbase.ServerName; +import org.apache.hadoop.hbase.TableName; +import org.apache.hadoop.hbase.TableNotFoundException; +import org.apache.hadoop.hbase.Waiter; +import org.apache.hadoop.hbase.constraint.ConstraintException; +import org.apache.hadoop.hbase.master.ServerManager; +import org.apache.hadoop.hbase.master.TableNamespaceManager; +import org.apache.hadoop.hbase.master.snapshot.SnapshotManager; +import org.apache.hadoop.hbase.net.Address; +import org.apache.hadoop.hbase.quotas.QuotaUtil; +import org.apache.hadoop.hbase.util.Bytes; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.hbase.testclassification.MediumTests; +import org.apache.hbase.thirdparty.com.google.common.collect.Sets; + +@Category({MediumTests.class}) +public class TestRSGroupsAdmin1 extends TestRSGroupsBase { + + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestRSGroupsAdmin1.class); + + protected static final Logger LOG = LoggerFactory.getLogger(TestRSGroupsAdmin1.class); + + @BeforeClass + public static void setUp() throws Exception { + setUpTestBeforeClass(); + } + + @AfterClass + public static void tearDown() throws Exception { + tearDownAfterClass(); + } + + @Before + public void beforeMethod() throws Exception { + setUpBeforeMethod(); + } + + @After + public void afterMethod() throws Exception { + tearDownAfterMethod(); + } + + @Test + public void testValidGroupNames() throws IOException { + String[] badNames = {"foo*","foo@","-"}; + String[] goodNames = {"foo_123"}; + + for(String entry: badNames) { + try { + rsGroupAdmin.addRSGroup(entry); + fail("Expected a constraint exception for: "+entry); + } catch(ConstraintException ex) { + //expected + } + } + + for(String entry: goodNames) { + rsGroupAdmin.addRSGroup(entry); + } + } + + @Test + public void testBogusArgs() throws Exception { + assertNull(rsGroupAdmin.getRSGroupInfoOfTable(TableName.valueOf("nonexistent"))); + assertNull(rsGroupAdmin.getRSGroupOfServer(Address.fromParts("bogus",123))); + assertNull(rsGroupAdmin.getRSGroupInfo("bogus")); + + try { + rsGroupAdmin.removeRSGroup("bogus"); + fail("Expected removing bogus group to fail"); + } catch(ConstraintException ex) { + //expected + } + + try { + rsGroupAdmin.moveTables(Sets.newHashSet(TableName.valueOf("bogustable")), "bogus"); + fail("Expected move with bogus group to fail"); + } catch(ConstraintException|TableNotFoundException ex) { + //expected + } + + try { + rsGroupAdmin.moveServers(Sets.newHashSet(Address.fromParts("bogus",123)), "bogus"); + fail("Expected move with bogus group to fail"); + } catch(ConstraintException ex) { + //expected + } + + try { + admin.setBalancerRunning(true,true); + rsGroupAdmin.balanceRSGroup("bogus"); + admin.setBalancerRunning(false,true); + fail("Expected move with bogus group to fail"); + } catch(ConstraintException ex) { + //expected + } + } + + @Test + public void testNamespaceConstraint() throws Exception { + String nsName = tablePrefix+"_foo"; + String groupName = tablePrefix+"_foo"; + LOG.info("testNamespaceConstraint"); + rsGroupAdmin.addRSGroup(groupName); + assertTrue(observer.preAddRSGroupCalled); + assertTrue(observer.postAddRSGroupCalled); + + admin.createNamespace(NamespaceDescriptor.create(nsName) + .addConfiguration(RSGroupInfo.NAMESPACE_DESC_PROP_GROUP, groupName) + .build()); + //test removing a referenced group + try { + rsGroupAdmin.removeRSGroup(groupName); + fail("Expected a constraint exception"); + } catch (IOException ex) { + } + //test modify group + //changing with the same name is fine + admin.modifyNamespace( + NamespaceDescriptor.create(nsName) + .addConfiguration(RSGroupInfo.NAMESPACE_DESC_PROP_GROUP, groupName) + .build()); + String anotherGroup = tablePrefix+"_anotherGroup"; + rsGroupAdmin.addRSGroup(anotherGroup); + //test add non-existent group + admin.deleteNamespace(nsName); + rsGroupAdmin.removeRSGroup(groupName); + assertTrue(observer.preRemoveRSGroupCalled); + assertTrue(observer.postRemoveRSGroupCalled); + try { + admin.createNamespace(NamespaceDescriptor.create(nsName) + .addConfiguration(RSGroupInfo.NAMESPACE_DESC_PROP_GROUP, "foo") + .build()); + fail("Expected a constraint exception"); + } catch (IOException ex) { + } + } + + @Test + public void testGroupInfoMultiAccessing() throws Exception { + RSGroupInfoManager manager = rsGroupAdminEndpoint.getGroupInfoManager(); + RSGroupInfo defaultGroup = manager.getRSGroup("default"); + // getRSGroup updates default group's server list + // this process must not affect other threads iterating the list + Iterator
it = defaultGroup.getServers().iterator(); + manager.getRSGroup("default"); + it.next(); + } + + @Test + public void testGetRSGroupInfoCPHookCalled() throws Exception { + rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP); + assertTrue(observer.preGetRSGroupInfoCalled); + assertTrue(observer.postGetRSGroupInfoCalled); + } + + @Test + public void testGetRSGroupInfoOfTableCPHookCalled() throws Exception { + rsGroupAdmin.getRSGroupInfoOfTable(TableName.META_TABLE_NAME); + assertTrue(observer.preGetRSGroupInfoOfTableCalled); + assertTrue(observer.postGetRSGroupInfoOfTableCalled); + } + + @Test + public void testListRSGroupsCPHookCalled() throws Exception { + rsGroupAdmin.listRSGroups(); + assertTrue(observer.preListRSGroupsCalled); + assertTrue(observer.postListRSGroupsCalled); + } + + @Test + public void testGetRSGroupInfoOfServerCPHookCalled() throws Exception { + ServerName masterServerName = ((MiniHBaseCluster) cluster).getMaster().getServerName(); + rsGroupAdmin.getRSGroupOfServer(masterServerName.getAddress()); + assertTrue(observer.preGetRSGroupInfoOfServerCalled); + assertTrue(observer.postGetRSGroupInfoOfServerCalled); + } + + @Test + public void testFailRemoveGroup() throws IOException, InterruptedException { + int initNumGroups = rsGroupAdmin.listRSGroups().size(); + addGroup("bar", 3); + TEST_UTIL.createTable(tableName, Bytes.toBytes("f")); + rsGroupAdmin.moveTables(Sets.newHashSet(tableName), "bar"); + RSGroupInfo barGroup = rsGroupAdmin.getRSGroupInfo("bar"); + //group is not empty therefore it should fail + try { + rsGroupAdmin.removeRSGroup(barGroup.getName()); + fail("Expected remove group to fail"); + } catch(IOException e) { + } + //group cannot lose all it's servers therefore it should fail + try { + rsGroupAdmin.moveServers(barGroup.getServers(), RSGroupInfo.DEFAULT_GROUP); + fail("Expected move servers to fail"); + } catch(IOException e) { + } + + rsGroupAdmin.moveTables(barGroup.getTables(), RSGroupInfo.DEFAULT_GROUP); + try { + rsGroupAdmin.removeRSGroup(barGroup.getName()); + fail("Expected move servers to fail"); + } catch(IOException e) { + } + + rsGroupAdmin.moveServers(barGroup.getServers(), RSGroupInfo.DEFAULT_GROUP); + rsGroupAdmin.removeRSGroup(barGroup.getName()); + + Assert.assertEquals(initNumGroups, rsGroupAdmin.listRSGroups().size()); + } + + @Test + public void testMultiTableMove() throws Exception { + final TableName tableNameA = TableName.valueOf(tablePrefix + name.getMethodName() + "A"); + final TableName tableNameB = TableName.valueOf(tablePrefix + name.getMethodName() + "B"); + final byte[] familyNameBytes = Bytes.toBytes("f"); + String newGroupName = getGroupName(name.getMethodName()); + final RSGroupInfo newGroup = addGroup(newGroupName, 1); + + TEST_UTIL.createTable(tableNameA, familyNameBytes); + TEST_UTIL.createTable(tableNameB, familyNameBytes); + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + List regionsA = getTableRegionMap().get(tableNameA); + if (regionsA == null) { + return false; + } + List regionsB = getTableRegionMap().get(tableNameB); + if (regionsB == null) { + return false; + } + return getTableRegionMap().get(tableNameA).size() >= 1 + && getTableRegionMap().get(tableNameB).size() >= 1; + } + }); + + RSGroupInfo tableGrpA = rsGroupAdmin.getRSGroupInfoOfTable(tableNameA); + assertTrue(tableGrpA.getName().equals(RSGroupInfo.DEFAULT_GROUP)); + + RSGroupInfo tableGrpB = rsGroupAdmin.getRSGroupInfoOfTable(tableNameB); + assertTrue(tableGrpB.getName().equals(RSGroupInfo.DEFAULT_GROUP)); + //change table's group + LOG.info("Moving table [" + tableNameA + "," + tableNameB + "] to " + newGroup.getName()); + rsGroupAdmin.moveTables(Sets.newHashSet(tableNameA, tableNameB), newGroup.getName()); + + //verify group change + Assert.assertEquals(newGroup.getName(), + rsGroupAdmin.getRSGroupInfoOfTable(tableNameA).getName()); + + Assert.assertEquals(newGroup.getName(), + rsGroupAdmin.getRSGroupInfoOfTable(tableNameB).getName()); + + //verify tables' not exist in old group + Set DefaultTables = + rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP).getTables(); + assertFalse(DefaultTables.contains(tableNameA)); + assertFalse(DefaultTables.contains(tableNameB)); + + //verify tables' exist in new group + Set newGroupTables = rsGroupAdmin.getRSGroupInfo(newGroupName).getTables(); + assertTrue(newGroupTables.contains(tableNameA)); + assertTrue(newGroupTables.contains(tableNameB)); + } + + @Test + public void testTableMoveTruncateAndDrop() throws Exception { + final byte[] familyNameBytes = Bytes.toBytes("f"); + String newGroupName = getGroupName(name.getMethodName()); + final RSGroupInfo newGroup = addGroup(newGroupName, 2); + + TEST_UTIL.createMultiRegionTable(tableName, familyNameBytes, 5); + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + List regions = getTableRegionMap().get(tableName); + if (regions == null) { + return false; + } + + return getTableRegionMap().get(tableName).size() >= 5; + } + }); + + RSGroupInfo tableGrp = rsGroupAdmin.getRSGroupInfoOfTable(tableName); + assertTrue(tableGrp.getName().equals(RSGroupInfo.DEFAULT_GROUP)); + + //change table's group + LOG.info("Moving table "+tableName+" to "+newGroup.getName()); + rsGroupAdmin.moveTables(Sets.newHashSet(tableName), newGroup.getName()); + + //verify group change + Assert.assertEquals(newGroup.getName(), + rsGroupAdmin.getRSGroupInfoOfTable(tableName).getName()); + + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + Map> serverMap = getTableServerRegionMap().get(tableName); + int count = 0; + if (serverMap != null) { + for (ServerName rs : serverMap.keySet()) { + if (newGroup.containsServer(rs.getAddress())) { + count += serverMap.get(rs).size(); + } + } + } + return count == 5; + } + }); + + //test truncate + admin.disableTable(tableName); + admin.truncateTable(tableName, true); + Assert.assertEquals(1, rsGroupAdmin.getRSGroupInfo(newGroup.getName()).getTables().size()); + Assert.assertEquals(tableName, rsGroupAdmin.getRSGroupInfo( + newGroup.getName()).getTables().first()); + + //verify removed table is removed from group + TEST_UTIL.deleteTable(tableName); + Assert.assertEquals(0, rsGroupAdmin.getRSGroupInfo(newGroup.getName()).getTables().size()); + + assertTrue(observer.preMoveTablesCalled); + assertTrue(observer.postMoveTablesCalled); + } + + @Test + public void testDisabledTableMove() throws Exception { + final byte[] familyNameBytes = Bytes.toBytes("f"); + String newGroupName = getGroupName(name.getMethodName()); + final RSGroupInfo newGroup = addGroup(newGroupName, 2); + + TEST_UTIL.createMultiRegionTable(tableName, familyNameBytes, 5); + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + List regions = getTableRegionMap().get(tableName); + if (regions == null) { + return false; + } + return getTableRegionMap().get(tableName).size() >= 5; + } + }); + + RSGroupInfo tableGrp = rsGroupAdmin.getRSGroupInfoOfTable(tableName); + assertTrue(tableGrp.getName().equals(RSGroupInfo.DEFAULT_GROUP)); + + //test disable table + admin.disableTable(tableName); + + //change table's group + LOG.info("Moving table "+ tableName + " to " + newGroup.getName()); + rsGroupAdmin.moveTables(Sets.newHashSet(tableName), newGroup.getName()); + + //verify group change + Assert.assertEquals(newGroup.getName(), + rsGroupAdmin.getRSGroupInfoOfTable(tableName).getName()); + } + + @Test + public void testNonExistentTableMove() throws Exception { + TableName tableName = TableName.valueOf(tablePrefix + name.getMethodName()); + + RSGroupInfo tableGrp = rsGroupAdmin.getRSGroupInfoOfTable(tableName); + assertNull(tableGrp); + + //test if table exists already. + boolean exist = admin.tableExists(tableName); + assertFalse(exist); + + LOG.info("Moving table "+ tableName + " to " + RSGroupInfo.DEFAULT_GROUP); + try { + rsGroupAdmin.moveTables(Sets.newHashSet(tableName), RSGroupInfo.DEFAULT_GROUP); + fail("Table " + tableName + " shouldn't have been successfully moved."); + } catch(IOException ex) { + assertTrue(ex instanceof TableNotFoundException); + } + + try { + rsGroupAdmin.moveServersAndTables( + Sets.newHashSet(Address.fromParts("bogus",123)), + Sets.newHashSet(tableName), RSGroupInfo.DEFAULT_GROUP); + fail("Table " + tableName + " shouldn't have been successfully moved."); + } catch(IOException ex) { + assertTrue(ex instanceof TableNotFoundException); + } + //verify group change + assertNull(rsGroupAdmin.getRSGroupInfoOfTable(tableName)); + } + + @Test + public void testRSGroupListDoesNotContainFailedTableCreation() throws Exception { + toggleQuotaCheckAndRestartMiniCluster(true); + String nsp = "np1"; + NamespaceDescriptor nspDesc = + NamespaceDescriptor.create(nsp) + .addConfiguration(TableNamespaceManager.KEY_MAX_REGIONS, "5") + .addConfiguration(TableNamespaceManager.KEY_MAX_TABLES, "2").build(); + admin.createNamespace(nspDesc); + assertEquals(3, admin.listNamespaceDescriptors().length); + HColumnDescriptor fam1 = new HColumnDescriptor("fam1"); + HTableDescriptor tableDescOne = + new HTableDescriptor(TableName.valueOf(nsp + TableName.NAMESPACE_DELIM + "table1")); + tableDescOne.addFamily(fam1); + admin.createTable(tableDescOne); + + HTableDescriptor tableDescTwo = + new HTableDescriptor(TableName.valueOf(nsp + TableName.NAMESPACE_DELIM + "table2")); + tableDescTwo.addFamily(fam1); + boolean constraintViolated = false; + + try { + admin.createTable(tableDescTwo, Bytes.toBytes("AAA"), Bytes.toBytes("ZZZ"), + 6); + Assert.fail("Creation table should fail because of quota violation."); + } catch (Exception exp) { + assertTrue(exp instanceof IOException); + constraintViolated = true; + } finally { + assertTrue("Constraint not violated for table " + tableDescTwo.getTableName(), + constraintViolated); + } + List rsGroupInfoList = rsGroupAdmin.listRSGroups(); + boolean foundTable2 = false; + boolean foundTable1 = false; + for(int i = 0; i < rsGroupInfoList.size(); i++){ + if(rsGroupInfoList.get(i).getTables().contains(tableDescTwo.getTableName())){ + foundTable2 = true; + } + if(rsGroupInfoList.get(i).getTables().contains(tableDescOne.getTableName())){ + foundTable1 = true; + } + } + assertFalse("Found table2 in rsgroup list.", foundTable2); + assertTrue("Did not find table1 in rsgroup list", foundTable1); + + TEST_UTIL.deleteTable(tableDescOne.getTableName()); + admin.deleteNamespace(nspDesc.getName()); + toggleQuotaCheckAndRestartMiniCluster(false); + + } + + private void toggleQuotaCheckAndRestartMiniCluster(boolean enable) throws Exception{ + TEST_UTIL.shutdownMiniCluster(); + TEST_UTIL.getConfiguration().setBoolean(QuotaUtil.QUOTA_CONF_KEY, enable); + TEST_UTIL.startMiniCluster(NUM_SLAVES_BASE - 1); + TEST_UTIL.getConfiguration().setInt( + ServerManager.WAIT_ON_REGIONSERVERS_MINTOSTART, + NUM_SLAVES_BASE - 1); + TEST_UTIL.getConfiguration().setBoolean(SnapshotManager.HBASE_SNAPSHOT_ENABLED, true); + initialize(); + } +} diff --git a/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsAdmin2.java b/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsAdmin2.java new file mode 100644 index 000000000000..f64e507c6f4f --- /dev/null +++ b/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsAdmin2.java @@ -0,0 +1,487 @@ +/** + * 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.hadoop.hbase.rsgroup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.hadoop.hbase.ClusterMetrics.Option; +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.ServerName; +import org.apache.hadoop.hbase.TableName; +import org.apache.hadoop.hbase.Waiter; +import org.apache.hadoop.hbase.client.ClusterConnection; +import org.apache.hadoop.hbase.client.RegionInfo; +import org.apache.hadoop.hbase.constraint.ConstraintException; +import org.apache.hadoop.hbase.net.Address; +import org.apache.hadoop.hbase.util.Bytes; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil; +import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos; +import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.GetServerInfoRequest; +import org.apache.hadoop.hbase.testclassification.MediumTests; +import org.apache.hbase.thirdparty.com.google.common.collect.Sets; + +@Category({MediumTests.class}) +public class TestRSGroupsAdmin2 extends TestRSGroupsBase { + + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestRSGroupsAdmin2.class); + + protected static final Logger LOG = LoggerFactory.getLogger(TestRSGroupsAdmin2.class); + + @BeforeClass + public static void setUp() throws Exception { + setUpTestBeforeClass(); + } + + @AfterClass + public static void tearDown() throws Exception { + tearDownAfterClass(); + } + + @Before + public void beforeMethod() throws Exception { + setUpBeforeMethod(); + } + + @After + public void afterMethod() throws Exception { + tearDownAfterMethod(); + } + + @Test + public void testRegionMove() throws Exception { + final RSGroupInfo newGroup = addGroup(getGroupName(name.getMethodName()), 1); + final byte[] familyNameBytes = Bytes.toBytes("f"); + // All the regions created below will be assigned to the default group. + TEST_UTIL.createMultiRegionTable(tableName, familyNameBytes, 6); + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + List regions = getTableRegionMap().get(tableName); + if (regions == null) { + return false; + } + + return getTableRegionMap().get(tableName).size() >= 6; + } + }); + + //get target region to move + Map> assignMap = + getTableServerRegionMap().get(tableName); + String targetRegion = null; + for(ServerName server : assignMap.keySet()) { + targetRegion = assignMap.get(server).size() > 0 ? assignMap.get(server).get(0) : null; + if(targetRegion != null) { + break; + } + } + //get server which is not a member of new group + ServerName targetServer = null; + for (ServerName server : admin.getClusterMetrics(EnumSet.of(Option.LIVE_SERVERS)) + .getLiveServerMetrics().keySet()) { + if (!newGroup.containsServer(server.getAddress())) { + targetServer = server; + break; + } + } + + final AdminProtos.AdminService.BlockingInterface targetRS = + ((ClusterConnection) admin.getConnection()).getAdmin(targetServer); + + //move target server to group + rsGroupAdmin.moveServers(Sets.newHashSet(targetServer.getAddress()), + newGroup.getName()); + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + return ProtobufUtil.getOnlineRegions(targetRS).size() <= 0; + } + }); + + // Lets move this region to the new group. + TEST_UTIL.getAdmin().move(Bytes.toBytes(RegionInfo.encodeRegionName( + Bytes.toBytes(targetRegion))), Bytes.toBytes(targetServer.getServerName())); + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + return + getTableRegionMap().get(tableName) != null && + getTableRegionMap().get(tableName).size() == 6 && + admin.getClusterMetrics(EnumSet.of(Option.REGIONS_IN_TRANSITION)) + .getRegionStatesInTransition().size() < 1; + } + }); + + //verify that targetServer didn't open it + for (RegionInfo region: ProtobufUtil.getOnlineRegions(targetRS)) { + if (targetRegion.equals(region.getRegionNameAsString())) { + fail("Target server opened region"); + } + } + } + + @Test + public void testRegionServerMove() throws IOException, + InterruptedException { + int initNumGroups = rsGroupAdmin.listRSGroups().size(); + RSGroupInfo appInfo = addGroup(getGroupName(name.getMethodName()), 1); + RSGroupInfo adminInfo = addGroup(getGroupName(name.getMethodName()), 1); + RSGroupInfo dInfo = rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP); + Assert.assertEquals(initNumGroups + 2, rsGroupAdmin.listRSGroups().size()); + assertEquals(1, adminInfo.getServers().size()); + assertEquals(1, appInfo.getServers().size()); + assertEquals(getNumServers() - 2, dInfo.getServers().size()); + rsGroupAdmin.moveServers(appInfo.getServers(), + RSGroupInfo.DEFAULT_GROUP); + rsGroupAdmin.removeRSGroup(appInfo.getName()); + rsGroupAdmin.moveServers(adminInfo.getServers(), + RSGroupInfo.DEFAULT_GROUP); + rsGroupAdmin.removeRSGroup(adminInfo.getName()); + Assert.assertEquals(rsGroupAdmin.listRSGroups().size(), initNumGroups); + } + + @Test + public void testMoveServers() throws Exception { + //create groups and assign servers + addGroup("bar", 3); + rsGroupAdmin.addRSGroup("foo"); + + RSGroupInfo barGroup = rsGroupAdmin.getRSGroupInfo("bar"); + RSGroupInfo fooGroup = rsGroupAdmin.getRSGroupInfo("foo"); + assertEquals(3, barGroup.getServers().size()); + assertEquals(0, fooGroup.getServers().size()); + + //test fail bogus server move + try { + rsGroupAdmin.moveServers(Sets.newHashSet(Address.fromString("foo:9999")),"foo"); + fail("Bogus servers shouldn't have been successfully moved."); + } catch(IOException ex) { + String exp = "Source RSGroup for server foo:9999 does not exist."; + String msg = "Expected '"+exp+"' in exception message: "; + assertTrue(msg+" "+ex.getMessage(), ex.getMessage().contains(exp)); + } + + //test success case + LOG.info("moving servers "+barGroup.getServers()+" to group foo"); + rsGroupAdmin.moveServers(barGroup.getServers(), fooGroup.getName()); + + barGroup = rsGroupAdmin.getRSGroupInfo("bar"); + fooGroup = rsGroupAdmin.getRSGroupInfo("foo"); + assertEquals(0,barGroup.getServers().size()); + assertEquals(3,fooGroup.getServers().size()); + + LOG.info("moving servers "+fooGroup.getServers()+" to group default"); + rsGroupAdmin.moveServers(fooGroup.getServers(), RSGroupInfo.DEFAULT_GROUP); + + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + return getNumServers() == + rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP).getServers().size(); + } + }); + + fooGroup = rsGroupAdmin.getRSGroupInfo("foo"); + assertEquals(0,fooGroup.getServers().size()); + + //test group removal + LOG.info("Remove group "+barGroup.getName()); + rsGroupAdmin.removeRSGroup(barGroup.getName()); + Assert.assertEquals(null, rsGroupAdmin.getRSGroupInfo(barGroup.getName())); + LOG.info("Remove group "+fooGroup.getName()); + rsGroupAdmin.removeRSGroup(fooGroup.getName()); + Assert.assertEquals(null, rsGroupAdmin.getRSGroupInfo(fooGroup.getName())); + } + + @Test + public void testRemoveServers() throws Exception { + LOG.info("testRemoveServers"); + final RSGroupInfo newGroup = addGroup(getGroupName(name.getMethodName()), 3); + Iterator
iterator = newGroup.getServers().iterator(); + ServerName targetServer = ServerName.parseServerName(iterator.next().toString()); + + // remove online servers + try { + rsGroupAdmin.removeServers(Sets.newHashSet(targetServer.getAddress())); + fail("Online servers shouldn't have been successfully removed."); + } catch(IOException ex) { + String exp = "Server " + targetServer.getAddress() + + " is an online server, not allowed to remove."; + String msg = "Expected '" + exp + "' in exception message: "; + assertTrue(msg + " " + ex.getMessage(), ex.getMessage().contains(exp)); + } + assertTrue(newGroup.getServers().contains(targetServer.getAddress())); + + // remove dead servers + NUM_DEAD_SERVERS = cluster.getClusterMetrics().getDeadServerNames().size(); + AdminProtos.AdminService.BlockingInterface targetRS = + ((ClusterConnection) admin.getConnection()).getAdmin(targetServer); + try { + targetServer = ProtobufUtil.toServerName(targetRS.getServerInfo(null, + GetServerInfoRequest.newBuilder().build()).getServerInfo().getServerName()); + //stopping may cause an exception + //due to the connection loss + LOG.info("stopping server " + targetServer.getServerName()); + targetRS.stopServer(null, + AdminProtos.StopServerRequest.newBuilder().setReason("Die").build()); + NUM_DEAD_SERVERS ++; + } catch(Exception e) { + } + + //wait for stopped regionserver to dead server list + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + return !master.getServerManager().areDeadServersInProgress() + && cluster.getClusterMetrics().getDeadServerNames().size() == NUM_DEAD_SERVERS; + } + }); + + try { + rsGroupAdmin.removeServers(Sets.newHashSet(targetServer.getAddress())); + fail("Dead servers shouldn't have been successfully removed."); + } catch(IOException ex) { + String exp = "Server " + targetServer.getAddress() + " is on the dead servers list," + + " Maybe it will come back again, not allowed to remove."; + String msg = "Expected '" + exp + "' in exception message: "; + assertTrue(msg + " " + ex.getMessage(), ex.getMessage().contains(exp)); + } + assertTrue(newGroup.getServers().contains(targetServer.getAddress())); + + // remove decommissioned servers + List serversToDecommission = new ArrayList<>(); + targetServer = ServerName.parseServerName(iterator.next().toString()); + targetRS = ((ClusterConnection) admin.getConnection()).getAdmin(targetServer); + targetServer = ProtobufUtil.toServerName(targetRS.getServerInfo(null, + GetServerInfoRequest.newBuilder().build()).getServerInfo().getServerName()); + assertTrue(master.getServerManager().getOnlineServers().containsKey(targetServer)); + serversToDecommission.add(targetServer); + + admin.decommissionRegionServers(serversToDecommission, true); + assertEquals(1, admin.listDecommissionedRegionServers().size()); + + assertTrue(newGroup.getServers().contains(targetServer.getAddress())); + rsGroupAdmin.removeServers(Sets.newHashSet(targetServer.getAddress())); + Set
newGroupServers = rsGroupAdmin.getRSGroupInfo(newGroup.getName()).getServers(); + assertFalse(newGroupServers.contains(targetServer.getAddress())); + assertEquals(2, newGroupServers.size()); + + assertTrue(observer.preRemoveServersCalled); + assertTrue(observer.postRemoveServersCalled); + } + + @Test + public void testMoveServersAndTables() throws Exception { + LOG.info("testMoveServersAndTables"); + final RSGroupInfo newGroup = addGroup(getGroupName(name.getMethodName()), 1); + //create table + final byte[] familyNameBytes = Bytes.toBytes("f"); + TEST_UTIL.createMultiRegionTable(tableName, familyNameBytes, 5); + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + List regions = getTableRegionMap().get(tableName); + if (regions == null) { + return false; + } + + return getTableRegionMap().get(tableName).size() >= 5; + } + }); + + //get server which is not a member of new group + ServerName targetServer = null; + for(ServerName server : admin.getClusterMetrics(EnumSet.of(Option.LIVE_SERVERS)) + .getLiveServerMetrics().keySet()) { + if(!newGroup.containsServer(server.getAddress()) && + !rsGroupAdmin.getRSGroupInfo("master").containsServer(server.getAddress())) { + targetServer = server; + break; + } + } + + LOG.debug("Print group info : " + rsGroupAdmin.listRSGroups()); + int oldDefaultGroupServerSize = + rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP).getServers().size(); + int oldDefaultGroupTableSize = + rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP).getTables().size(); + + //test fail bogus server move + try { + rsGroupAdmin.moveServersAndTables(Sets.newHashSet(Address.fromString("foo:9999")), + Sets.newHashSet(tableName), newGroup.getName()); + fail("Bogus servers shouldn't have been successfully moved."); + } catch(IOException ex) { + String exp = "Source RSGroup for server foo:9999 does not exist."; + String msg = "Expected '" + exp + "' in exception message: "; + assertTrue(msg + " " + ex.getMessage(), ex.getMessage().contains(exp)); + } + + //test fail server move + try { + rsGroupAdmin.moveServersAndTables(Sets.newHashSet(targetServer.getAddress()), + Sets.newHashSet(tableName), RSGroupInfo.DEFAULT_GROUP); + fail("servers shouldn't have been successfully moved."); + } catch(IOException ex) { + String exp = "Target RSGroup " + RSGroupInfo.DEFAULT_GROUP + + " is same as source " + RSGroupInfo.DEFAULT_GROUP + " RSGroup."; + String msg = "Expected '" + exp + "' in exception message: "; + assertTrue(msg + " " + ex.getMessage(), ex.getMessage().contains(exp)); + } + + //verify default group info + Assert.assertEquals(oldDefaultGroupServerSize, + rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP).getServers().size()); + Assert.assertEquals(oldDefaultGroupTableSize, + rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP).getTables().size()); + + //verify new group info + Assert.assertEquals(1, + rsGroupAdmin.getRSGroupInfo(newGroup.getName()).getServers().size()); + Assert.assertEquals(0, + rsGroupAdmin.getRSGroupInfo(newGroup.getName()).getTables().size()); + + //get all region to move targetServer + List regionList = getTableRegionMap().get(tableName); + for(String region : regionList) { + // Lets move this region to the targetServer + TEST_UTIL.getAdmin().move(Bytes.toBytes(RegionInfo.encodeRegionName(Bytes.toBytes(region))), + Bytes.toBytes(targetServer.getServerName())); + } + + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + return getTableRegionMap().get(tableName) != null && + getTableRegionMap().get(tableName).size() == 5 && + getTableServerRegionMap().get(tableName).size() == 1 && + admin.getClusterMetrics(EnumSet.of(Option.REGIONS_IN_TRANSITION)) + .getRegionStatesInTransition().size() < 1; + } + }); + + //verify that all region move to targetServer + Assert.assertEquals(5, getTableServerRegionMap().get(tableName).get(targetServer).size()); + + //move targetServer and table to newGroup + LOG.info("moving server and table to newGroup"); + rsGroupAdmin.moveServersAndTables(Sets.newHashSet(targetServer.getAddress()), + Sets.newHashSet(tableName), newGroup.getName()); + + //verify group change + Assert.assertEquals(newGroup.getName(), + rsGroupAdmin.getRSGroupInfoOfTable(tableName).getName()); + + //verify servers' not exist in old group + Set
defaultServers = rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP) + .getServers(); + assertFalse(defaultServers.contains(targetServer.getAddress())); + + //verify servers' exist in new group + Set
newGroupServers = rsGroupAdmin.getRSGroupInfo(newGroup.getName()).getServers(); + assertTrue(newGroupServers.contains(targetServer.getAddress())); + + //verify tables' not exist in old group + Set defaultTables = rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP) + .getTables(); + assertFalse(defaultTables.contains(tableName)); + + //verify tables' exist in new group + Set newGroupTables = rsGroupAdmin.getRSGroupInfo(newGroup.getName()).getTables(); + assertTrue(newGroupTables.contains(tableName)); + + //verify that all region still assgin on targetServer + Assert.assertEquals(5, getTableServerRegionMap().get(tableName).get(targetServer).size()); + + assertTrue(observer.preMoveServersAndTables); + assertTrue(observer.postMoveServersAndTables); + } + + @Test + public void testMoveServersFromDefaultGroup() throws Exception { + //create groups and assign servers + rsGroupAdmin.addRSGroup("foo"); + + RSGroupInfo fooGroup = rsGroupAdmin.getRSGroupInfo("foo"); + assertEquals(0, fooGroup.getServers().size()); + RSGroupInfo defaultGroup = rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP); + + //test remove all servers from default + try { + rsGroupAdmin.moveServers(defaultGroup.getServers(), fooGroup.getName()); + fail(RSGroupAdminServer.KEEP_ONE_SERVER_IN_DEFAULT_ERROR_MESSAGE); + } catch (ConstraintException ex) { + assertTrue(ex.getMessage().contains(RSGroupAdminServer + .KEEP_ONE_SERVER_IN_DEFAULT_ERROR_MESSAGE)); + } + + //test success case, remove one server from default ,keep at least one server + if (defaultGroup.getServers().size() > 1) { + Address serverInDefaultGroup = defaultGroup.getServers().iterator().next(); + LOG.info("moving server " + serverInDefaultGroup + " from group default to group " + + fooGroup.getName()); + rsGroupAdmin.moveServers(Sets.newHashSet(serverInDefaultGroup), fooGroup.getName()); + } + + fooGroup = rsGroupAdmin.getRSGroupInfo("foo"); + LOG.info("moving servers " + fooGroup.getServers() + " to group default"); + rsGroupAdmin.moveServers(fooGroup.getServers(), RSGroupInfo.DEFAULT_GROUP); + + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + return getNumServers() == + rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP).getServers().size(); + } + }); + + fooGroup = rsGroupAdmin.getRSGroupInfo("foo"); + assertEquals(0, fooGroup.getServers().size()); + + //test group removal + LOG.info("Remove group " + fooGroup.getName()); + rsGroupAdmin.removeRSGroup(fooGroup.getName()); + Assert.assertEquals(null, rsGroupAdmin.getRSGroupInfo(fooGroup.getName())); + } + +} diff --git a/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsBalance.java b/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsBalance.java new file mode 100644 index 000000000000..6cb6c81e7e02 --- /dev/null +++ b/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsBalance.java @@ -0,0 +1,188 @@ +/** + * 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.hadoop.hbase.rsgroup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.List; +import java.util.Map; + +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.HColumnDescriptor; +import org.apache.hadoop.hbase.HTableDescriptor; +import org.apache.hadoop.hbase.NamespaceDescriptor; +import org.apache.hadoop.hbase.ServerName; +import org.apache.hadoop.hbase.TableName; +import org.apache.hadoop.hbase.Waiter; +import org.apache.hadoop.hbase.Waiter.Predicate; +import org.apache.hadoop.hbase.client.RegionInfo; +import org.apache.hadoop.hbase.util.Bytes; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.hbase.testclassification.MediumTests; +import org.apache.hbase.thirdparty.com.google.common.collect.Sets; + +@Category({MediumTests.class}) +public class TestRSGroupsBalance extends TestRSGroupsBase { + + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestRSGroupsBalance.class); + + protected static final Logger LOG = LoggerFactory.getLogger(TestRSGroupsBalance.class); + + @BeforeClass + public static void setUp() throws Exception { + setUpTestBeforeClass(); + } + + @AfterClass + public static void tearDown() throws Exception { + tearDownAfterClass(); + } + + @Before + public void beforeMethod() throws Exception { + setUpBeforeMethod(); + } + + @After + public void afterMethod() throws Exception { + tearDownAfterMethod(); + } + + @Test + public void testGroupBalance() throws Exception { + LOG.info(name.getMethodName()); + String newGroupName = getGroupName(name.getMethodName()); + final RSGroupInfo newGroup = addGroup(newGroupName, 3); + + final TableName tableName = TableName.valueOf(tablePrefix+"_ns", name.getMethodName()); + admin.createNamespace( + NamespaceDescriptor.create(tableName.getNamespaceAsString()) + .addConfiguration(RSGroupInfo.NAMESPACE_DESC_PROP_GROUP, newGroupName).build()); + final byte[] familyNameBytes = Bytes.toBytes("f"); + final HTableDescriptor desc = new HTableDescriptor(tableName); + desc.addFamily(new HColumnDescriptor("f")); + byte [] startKey = Bytes.toBytes("aaaaa"); + byte [] endKey = Bytes.toBytes("zzzzz"); + admin.createTable(desc, startKey, endKey, 6); + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + List regions = getTableRegionMap().get(tableName); + if (regions == null) { + return false; + } + return regions.size() >= 6; + } + }); + + //make assignment uneven, move all regions to one server + Map> assignMap = + getTableServerRegionMap().get(tableName); + final ServerName first = assignMap.entrySet().iterator().next().getKey(); + for(RegionInfo region: admin.getTableRegions(tableName)) { + if(!assignMap.get(first).contains(region.getRegionNameAsString())) { + admin.move(region.getEncodedNameAsBytes(), Bytes.toBytes(first.getServerName())); + } + } + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + Map> map = getTableServerRegionMap().get(tableName); + if (map == null) { + return true; + } + List regions = map.get(first); + if (regions == null) { + return true; + } + return regions.size() >= 6; + } + }); + + //balance the other group and make sure it doesn't affect the new group + admin.setBalancerRunning(true,true); + rsGroupAdmin.balanceRSGroup(RSGroupInfo.DEFAULT_GROUP); + assertEquals(6, getTableServerRegionMap().get(tableName).get(first).size()); + + //disable balance, balancer will not be run and return false + admin.setBalancerRunning(false,true); + assertFalse(rsGroupAdmin.balanceRSGroup(newGroupName)); + assertEquals(6, getTableServerRegionMap().get(tableName).get(first).size()); + + //enable balance + admin.setBalancerRunning(true,true); + rsGroupAdmin.balanceRSGroup(newGroupName); + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + for (List regions : getTableServerRegionMap().get(tableName).values()) { + if (2 != regions.size()) { + return false; + } + } + return true; + } + }); + admin.setBalancerRunning(false,true); + } + + @Test + public void testMisplacedRegions() throws Exception { + final TableName tableName = TableName.valueOf(tablePrefix+"_testMisplacedRegions"); + LOG.info("testMisplacedRegions"); + + final RSGroupInfo RSGroupInfo = addGroup("testMisplacedRegions", 1); + + TEST_UTIL.createMultiRegionTable(tableName, new byte[]{'f'}, 15); + TEST_UTIL.waitUntilAllRegionsAssigned(tableName); + + rsGroupAdminEndpoint.getGroupInfoManager() + .moveTables(Sets.newHashSet(tableName), RSGroupInfo.getName()); + + admin.setBalancerRunning(true,true); + assertTrue(rsGroupAdmin.balanceRSGroup(RSGroupInfo.getName())); + admin.setBalancerRunning(false,true); + assertTrue(observer.preBalanceRSGroupCalled); + assertTrue(observer.postBalanceRSGroupCalled); + + TEST_UTIL.waitFor(60000, new Predicate() { + @Override + public boolean evaluate() throws Exception { + ServerName serverName = + ServerName.valueOf(RSGroupInfo.getServers().iterator().next().toString(), 1); + return admin.getConnection().getAdmin() + .getOnlineRegions(serverName).size() == 15; + } + }); + } + +} diff --git a/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsBase.java b/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsBase.java index 43099db13777..6a70b3070bb9 100644 --- a/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsBase.java +++ b/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsBase.java @@ -17,67 +17,61 @@ */ package org.apache.hadoop.hbase.rsgroup; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import java.io.IOException; -import java.security.SecureRandom; -import java.util.ArrayList; import java.util.EnumSet; import java.util.HashSet; -import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Random; import java.util.Set; import java.util.TreeMap; + import org.apache.hadoop.hbase.ClusterMetrics; import org.apache.hadoop.hbase.ClusterMetrics.Option; import org.apache.hadoop.hbase.HBaseCluster; import org.apache.hadoop.hbase.HBaseTestingUtility; -import org.apache.hadoop.hbase.HColumnDescriptor; -import org.apache.hadoop.hbase.HTableDescriptor; +import org.apache.hadoop.hbase.HConstants; +import org.apache.hadoop.hbase.MiniHBaseCluster; import org.apache.hadoop.hbase.NamespaceDescriptor; import org.apache.hadoop.hbase.RegionMetrics; import org.apache.hadoop.hbase.ServerMetrics; import org.apache.hadoop.hbase.ServerName; import org.apache.hadoop.hbase.TableName; -import org.apache.hadoop.hbase.TableNotFoundException; import org.apache.hadoop.hbase.Waiter; import org.apache.hadoop.hbase.client.Admin; -import org.apache.hadoop.hbase.client.ClusterConnection; import org.apache.hadoop.hbase.client.RegionInfo; -import org.apache.hadoop.hbase.constraint.ConstraintException; +import org.apache.hadoop.hbase.client.TableDescriptor; +import org.apache.hadoop.hbase.coprocessor.CoprocessorHost; +import org.apache.hadoop.hbase.coprocessor.MasterCoprocessor; +import org.apache.hadoop.hbase.coprocessor.MasterCoprocessorEnvironment; +import org.apache.hadoop.hbase.coprocessor.MasterObserver; +import org.apache.hadoop.hbase.coprocessor.ObserverContext; import org.apache.hadoop.hbase.master.HMaster; +import org.apache.hadoop.hbase.master.MasterCoprocessorHost; +import org.apache.hadoop.hbase.master.ServerManager; +import org.apache.hadoop.hbase.master.snapshot.SnapshotManager; import org.apache.hadoop.hbase.net.Address; import org.apache.hadoop.hbase.util.Bytes; -import org.junit.Assert; -import org.junit.Before; + import org.junit.Rule; -import org.junit.Test; import org.junit.rules.TestName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.apache.hbase.thirdparty.com.google.common.collect.Lists; import org.apache.hbase.thirdparty.com.google.common.collect.Maps; import org.apache.hbase.thirdparty.com.google.common.collect.Sets; -import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil; -import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos; -import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.GetServerInfoRequest; public abstract class TestRSGroupsBase { protected static final Logger LOG = LoggerFactory.getLogger(TestRSGroupsBase.class); - @Rule - public TestName name = new TestName(); //shared protected final static String groupPrefix = "Group"; protected final static String tablePrefix = "Group"; - protected final static SecureRandom rand = new SecureRandom(); + protected final static Random rand = new Random(); //shared, cluster type specific protected static HBaseTestingUtility TEST_UTIL; @@ -85,26 +79,115 @@ public abstract class TestRSGroupsBase { protected static HBaseCluster cluster; protected static RSGroupAdmin rsGroupAdmin; protected static HMaster master; + protected static boolean INIT = false; + protected static RSGroupAdminEndpoint rsGroupAdminEndpoint; + protected static CPMasterObserver observer; public final static long WAIT_TIMEOUT = 60000*5; public final static int NUM_SLAVES_BASE = 4; //number of slaves for the smallest cluster public static int NUM_DEAD_SERVERS = 0; // Per test variables - TableName tableName; - @Before - public void setup() { + @Rule + public TestName name = new TestName(); + protected TableName tableName; + + public static void setUpTestBeforeClass() throws Exception { + TEST_UTIL = new HBaseTestingUtility(); + TEST_UTIL.getConfiguration().setFloat( + "hbase.master.balancer.stochastic.tableSkewCost", 6000); + TEST_UTIL.getConfiguration().set( + HConstants.HBASE_MASTER_LOADBALANCER_CLASS, + RSGroupBasedLoadBalancer.class.getName()); + TEST_UTIL.getConfiguration().set(CoprocessorHost.MASTER_COPROCESSOR_CONF_KEY, + RSGroupAdminEndpoint.class.getName() + "," + CPMasterObserver.class.getName()); + TEST_UTIL.startMiniCluster(NUM_SLAVES_BASE - 1); + TEST_UTIL.getConfiguration().setInt( + ServerManager.WAIT_ON_REGIONSERVERS_MINTOSTART, + NUM_SLAVES_BASE - 1); + TEST_UTIL.getConfiguration().setBoolean(SnapshotManager.HBASE_SNAPSHOT_ENABLED, true); + initialize(); + } + + protected static void initialize() throws Exception { + admin = TEST_UTIL.getAdmin(); + cluster = TEST_UTIL.getHBaseCluster(); + master = ((MiniHBaseCluster)cluster).getMaster(); + + //wait for balancer to come online + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + return master.isInitialized() && + ((RSGroupBasedLoadBalancer) master.getLoadBalancer()).isOnline(); + } + }); + admin.setBalancerRunning(false,true); + rsGroupAdmin = new VerifyingRSGroupAdminClient( + new RSGroupAdminClient(TEST_UTIL.getConnection()), TEST_UTIL.getConfiguration()); + MasterCoprocessorHost host = master.getMasterCoprocessorHost(); + observer = (CPMasterObserver) host.findCoprocessor(CPMasterObserver.class.getName()); + rsGroupAdminEndpoint = (RSGroupAdminEndpoint) + host.findCoprocessor(RSGroupAdminEndpoint.class.getName()); + } + + public static void tearDownAfterClass() throws Exception { + TEST_UTIL.shutdownMiniCluster(); + } + + public void setUpBeforeMethod() throws Exception { LOG.info(name.getMethodName()); tableName = TableName.valueOf(tablePrefix + "_" + name.getMethodName()); + if (!INIT) { + INIT = true; + tearDownAfterMethod(); + } + observer.resetFlags(); } - protected RSGroupInfo addGroup(String groupName, int serverCount) + public void tearDownAfterMethod() throws Exception { + deleteTableIfNecessary(); + deleteNamespaceIfNecessary(); + deleteGroups(); + + for(ServerName sn : admin.listDecommissionedRegionServers()){ + admin.recommissionRegionServer(sn, null); + } + assertTrue(admin.listDecommissionedRegionServers().isEmpty()); + + int missing = NUM_SLAVES_BASE - getNumServers(); + LOG.info("Restoring servers: "+missing); + for(int i=0; i() { + @Override + public boolean evaluate() throws Exception { + LOG.info("Waiting for cleanup to finish " + rsGroupAdmin.listRSGroups()); + //Might be greater since moving servers back to default + //is after starting a server + + return rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP).getServers().size() + == NUM_SLAVES_BASE; + } + }); + } + + public RSGroupInfo addGroup(String groupName, int serverCount) throws IOException, InterruptedException { RSGroupInfo defaultInfo = rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP); - assertTrue(defaultInfo != null); - assertTrue(defaultInfo.getServers().size() >= serverCount); rsGroupAdmin.addRSGroup(groupName); - Set
set = new HashSet<>(); for(Address server: defaultInfo.getServers()) { if(set.size() == serverCount) { @@ -114,24 +197,23 @@ protected RSGroupInfo addGroup(String groupName, int serverCount) } rsGroupAdmin.moveServers(set, groupName); RSGroupInfo result = rsGroupAdmin.getRSGroupInfo(groupName); - assertTrue(result.getServers().size() >= serverCount); return result; } - void removeGroup(String groupName) throws IOException { - RSGroupInfo RSGroupInfo = rsGroupAdmin.getRSGroupInfo(groupName); - rsGroupAdmin.moveTables(RSGroupInfo.getTables(), RSGroupInfo.DEFAULT_GROUP); - rsGroupAdmin.moveServers(RSGroupInfo.getServers(), RSGroupInfo.DEFAULT_GROUP); + public void removeGroup(String groupName) throws IOException { + RSGroupInfo groupInfo = rsGroupAdmin.getRSGroupInfo(groupName); + rsGroupAdmin.moveTables(groupInfo.getTables(), RSGroupInfo.DEFAULT_GROUP); + rsGroupAdmin.moveServers(groupInfo.getServers(), RSGroupInfo.DEFAULT_GROUP); rsGroupAdmin.removeRSGroup(groupName); } - protected void deleteTableIfNecessary() throws IOException { - for (HTableDescriptor desc : TEST_UTIL.getAdmin().listTables(tablePrefix+".*")) { + public void deleteTableIfNecessary() throws IOException { + for (TableDescriptor desc : TEST_UTIL.getAdmin().listTables(tablePrefix+".*")) { TEST_UTIL.deleteTable(desc.getTableName()); } } - protected void deleteNamespaceIfNecessary() throws IOException { + public void deleteNamespaceIfNecessary() throws IOException { for (NamespaceDescriptor desc : TEST_UTIL.getAdmin().listNamespaceDescriptors()) { if(desc.getName().startsWith(tablePrefix)) { admin.deleteNamespace(desc.getName()); @@ -139,9 +221,8 @@ protected void deleteNamespaceIfNecessary() throws IOException { } } - protected void deleteGroups() throws IOException { - RSGroupAdmin groupAdmin = - new RSGroupAdminClient(TEST_UTIL.getConnection()); + public void deleteGroups() throws IOException { + RSGroupAdmin groupAdmin = new RSGroupAdminClient(TEST_UTIL.getConnection()); for(RSGroupInfo group: groupAdmin.listRSGroups()) { if(!group.getName().equals(RSGroupInfo.DEFAULT_GROUP)) { groupAdmin.moveTables(group.getTables(), RSGroupInfo.DEFAULT_GROUP); @@ -193,85 +274,6 @@ public Map>> getTableServerRegionMap() return map; } - @Test - public void testBogusArgs() throws Exception { - assertNull(rsGroupAdmin.getRSGroupInfoOfTable(TableName.valueOf("nonexistent"))); - assertNull(rsGroupAdmin.getRSGroupOfServer(Address.fromParts("bogus",123))); - assertNull(rsGroupAdmin.getRSGroupInfo("bogus")); - - try { - rsGroupAdmin.removeRSGroup("bogus"); - fail("Expected removing bogus group to fail"); - } catch(ConstraintException ex) { - //expected - } - - try { - rsGroupAdmin.moveTables(Sets.newHashSet(TableName.valueOf("bogustable")), "bogus"); - fail("Expected move with bogus group to fail"); - } catch(ConstraintException|TableNotFoundException ex) { - //expected - } - - try { - rsGroupAdmin.moveServers(Sets.newHashSet(Address.fromParts("bogus",123)), "bogus"); - fail("Expected move with bogus group to fail"); - } catch(ConstraintException ex) { - //expected - } - - try { - admin.setBalancerRunning(true,true); - rsGroupAdmin.balanceRSGroup("bogus"); - admin.setBalancerRunning(false,true); - fail("Expected move with bogus group to fail"); - } catch(ConstraintException ex) { - //expected - } - } - - @Test - public void testCreateMultiRegion() throws IOException { - byte[] end = {1,3,5,7,9}; - byte[] start = {0,2,4,6,8}; - byte[][] f = {Bytes.toBytes("f")}; - TEST_UTIL.createTable(tableName, f,1,start,end,10); - } - - @Test - public void testCreateAndDrop() throws Exception { - TEST_UTIL.createTable(tableName, Bytes.toBytes("cf")); - //wait for created table to be assigned - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - return getTableRegionMap().get(tableName) != null; - } - }); - TEST_UTIL.deleteTable(tableName); - } - - - @Test - public void testSimpleRegionServerMove() throws IOException, - InterruptedException { - int initNumGroups = rsGroupAdmin.listRSGroups().size(); - RSGroupInfo appInfo = addGroup(getGroupName(name.getMethodName()), 1); - RSGroupInfo adminInfo = addGroup(getGroupName(name.getMethodName()), 1); - RSGroupInfo dInfo = rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP); - Assert.assertEquals(initNumGroups + 2, rsGroupAdmin.listRSGroups().size()); - assertEquals(1, adminInfo.getServers().size()); - assertEquals(1, appInfo.getServers().size()); - assertEquals(getNumServers() - 2, dInfo.getServers().size()); - rsGroupAdmin.moveServers(appInfo.getServers(), - RSGroupInfo.DEFAULT_GROUP); - rsGroupAdmin.removeRSGroup(appInfo.getName()); - rsGroupAdmin.moveServers(adminInfo.getServers(), - RSGroupInfo.DEFAULT_GROUP); - rsGroupAdmin.removeRSGroup(adminInfo.getName()); - Assert.assertEquals(rsGroupAdmin.listRSGroups().size(), initNumGroups); - } - // return the real number of region servers, excluding the master embedded region server in 2.0+ public int getNumServers() throws IOException { ClusterMetrics status = @@ -286,867 +288,197 @@ public int getNumServers() throws IOException { return count; } - @Test - public void testMoveServers() throws Exception { - //create groups and assign servers - addGroup("bar", 3); - rsGroupAdmin.addRSGroup("foo"); - - RSGroupInfo barGroup = rsGroupAdmin.getRSGroupInfo("bar"); - RSGroupInfo fooGroup = rsGroupAdmin.getRSGroupInfo("foo"); - assertEquals(3, barGroup.getServers().size()); - assertEquals(0, fooGroup.getServers().size()); - - //test fail bogus server move - try { - rsGroupAdmin.moveServers(Sets.newHashSet(Address.fromString("foo:9999")),"foo"); - fail("Bogus servers shouldn't have been successfully moved."); - } catch(IOException ex) { - String exp = "Source RSGroup for server foo:9999 does not exist."; - String msg = "Expected '"+exp+"' in exception message: "; - assertTrue(msg+" "+ex.getMessage(), ex.getMessage().contains(exp)); - } - - //test success case - LOG.info("moving servers "+barGroup.getServers()+" to group foo"); - rsGroupAdmin.moveServers(barGroup.getServers(), fooGroup.getName()); - - barGroup = rsGroupAdmin.getRSGroupInfo("bar"); - fooGroup = rsGroupAdmin.getRSGroupInfo("foo"); - assertEquals(0,barGroup.getServers().size()); - assertEquals(3,fooGroup.getServers().size()); - - LOG.info("moving servers "+fooGroup.getServers()+" to group default"); - rsGroupAdmin.moveServers(fooGroup.getServers(), RSGroupInfo.DEFAULT_GROUP); - - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - return getNumServers() == - rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP).getServers().size(); - } - }); - - fooGroup = rsGroupAdmin.getRSGroupInfo("foo"); - assertEquals(0,fooGroup.getServers().size()); - - //test group removal - LOG.info("Remove group "+barGroup.getName()); - rsGroupAdmin.removeRSGroup(barGroup.getName()); - Assert.assertEquals(null, rsGroupAdmin.getRSGroupInfo(barGroup.getName())); - LOG.info("Remove group "+fooGroup.getName()); - rsGroupAdmin.removeRSGroup(fooGroup.getName()); - Assert.assertEquals(null, rsGroupAdmin.getRSGroupInfo(fooGroup.getName())); + public String getGroupName(String baseName) { + return groupPrefix+"_"+baseName+"_"+rand.nextInt(Integer.MAX_VALUE); } - @Test - public void testMoveServersFromDefaultGroup() throws Exception { - //create groups and assign servers - rsGroupAdmin.addRSGroup("foo"); - - RSGroupInfo fooGroup = rsGroupAdmin.getRSGroupInfo("foo"); - assertEquals(0, fooGroup.getServers().size()); - RSGroupInfo defaultGroup = rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP); - - //test remove all servers from default - try { - rsGroupAdmin.moveServers(defaultGroup.getServers(), fooGroup.getName()); - fail(RSGroupAdminServer.KEEP_ONE_SERVER_IN_DEFAULT_ERROR_MESSAGE); - } catch (ConstraintException ex) { - assertTrue(ex.getMessage().contains(RSGroupAdminServer - .KEEP_ONE_SERVER_IN_DEFAULT_ERROR_MESSAGE)); + public static class CPMasterObserver implements MasterCoprocessor, MasterObserver { + boolean preBalanceRSGroupCalled = false; + boolean postBalanceRSGroupCalled = false; + boolean preMoveServersCalled = false; + boolean postMoveServersCalled = false; + boolean preMoveTablesCalled = false; + boolean postMoveTablesCalled = false; + boolean preAddRSGroupCalled = false; + boolean postAddRSGroupCalled = false; + boolean preRemoveRSGroupCalled = false; + boolean postRemoveRSGroupCalled = false; + boolean preRemoveServersCalled = false; + boolean postRemoveServersCalled = false; + boolean preMoveServersAndTables = false; + boolean postMoveServersAndTables = false; + boolean preGetRSGroupInfoCalled = false; + boolean postGetRSGroupInfoCalled = false; + boolean preGetRSGroupInfoOfTableCalled = false; + boolean postGetRSGroupInfoOfTableCalled = false; + boolean preListRSGroupsCalled = false; + boolean postListRSGroupsCalled = false; + boolean preGetRSGroupInfoOfServerCalled = false; + boolean postGetRSGroupInfoOfServerCalled = false; + + public void resetFlags() { + preBalanceRSGroupCalled = false; + postBalanceRSGroupCalled = false; + preMoveServersCalled = false; + postMoveServersCalled = false; + preMoveTablesCalled = false; + postMoveTablesCalled = false; + preAddRSGroupCalled = false; + postAddRSGroupCalled = false; + preRemoveRSGroupCalled = false; + postRemoveRSGroupCalled = false; + preRemoveServersCalled = false; + postRemoveServersCalled = false; + preMoveServersAndTables = false; + postMoveServersAndTables = false; + preGetRSGroupInfoCalled = false; + postGetRSGroupInfoCalled = false; + preGetRSGroupInfoOfTableCalled = false; + postGetRSGroupInfoOfTableCalled = false; + preListRSGroupsCalled = false; + postListRSGroupsCalled = false; + preGetRSGroupInfoOfServerCalled = false; + postGetRSGroupInfoOfServerCalled = false; + } + + @Override + public Optional getMasterObserver() { + return Optional.of(this); } - //test success case, remove one server from default ,keep at least one server - if (defaultGroup.getServers().size() > 1) { - Address serverInDefaultGroup = defaultGroup.getServers().iterator().next(); - LOG.info("moving server " + serverInDefaultGroup + " from group default to group " + - fooGroup.getName()); - rsGroupAdmin.moveServers(Sets.newHashSet(serverInDefaultGroup), fooGroup.getName()); + @Override + public void preMoveServersAndTables(final ObserverContext ctx, + Set
servers, Set tables, String targetGroup) throws IOException { + preMoveServersAndTables = true; } - fooGroup = rsGroupAdmin.getRSGroupInfo("foo"); - LOG.info("moving servers " + fooGroup.getServers() + " to group default"); - rsGroupAdmin.moveServers(fooGroup.getServers(), RSGroupInfo.DEFAULT_GROUP); - - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - return getNumServers() == - rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP).getServers().size(); - } - }); - - fooGroup = rsGroupAdmin.getRSGroupInfo("foo"); - assertEquals(0, fooGroup.getServers().size()); - - //test group removal - LOG.info("Remove group " + fooGroup.getName()); - rsGroupAdmin.removeRSGroup(fooGroup.getName()); - Assert.assertEquals(null, rsGroupAdmin.getRSGroupInfo(fooGroup.getName())); - } - - @Test - public void testTableMoveTruncateAndDrop() throws Exception { - final byte[] familyNameBytes = Bytes.toBytes("f"); - String newGroupName = getGroupName(name.getMethodName()); - final RSGroupInfo newGroup = addGroup(newGroupName, 2); - - TEST_UTIL.createMultiRegionTable(tableName, familyNameBytes, 5); - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - List regions = getTableRegionMap().get(tableName); - if (regions == null) { - return false; - } - - return getTableRegionMap().get(tableName).size() >= 5; - } - }); - - RSGroupInfo tableGrp = rsGroupAdmin.getRSGroupInfoOfTable(tableName); - assertTrue(tableGrp.getName().equals(RSGroupInfo.DEFAULT_GROUP)); - - //change table's group - LOG.info("Moving table "+tableName+" to "+newGroup.getName()); - rsGroupAdmin.moveTables(Sets.newHashSet(tableName), newGroup.getName()); - - //verify group change - Assert.assertEquals(newGroup.getName(), - rsGroupAdmin.getRSGroupInfoOfTable(tableName).getName()); - - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - Map> serverMap = getTableServerRegionMap().get(tableName); - int count = 0; - if (serverMap != null) { - for (ServerName rs : serverMap.keySet()) { - if (newGroup.containsServer(rs.getAddress())) { - count += serverMap.get(rs).size(); - } - } - } - return count == 5; - } - }); - - //test truncate - admin.disableTable(tableName); - admin.truncateTable(tableName, true); - Assert.assertEquals(1, rsGroupAdmin.getRSGroupInfo(newGroup.getName()).getTables().size()); - Assert.assertEquals(tableName, rsGroupAdmin.getRSGroupInfo( - newGroup.getName()).getTables().first()); - - //verify removed table is removed from group - TEST_UTIL.deleteTable(tableName); - Assert.assertEquals(0, rsGroupAdmin.getRSGroupInfo(newGroup.getName()).getTables().size()); - } - - @Test - public void testGroupBalance() throws Exception { - LOG.info(name.getMethodName()); - String newGroupName = getGroupName(name.getMethodName()); - final RSGroupInfo newGroup = addGroup(newGroupName, 3); - - final TableName tableName = TableName.valueOf(tablePrefix+"_ns", name.getMethodName()); - admin.createNamespace( - NamespaceDescriptor.create(tableName.getNamespaceAsString()) - .addConfiguration(RSGroupInfo.NAMESPACE_DESC_PROP_GROUP, newGroupName).build()); - final byte[] familyNameBytes = Bytes.toBytes("f"); - final HTableDescriptor desc = new HTableDescriptor(tableName); - desc.addFamily(new HColumnDescriptor("f")); - byte [] startKey = Bytes.toBytes("aaaaa"); - byte [] endKey = Bytes.toBytes("zzzzz"); - admin.createTable(desc, startKey, endKey, 6); - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - List regions = getTableRegionMap().get(tableName); - if (regions == null) { - return false; - } - return regions.size() >= 6; - } - }); - - //make assignment uneven, move all regions to one server - Map> assignMap = - getTableServerRegionMap().get(tableName); - final ServerName first = assignMap.entrySet().iterator().next().getKey(); - for(RegionInfo region: admin.getTableRegions(tableName)) { - if(!assignMap.get(first).contains(region.getRegionNameAsString())) { - admin.move(region.getEncodedNameAsBytes(), Bytes.toBytes(first.getServerName())); - } + @Override + public void postMoveServersAndTables(final ObserverContext ctx, + Set
servers, Set tables, String targetGroup) throws IOException { + postMoveServersAndTables = true; } - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - Map> map = getTableServerRegionMap().get(tableName); - if (map == null) { - return true; - } - List regions = map.get(first); - if (regions == null) { - return true; - } - return regions.size() >= 6; - } - }); - - //balance the other group and make sure it doesn't affect the new group - admin.setBalancerRunning(true,true); - rsGroupAdmin.balanceRSGroup(RSGroupInfo.DEFAULT_GROUP); - assertEquals(6, getTableServerRegionMap().get(tableName).get(first).size()); - - //disable balance, balancer will not be run and return false - admin.setBalancerRunning(false,true); - assertFalse(rsGroupAdmin.balanceRSGroup(newGroupName)); - assertEquals(6, getTableServerRegionMap().get(tableName).get(first).size()); - //enable balance - admin.setBalancerRunning(true,true); - rsGroupAdmin.balanceRSGroup(newGroupName); - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - for (List regions : getTableServerRegionMap().get(tableName).values()) { - if (2 != regions.size()) { - return false; - } - } - return true; - } - }); - admin.setBalancerRunning(false,true); - } - - @Test - public void testRegionMove() throws Exception { - final RSGroupInfo newGroup = addGroup(getGroupName(name.getMethodName()), 1); - final byte[] familyNameBytes = Bytes.toBytes("f"); - // All the regions created below will be assigned to the default group. - TEST_UTIL.createMultiRegionTable(tableName, familyNameBytes, 6); - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - List regions = getTableRegionMap().get(tableName); - if (regions == null) { - return false; - } - - return getTableRegionMap().get(tableName).size() >= 6; - } - }); - - //get target region to move - Map> assignMap = - getTableServerRegionMap().get(tableName); - String targetRegion = null; - for(ServerName server : assignMap.keySet()) { - targetRegion = assignMap.get(server).size() > 0 ? assignMap.get(server).get(0) : null; - if(targetRegion != null) { - break; - } + @Override + public void preRemoveServers( + final ObserverContext ctx, + Set
servers) throws IOException { + preRemoveServersCalled = true; } - //get server which is not a member of new group - ServerName targetServer = null; - for (ServerName server : admin.getClusterMetrics(EnumSet.of(Option.LIVE_SERVERS)) - .getLiveServerMetrics().keySet()) { - if (!newGroup.containsServer(server.getAddress())) { - targetServer = server; - break; - } - } - - final AdminProtos.AdminService.BlockingInterface targetRS = - ((ClusterConnection) admin.getConnection()).getAdmin(targetServer); - //move target server to group - rsGroupAdmin.moveServers(Sets.newHashSet(targetServer.getAddress()), - newGroup.getName()); - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - return ProtobufUtil.getOnlineRegions(targetRS).size() <= 0; - } - }); - - // Lets move this region to the new group. - TEST_UTIL.getAdmin().move(Bytes.toBytes(RegionInfo.encodeRegionName( - Bytes.toBytes(targetRegion))), Bytes.toBytes(targetServer.getServerName())); - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - return - getTableRegionMap().get(tableName) != null && - getTableRegionMap().get(tableName).size() == 6 && - admin.getClusterMetrics(EnumSet.of(Option.REGIONS_IN_TRANSITION)) - .getRegionStatesInTransition().size() < 1; - } - }); - - //verify that targetServer didn't open it - for (RegionInfo region: ProtobufUtil.getOnlineRegions(targetRS)) { - if (targetRegion.equals(region.getRegionNameAsString())) { - fail("Target server opened region"); - } + @Override + public void postRemoveServers( + final ObserverContext ctx, + Set
servers) throws IOException { + postRemoveServersCalled = true; } - } - @Test - public void testFailRemoveGroup() throws IOException, InterruptedException { - int initNumGroups = rsGroupAdmin.listRSGroups().size(); - addGroup("bar", 3); - TEST_UTIL.createTable(tableName, Bytes.toBytes("f")); - rsGroupAdmin.moveTables(Sets.newHashSet(tableName), "bar"); - RSGroupInfo barGroup = rsGroupAdmin.getRSGroupInfo("bar"); - //group is not empty therefore it should fail - try { - rsGroupAdmin.removeRSGroup(barGroup.getName()); - fail("Expected remove group to fail"); - } catch(IOException e) { - } - //group cannot lose all it's servers therefore it should fail - try { - rsGroupAdmin.moveServers(barGroup.getServers(), RSGroupInfo.DEFAULT_GROUP); - fail("Expected move servers to fail"); - } catch(IOException e) { + @Override + public void preRemoveRSGroup(final ObserverContext ctx, + String name) throws IOException { + preRemoveRSGroupCalled = true; } - rsGroupAdmin.moveTables(barGroup.getTables(), RSGroupInfo.DEFAULT_GROUP); - try { - rsGroupAdmin.removeRSGroup(barGroup.getName()); - fail("Expected move servers to fail"); - } catch(IOException e) { + @Override + public void postRemoveRSGroup(final ObserverContext ctx, + String name) throws IOException { + postRemoveRSGroupCalled = true; } - rsGroupAdmin.moveServers(barGroup.getServers(), RSGroupInfo.DEFAULT_GROUP); - rsGroupAdmin.removeRSGroup(barGroup.getName()); - - Assert.assertEquals(initNumGroups, rsGroupAdmin.listRSGroups().size()); - } - - @Test - public void testKillRS() throws Exception { - RSGroupInfo appInfo = addGroup("appInfo", 1); - - final TableName tableName = TableName.valueOf(tablePrefix+"_ns", name.getMethodName()); - admin.createNamespace( - NamespaceDescriptor.create(tableName.getNamespaceAsString()) - .addConfiguration(RSGroupInfo.NAMESPACE_DESC_PROP_GROUP, appInfo.getName()).build()); - final HTableDescriptor desc = new HTableDescriptor(tableName); - desc.addFamily(new HColumnDescriptor("f")); - admin.createTable(desc); - //wait for created table to be assigned - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - return getTableRegionMap().get(desc.getTableName()) != null; - } - }); - - ServerName targetServer = ServerName.parseServerName( - appInfo.getServers().iterator().next().toString()); - AdminProtos.AdminService.BlockingInterface targetRS = - ((ClusterConnection) admin.getConnection()).getAdmin(targetServer); - RegionInfo targetRegion = ProtobufUtil.getOnlineRegions(targetRS).get(0); - Assert.assertEquals(1, ProtobufUtil.getOnlineRegions(targetRS).size()); - - try { - //stopping may cause an exception - //due to the connection loss - targetRS.stopServer(null, - AdminProtos.StopServerRequest.newBuilder().setReason("Die").build()); - } catch(Exception e) { + @Override + public void preAddRSGroup(final ObserverContext ctx, + String name) throws IOException { + preAddRSGroupCalled = true; } - assertFalse(cluster.getClusterMetrics().getLiveServerMetrics().containsKey(targetServer)); - - //wait for created table to be assigned - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - return cluster.getClusterMetrics().getRegionStatesInTransition().isEmpty(); - } - }); - Set
newServers = Sets.newHashSet(); - newServers.add( - rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP).getServers().iterator().next()); - rsGroupAdmin.moveServers(newServers, appInfo.getName()); - - //Make sure all the table's regions get reassigned - //disabling the table guarantees no conflicting assign/unassign (ie SSH) happens - admin.disableTable(tableName); - admin.enableTable(tableName); - - //wait for region to be assigned - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - return cluster.getClusterMetrics().getRegionStatesInTransition().isEmpty(); - } - }); - - targetServer = ServerName.parseServerName( - newServers.iterator().next().toString()); - targetRS = - ((ClusterConnection) admin.getConnection()).getAdmin(targetServer); - Assert.assertEquals(1, ProtobufUtil.getOnlineRegions(targetRS).size()); - Assert.assertEquals(tableName, - ProtobufUtil.getOnlineRegions(targetRS).get(0).getTable()); - } - - @Test - public void testValidGroupNames() throws IOException { - String[] badNames = {"foo*","foo@","-"}; - String[] goodNames = {"foo_123"}; - for(String entry: badNames) { - try { - rsGroupAdmin.addRSGroup(entry); - fail("Expected a constraint exception for: "+entry); - } catch(ConstraintException ex) { - //expected - } + @Override + public void postAddRSGroup(final ObserverContext ctx, + String name) throws IOException { + postAddRSGroupCalled = true; } - for(String entry: goodNames) { - rsGroupAdmin.addRSGroup(entry); + @Override + public void preMoveTables(final ObserverContext ctx, + Set tables, String targetGroup) throws IOException { + preMoveTablesCalled = true; } - } - - private String getGroupName(String baseName) { - return groupPrefix+"_"+baseName+"_"+rand.nextInt(Integer.MAX_VALUE); - } - - @Test - public void testMultiTableMove() throws Exception { - final TableName tableNameA = TableName.valueOf(tablePrefix + name.getMethodName() + "A"); - final TableName tableNameB = TableName.valueOf(tablePrefix + name.getMethodName() + "B"); - final byte[] familyNameBytes = Bytes.toBytes("f"); - String newGroupName = getGroupName(name.getMethodName()); - final RSGroupInfo newGroup = addGroup(newGroupName, 1); - - TEST_UTIL.createTable(tableNameA, familyNameBytes); - TEST_UTIL.createTable(tableNameB, familyNameBytes); - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - List regionsA = getTableRegionMap().get(tableNameA); - if (regionsA == null) { - return false; - } - - List regionsB = getTableRegionMap().get(tableNameB); - if (regionsB == null) { - return false; - } - - return getTableRegionMap().get(tableNameA).size() >= 1 - && getTableRegionMap().get(tableNameB).size() >= 1; - } - }); - - RSGroupInfo tableGrpA = rsGroupAdmin.getRSGroupInfoOfTable(tableNameA); - assertTrue(tableGrpA.getName().equals(RSGroupInfo.DEFAULT_GROUP)); - - RSGroupInfo tableGrpB = rsGroupAdmin.getRSGroupInfoOfTable(tableNameB); - assertTrue(tableGrpB.getName().equals(RSGroupInfo.DEFAULT_GROUP)); - //change table's group - LOG.info("Moving table [" + tableNameA + "," + tableNameB + "] to " + newGroup.getName()); - rsGroupAdmin.moveTables(Sets.newHashSet(tableNameA, tableNameB), newGroup.getName()); - - //verify group change - Assert.assertEquals(newGroup.getName(), - rsGroupAdmin.getRSGroupInfoOfTable(tableNameA).getName()); - - Assert.assertEquals(newGroup.getName(), - rsGroupAdmin.getRSGroupInfoOfTable(tableNameB).getName()); - - //verify tables' not exist in old group - Set DefaultTables = rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP) - .getTables(); - assertFalse(DefaultTables.contains(tableNameA)); - assertFalse(DefaultTables.contains(tableNameB)); - - //verify tables' exist in new group - Set newGroupTables = rsGroupAdmin.getRSGroupInfo(newGroupName).getTables(); - assertTrue(newGroupTables.contains(tableNameA)); - assertTrue(newGroupTables.contains(tableNameB)); - } - - @Test - public void testDisabledTableMove() throws Exception { - final byte[] familyNameBytes = Bytes.toBytes("f"); - String newGroupName = getGroupName(name.getMethodName()); - final RSGroupInfo newGroup = addGroup(newGroupName, 2); - - TEST_UTIL.createMultiRegionTable(tableName, familyNameBytes, 5); - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - List regions = getTableRegionMap().get(tableName); - if (regions == null) { - return false; - } - return getTableRegionMap().get(tableName).size() >= 5; - } - }); - - RSGroupInfo tableGrp = rsGroupAdmin.getRSGroupInfoOfTable(tableName); - assertTrue(tableGrp.getName().equals(RSGroupInfo.DEFAULT_GROUP)); - //test disable table - admin.disableTable(tableName); - - //change table's group - LOG.info("Moving table "+ tableName + " to " + newGroup.getName()); - rsGroupAdmin.moveTables(Sets.newHashSet(tableName), newGroup.getName()); - - //verify group change - Assert.assertEquals(newGroup.getName(), - rsGroupAdmin.getRSGroupInfoOfTable(tableName).getName()); - } - - @Test - public void testNonExistentTableMove() throws Exception { - TableName tableName = TableName.valueOf(tablePrefix + name.getMethodName()); - - RSGroupInfo tableGrp = rsGroupAdmin.getRSGroupInfoOfTable(tableName); - assertNull(tableGrp); - - //test if table exists already. - boolean exist = admin.tableExists(tableName); - assertFalse(exist); - - LOG.info("Moving table "+ tableName + " to " + RSGroupInfo.DEFAULT_GROUP); - try { - rsGroupAdmin.moveTables(Sets.newHashSet(tableName), RSGroupInfo.DEFAULT_GROUP); - fail("Table " + tableName + " shouldn't have been successfully moved."); - } catch(IOException ex) { - assertTrue(ex instanceof TableNotFoundException); + @Override + public void postMoveTables(final ObserverContext ctx, + Set tables, String targetGroup) throws IOException { + postMoveTablesCalled = true; } - try { - rsGroupAdmin.moveServersAndTables( - Sets.newHashSet(Address.fromParts("bogus",123)), - Sets.newHashSet(tableName), RSGroupInfo.DEFAULT_GROUP); - fail("Table " + tableName + " shouldn't have been successfully moved."); - } catch(IOException ex) { - assertTrue(ex instanceof TableNotFoundException); + @Override + public void preMoveServers(final ObserverContext ctx, + Set
servers, String targetGroup) throws IOException { + preMoveServersCalled = true; } - //verify group change - assertNull(rsGroupAdmin.getRSGroupInfoOfTable(tableName)); - } - @Test - public void testMoveServersAndTables() throws Exception { - LOG.info("testMoveServersAndTables"); - final RSGroupInfo newGroup = addGroup(getGroupName(name.getMethodName()), 1); - //create table - final byte[] familyNameBytes = Bytes.toBytes("f"); - TEST_UTIL.createMultiRegionTable(tableName, familyNameBytes, 5); - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - List regions = getTableRegionMap().get(tableName); - if (regions == null) { - return false; - } - - return getTableRegionMap().get(tableName).size() >= 5; - } - }); - - //get server which is not a member of new group - ServerName targetServer = null; - for(ServerName server : admin.getClusterMetrics(EnumSet.of(Option.LIVE_SERVERS)) - .getLiveServerMetrics().keySet()) { - if(!newGroup.containsServer(server.getAddress()) && - !rsGroupAdmin.getRSGroupInfo("master").containsServer(server.getAddress())) { - targetServer = server; - break; - } + @Override + public void postMoveServers(final ObserverContext ctx, + Set
servers, String targetGroup) throws IOException { + postMoveServersCalled = true; } - LOG.debug("Print group info : " + rsGroupAdmin.listRSGroups()); - int oldDefaultGroupServerSize = - rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP).getServers().size(); - int oldDefaultGroupTableSize = - rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP).getTables().size(); - - //test fail bogus server move - try { - rsGroupAdmin.moveServersAndTables(Sets.newHashSet(Address.fromString("foo:9999")), - Sets.newHashSet(tableName), newGroup.getName()); - fail("Bogus servers shouldn't have been successfully moved."); - } catch(IOException ex) { - String exp = "Source RSGroup for server foo:9999 does not exist."; - String msg = "Expected '" + exp + "' in exception message: "; - assertTrue(msg + " " + ex.getMessage(), ex.getMessage().contains(exp)); + @Override + public void preBalanceRSGroup(final ObserverContext ctx, + String groupName) throws IOException { + preBalanceRSGroupCalled = true; } - //test fail server move - try { - rsGroupAdmin.moveServersAndTables(Sets.newHashSet(targetServer.getAddress()), - Sets.newHashSet(tableName), RSGroupInfo.DEFAULT_GROUP); - fail("servers shouldn't have been successfully moved."); - } catch(IOException ex) { - String exp = "Target RSGroup " + RSGroupInfo.DEFAULT_GROUP + - " is same as source " + RSGroupInfo.DEFAULT_GROUP + " RSGroup."; - String msg = "Expected '" + exp + "' in exception message: "; - assertTrue(msg + " " + ex.getMessage(), ex.getMessage().contains(exp)); + @Override + public void postBalanceRSGroup(final ObserverContext ctx, + String groupName, boolean balancerRan) throws IOException { + postBalanceRSGroupCalled = true; } - //verify default group info - Assert.assertEquals(oldDefaultGroupServerSize, - rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP).getServers().size()); - Assert.assertEquals(oldDefaultGroupTableSize, - rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP).getTables().size()); - - //verify new group info - Assert.assertEquals(1, - rsGroupAdmin.getRSGroupInfo(newGroup.getName()).getServers().size()); - Assert.assertEquals(0, - rsGroupAdmin.getRSGroupInfo(newGroup.getName()).getTables().size()); - - //get all region to move targetServer - List regionList = getTableRegionMap().get(tableName); - for(String region : regionList) { - // Lets move this region to the targetServer - TEST_UTIL.getAdmin().move(Bytes.toBytes(RegionInfo.encodeRegionName(Bytes.toBytes(region))), - Bytes.toBytes(targetServer.getServerName())); + @Override + public void preGetRSGroupInfo(final ObserverContext ctx, + final String groupName) throws IOException { + preGetRSGroupInfoCalled = true; } - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - return getTableRegionMap().get(tableName) != null && - getTableRegionMap().get(tableName).size() == 5 && - getTableServerRegionMap().get(tableName).size() == 1 && - admin.getClusterMetrics(EnumSet.of(Option.REGIONS_IN_TRANSITION)) - .getRegionStatesInTransition().size() < 1; - } - }); - - //verify that all region move to targetServer - Assert.assertEquals(5, getTableServerRegionMap().get(tableName).get(targetServer).size()); - - //move targetServer and table to newGroup - LOG.info("moving server and table to newGroup"); - rsGroupAdmin.moveServersAndTables(Sets.newHashSet(targetServer.getAddress()), - Sets.newHashSet(tableName), newGroup.getName()); - - //verify group change - Assert.assertEquals(newGroup.getName(), - rsGroupAdmin.getRSGroupInfoOfTable(tableName).getName()); - - //verify servers' not exist in old group - Set
defaultServers = rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP) - .getServers(); - assertFalse(defaultServers.contains(targetServer.getAddress())); - - //verify servers' exist in new group - Set
newGroupServers = rsGroupAdmin.getRSGroupInfo(newGroup.getName()).getServers(); - assertTrue(newGroupServers.contains(targetServer.getAddress())); - - //verify tables' not exist in old group - Set defaultTables = rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP) - .getTables(); - assertFalse(defaultTables.contains(tableName)); - - //verify tables' exist in new group - Set newGroupTables = rsGroupAdmin.getRSGroupInfo(newGroup.getName()).getTables(); - assertTrue(newGroupTables.contains(tableName)); - - //verify that all region still assgin on targetServer - Assert.assertEquals(5, getTableServerRegionMap().get(tableName).get(targetServer).size()); - } - - @Test - public void testClearDeadServers() throws Exception { - LOG.info("testClearDeadServers"); - final RSGroupInfo newGroup = addGroup(getGroupName(name.getMethodName()), 3); - NUM_DEAD_SERVERS = cluster.getClusterMetrics().getDeadServerNames().size(); - - ServerName targetServer = ServerName.parseServerName( - newGroup.getServers().iterator().next().toString()); - AdminProtos.AdminService.BlockingInterface targetRS = - ((ClusterConnection) admin.getConnection()).getAdmin(targetServer); - try { - targetServer = ProtobufUtil.toServerName(targetRS.getServerInfo(null, - GetServerInfoRequest.newBuilder().build()).getServerInfo().getServerName()); - //stopping may cause an exception - //due to the connection loss - targetRS.stopServer(null, - AdminProtos.StopServerRequest.newBuilder().setReason("Die").build()); - NUM_DEAD_SERVERS ++; - } catch(Exception e) { + @Override + public void postGetRSGroupInfo(final ObserverContext ctx, + final String groupName) throws IOException { + postGetRSGroupInfoCalled = true; } - //wait for stopped regionserver to dead server list - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - return !master.getServerManager().areDeadServersInProgress() - && cluster.getClusterMetrics().getDeadServerNames().size() == NUM_DEAD_SERVERS; - } - }); - assertFalse(cluster.getClusterMetrics().getLiveServerMetrics().containsKey(targetServer)); - assertTrue(cluster.getClusterMetrics().getDeadServerNames().contains(targetServer)); - assertTrue(newGroup.getServers().contains(targetServer.getAddress())); - - //clear dead servers list - List notClearedServers = admin.clearDeadServers(Lists.newArrayList(targetServer)); - assertEquals(0, notClearedServers.size()); - - Set
newGroupServers = rsGroupAdmin.getRSGroupInfo(newGroup.getName()).getServers(); - assertFalse(newGroupServers.contains(targetServer.getAddress())); - assertEquals(2, newGroupServers.size()); - } - - @Test - public void testRemoveServers() throws Exception { - LOG.info("testRemoveServers"); - final RSGroupInfo newGroup = addGroup(getGroupName(name.getMethodName()), 3); - Iterator
iterator = newGroup.getServers().iterator(); - ServerName targetServer = ServerName.parseServerName(iterator.next().toString()); - // remove online servers - try { - rsGroupAdmin.removeServers(Sets.newHashSet(targetServer.getAddress())); - fail("Online servers shouldn't have been successfully removed."); - } catch(IOException ex) { - String exp = "Server " + targetServer.getAddress() - + " is an online server, not allowed to remove."; - String msg = "Expected '" + exp + "' in exception message: "; - assertTrue(msg + " " + ex.getMessage(), ex.getMessage().contains(exp)); + @Override + public void preGetRSGroupInfoOfTable(final ObserverContext ctx, + final TableName tableName) throws IOException { + preGetRSGroupInfoOfTableCalled = true; } - assertTrue(newGroup.getServers().contains(targetServer.getAddress())); - // remove dead servers - NUM_DEAD_SERVERS = cluster.getClusterMetrics().getDeadServerNames().size(); - AdminProtos.AdminService.BlockingInterface targetRS = - ((ClusterConnection) admin.getConnection()).getAdmin(targetServer); - try { - targetServer = ProtobufUtil.toServerName(targetRS.getServerInfo(null, - GetServerInfoRequest.newBuilder().build()).getServerInfo().getServerName()); - //stopping may cause an exception - //due to the connection loss - LOG.info("stopping server " + targetServer.getHostAndPort()); - targetRS.stopServer(null, - AdminProtos.StopServerRequest.newBuilder().setReason("Die").build()); - NUM_DEAD_SERVERS ++; - } catch(Exception e) { + @Override + public void postGetRSGroupInfoOfTable(final ObserverContext ctx, + final TableName tableName) throws IOException { + postGetRSGroupInfoOfTableCalled = true; } - //wait for stopped regionserver to dead server list - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - return !master.getServerManager().areDeadServersInProgress() - && cluster.getClusterMetrics().getDeadServerNames().size() == NUM_DEAD_SERVERS; - } - }); - - try { - rsGroupAdmin.removeServers(Sets.newHashSet(targetServer.getAddress())); - fail("Dead servers shouldn't have been successfully removed."); - } catch(IOException ex) { - String exp = "Server " + targetServer.getAddress() + " is on the dead servers list," - + " Maybe it will come back again, not allowed to remove."; - String msg = "Expected '" + exp + "' in exception message: "; - assertTrue(msg + " " + ex.getMessage(), ex.getMessage().contains(exp)); + @Override + public void preListRSGroups(final ObserverContext ctx) + throws IOException { + preListRSGroupsCalled = true; } - assertTrue(newGroup.getServers().contains(targetServer.getAddress())); - - // remove decommissioned servers - List serversToDecommission = new ArrayList<>(); - targetServer = ServerName.parseServerName(iterator.next().toString()); - targetRS = ((ClusterConnection) admin.getConnection()).getAdmin(targetServer); - targetServer = ProtobufUtil.toServerName(targetRS.getServerInfo(null, - GetServerInfoRequest.newBuilder().build()).getServerInfo().getServerName()); - assertTrue(master.getServerManager().getOnlineServers().containsKey(targetServer)); - serversToDecommission.add(targetServer); - - admin.decommissionRegionServers(serversToDecommission, true); - assertEquals(1, admin.listDecommissionedRegionServers().size()); - - assertTrue(newGroup.getServers().contains(targetServer.getAddress())); - rsGroupAdmin.removeServers(Sets.newHashSet(targetServer.getAddress())); - Set
newGroupServers = rsGroupAdmin.getRSGroupInfo(newGroup.getName()).getServers(); - assertFalse(newGroupServers.contains(targetServer.getAddress())); - assertEquals(2, newGroupServers.size()); - } - @Test - public void testCreateWhenRsgroupNoOnlineServers() throws Exception { - LOG.info("testCreateWhenRsgroupNoOnlineServers"); - - // set rsgroup has no online servers and test create table - final RSGroupInfo appInfo = addGroup("appInfo", 1); - Iterator
iterator = appInfo.getServers().iterator(); - List serversToDecommission = new ArrayList<>(); - ServerName targetServer = ServerName.parseServerName(iterator.next().toString()); - AdminProtos.AdminService.BlockingInterface targetRS = - ((ClusterConnection) admin.getConnection()).getAdmin(targetServer); - targetServer = ProtobufUtil.toServerName( - targetRS.getServerInfo(null, GetServerInfoRequest.newBuilder().build()).getServerInfo() - .getServerName()); - assertTrue(master.getServerManager().getOnlineServers().containsKey(targetServer)); - serversToDecommission.add(targetServer); - admin.decommissionRegionServers(serversToDecommission, true); - assertEquals(1, admin.listDecommissionedRegionServers().size()); + @Override + public void postListRSGroups(final ObserverContext ctx) + throws IOException { + postListRSGroupsCalled = true; + } - final TableName tableName = TableName.valueOf(tablePrefix + "_ns", name.getMethodName()); - admin.createNamespace(NamespaceDescriptor.create(tableName.getNamespaceAsString()) - .addConfiguration(RSGroupInfo.NAMESPACE_DESC_PROP_GROUP, appInfo.getName()).build()); - final HTableDescriptor desc = new HTableDescriptor(tableName); - desc.addFamily(new HColumnDescriptor("f")); - try { - admin.createTable(desc); - fail("Shouldn't create table successfully!"); - } catch (Exception e) { - LOG.debug("create table error", e); + @Override + public void preGetRSGroupInfoOfServer(final ObserverContext ctx, + final Address server) throws IOException { + preGetRSGroupInfoOfServerCalled = true; } - // recommission and test create table - admin.recommissionRegionServer(targetServer, null); - assertEquals(0, admin.listDecommissionedRegionServers().size()); - admin.createTable(desc); - // wait for created table to be assigned - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override public boolean evaluate() throws Exception { - return getTableRegionMap().get(desc.getTableName()) != null; - } - }); - } - @Test - public void testClearNotProcessedDeadServer() throws Exception { - LOG.info("testClearNotProcessedDeadServer"); - NUM_DEAD_SERVERS = cluster.getClusterMetrics().getDeadServerNames().size(); - RSGroupInfo appInfo = addGroup("deadServerGroup", 1); - ServerName targetServer = - ServerName.parseServerName(appInfo.getServers().iterator().next().toString()); - AdminProtos.AdminService.BlockingInterface targetRS = - ((ClusterConnection) admin.getConnection()).getAdmin(targetServer); - try { - targetServer = ProtobufUtil.toServerName(targetRS.getServerInfo(null, - AdminProtos.GetServerInfoRequest.newBuilder().build()).getServerInfo().getServerName()); - //stopping may cause an exception - //due to the connection loss - targetRS.stopServer(null, - AdminProtos.StopServerRequest.newBuilder().setReason("Die").build()); - NUM_DEAD_SERVERS ++; - } catch(Exception e) { + @Override + public void postGetRSGroupInfoOfServer(final ObserverContext ctx, + final Address server) throws IOException { + postGetRSGroupInfoOfServerCalled = true; } - TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { - @Override - public boolean evaluate() throws Exception { - return cluster.getClusterMetrics().getDeadServerNames().size() == NUM_DEAD_SERVERS; - } - }); - List notClearedServers = admin.clearDeadServers(Lists.newArrayList(targetServer)); - assertEquals(1, notClearedServers.size()); } + } diff --git a/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsBasics.java b/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsBasics.java new file mode 100644 index 000000000000..7415ab571195 --- /dev/null +++ b/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsBasics.java @@ -0,0 +1,320 @@ +/** + * 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.hadoop.hbase.rsgroup; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.HColumnDescriptor; +import org.apache.hadoop.hbase.HTableDescriptor; +import org.apache.hadoop.hbase.NamespaceDescriptor; +import org.apache.hadoop.hbase.ServerName; +import org.apache.hadoop.hbase.TableName; +import org.apache.hadoop.hbase.Waiter; +import org.apache.hadoop.hbase.client.ClusterConnection; +import org.apache.hadoop.hbase.net.Address; +import org.apache.hadoop.hbase.quotas.QuotaTableUtil; +import org.apache.hadoop.hbase.quotas.QuotaUtil; +import org.apache.hadoop.hbase.util.Bytes; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil; +import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos; +import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.GetServerInfoRequest; +import org.apache.hadoop.hbase.testclassification.MediumTests; +import org.apache.hbase.thirdparty.com.google.common.collect.Lists; + +@Category({MediumTests.class}) +public class TestRSGroupsBasics extends TestRSGroupsBase { + + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestRSGroupsBasics.class); + + protected static final Logger LOG = LoggerFactory.getLogger(TestRSGroupsBasics.class); + + @BeforeClass + public static void setUp() throws Exception { + setUpTestBeforeClass(); + } + + @AfterClass + public static void tearDown() throws Exception { + tearDownAfterClass(); + } + + @Before + public void beforeMethod() throws Exception { + setUpBeforeMethod(); + } + + @After + public void afterMethod() throws Exception { + tearDownAfterMethod(); + } + + @Test + public void testBasicStartUp() throws IOException { + RSGroupInfo defaultInfo = rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP); + assertEquals(4, defaultInfo.getServers().size()); + // Assignment of root and meta regions. + int count = master.getAssignmentManager().getRegionStates().getRegionAssignments().size(); + //3 meta,namespace, group + assertEquals(3, count); + } + + @Test + public void testCreateAndDrop() throws Exception { + TEST_UTIL.createTable(tableName, Bytes.toBytes("cf")); + //wait for created table to be assigned + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + return getTableRegionMap().get(tableName) != null; + } + }); + TEST_UTIL.deleteTable(tableName); + } + + @Test + public void testCreateMultiRegion() throws IOException { + byte[] end = {1,3,5,7,9}; + byte[] start = {0,2,4,6,8}; + byte[][] f = {Bytes.toBytes("f")}; + TEST_UTIL.createTable(tableName, f,1,start,end,10); + } + + @Test + public void testNamespaceCreateAndAssign() throws Exception { + LOG.info("testNamespaceCreateAndAssign"); + String nsName = tablePrefix+"_foo"; + final TableName tableName = TableName.valueOf(nsName, tablePrefix + "_testCreateAndAssign"); + RSGroupInfo appInfo = addGroup("appInfo", 1); + admin.createNamespace(NamespaceDescriptor.create(nsName) + .addConfiguration(RSGroupInfo.NAMESPACE_DESC_PROP_GROUP, "appInfo").build()); + final HTableDescriptor desc = new HTableDescriptor(tableName); + desc.addFamily(new HColumnDescriptor("f")); + admin.createTable(desc); + //wait for created table to be assigned + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + return getTableRegionMap().get(desc.getTableName()) != null; + } + }); + ServerName targetServer = + ServerName.parseServerName(appInfo.getServers().iterator().next().toString()); + AdminProtos.AdminService.BlockingInterface rs = + ((ClusterConnection) admin.getConnection()).getAdmin(targetServer); + //verify it was assigned to the right group + Assert.assertEquals(1, ProtobufUtil.getOnlineRegions(rs).size()); + } + + @Test + public void testCreateWhenRsgroupNoOnlineServers() throws Exception { + LOG.info("testCreateWhenRsgroupNoOnlineServers"); + + // set rsgroup has no online servers and test create table + final RSGroupInfo appInfo = addGroup("appInfo", 1); + Iterator
iterator = appInfo.getServers().iterator(); + List serversToDecommission = new ArrayList<>(); + ServerName targetServer = ServerName.parseServerName(iterator.next().toString()); + AdminProtos.AdminService.BlockingInterface targetRS = + ((ClusterConnection) admin.getConnection()).getAdmin(targetServer); + targetServer = ProtobufUtil.toServerName( + targetRS.getServerInfo(null, GetServerInfoRequest.newBuilder().build()).getServerInfo() + .getServerName()); + assertTrue(master.getServerManager().getOnlineServers().containsKey(targetServer)); + serversToDecommission.add(targetServer); + admin.decommissionRegionServers(serversToDecommission, true); + assertEquals(1, admin.listDecommissionedRegionServers().size()); + + final TableName tableName = TableName.valueOf(tablePrefix + "_ns", name.getMethodName()); + admin.createNamespace(NamespaceDescriptor.create(tableName.getNamespaceAsString()) + .addConfiguration(RSGroupInfo.NAMESPACE_DESC_PROP_GROUP, appInfo.getName()).build()); + final HTableDescriptor desc = new HTableDescriptor(tableName); + desc.addFamily(new HColumnDescriptor("f")); + try { + admin.createTable(desc); + fail("Shouldn't create table successfully!"); + } catch (Exception e) { + LOG.debug("create table error", e); + } + + // recommission and test create table + admin.recommissionRegionServer(targetServer, null); + assertEquals(0, admin.listDecommissionedRegionServers().size()); + admin.createTable(desc); + // wait for created table to be assigned + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override public boolean evaluate() throws Exception { + return getTableRegionMap().get(desc.getTableName()) != null; + } + }); + } + + @Test + public void testDefaultNamespaceCreateAndAssign() throws Exception { + LOG.info("testDefaultNamespaceCreateAndAssign"); + String tableName = tablePrefix + "_testCreateAndAssign"; + admin.modifyNamespace(NamespaceDescriptor.create("default") + .addConfiguration(RSGroupInfo.NAMESPACE_DESC_PROP_GROUP, "default").build()); + final HTableDescriptor desc = new HTableDescriptor(TableName.valueOf(tableName)); + desc.addFamily(new HColumnDescriptor("f")); + admin.createTable(desc); + //wait for created table to be assigned + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + return getTableRegionMap().get(desc.getTableName()) != null; + } + }); + } + + @Test + public void testCloneSnapshot() throws Exception { + byte[] FAMILY = Bytes.toBytes("test"); + String snapshotName = tableName.getNameAsString() + "_snap"; + TableName clonedTableName = TableName.valueOf(tableName.getNameAsString() + "_clone"); + + // create base table + TEST_UTIL.createTable(tableName, FAMILY); + + // create snapshot + admin.snapshot(snapshotName, tableName); + + // clone + admin.cloneSnapshot(snapshotName, clonedTableName); + } + + @Test + public void testClearDeadServers() throws Exception { + LOG.info("testClearDeadServers"); + final RSGroupInfo newGroup = addGroup(getGroupName(name.getMethodName()), 3); + NUM_DEAD_SERVERS = cluster.getClusterMetrics().getDeadServerNames().size(); + + ServerName targetServer = ServerName.parseServerName( + newGroup.getServers().iterator().next().toString()); + AdminProtos.AdminService.BlockingInterface targetRS = + ((ClusterConnection) admin.getConnection()).getAdmin(targetServer); + try { + targetServer = ProtobufUtil.toServerName(targetRS.getServerInfo(null, + GetServerInfoRequest.newBuilder().build()).getServerInfo().getServerName()); + //stopping may cause an exception + //due to the connection loss + targetRS.stopServer(null, + AdminProtos.StopServerRequest.newBuilder().setReason("Die").build()); + NUM_DEAD_SERVERS ++; + } catch(Exception e) { + } + //wait for stopped regionserver to dead server list + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + return !master.getServerManager().areDeadServersInProgress() + && cluster.getClusterMetrics().getDeadServerNames().size() == NUM_DEAD_SERVERS; + } + }); + assertFalse(cluster.getClusterMetrics().getLiveServerMetrics().containsKey(targetServer)); + assertTrue(cluster.getClusterMetrics().getDeadServerNames().contains(targetServer)); + assertTrue(newGroup.getServers().contains(targetServer.getAddress())); + + //clear dead servers list + List notClearedServers = admin.clearDeadServers(Lists.newArrayList(targetServer)); + assertEquals(0, notClearedServers.size()); + + Set
newGroupServers = rsGroupAdmin.getRSGroupInfo(newGroup.getName()).getServers(); + assertFalse(newGroupServers.contains(targetServer.getAddress())); + assertEquals(2, newGroupServers.size()); + } + + @Test + public void testClearNotProcessedDeadServer() throws Exception { + LOG.info("testClearNotProcessedDeadServer"); + NUM_DEAD_SERVERS = cluster.getClusterMetrics().getDeadServerNames().size(); + RSGroupInfo appInfo = addGroup("deadServerGroup", 1); + ServerName targetServer = + ServerName.parseServerName(appInfo.getServers().iterator().next().toString()); + AdminProtos.AdminService.BlockingInterface targetRS = + ((ClusterConnection) admin.getConnection()).getAdmin(targetServer); + try { + targetServer = ProtobufUtil.toServerName(targetRS.getServerInfo(null, + AdminProtos.GetServerInfoRequest.newBuilder().build()).getServerInfo().getServerName()); + //stopping may cause an exception + //due to the connection loss + targetRS.stopServer(null, + AdminProtos.StopServerRequest.newBuilder().setReason("Die").build()); + NUM_DEAD_SERVERS ++; + } catch(Exception e) { + } + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + return cluster.getClusterMetrics().getDeadServerNames().size() == NUM_DEAD_SERVERS; + } + }); + List notClearedServers = + admin.clearDeadServers(Lists.newArrayList(targetServer)); + assertEquals(1, notClearedServers.size()); + } + + @Test + public void testRSGroupsWithHBaseQuota() throws Exception { + TEST_UTIL.getConfiguration().setBoolean(QuotaUtil.QUOTA_CONF_KEY, true); + restartHBaseCluster(); + try { + TEST_UTIL.waitFor(90000, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + return admin.isTableAvailable(QuotaTableUtil.QUOTA_TABLE_NAME); + } + }); + } finally { + TEST_UTIL.getConfiguration().setBoolean(QuotaUtil.QUOTA_CONF_KEY, false); + restartHBaseCluster(); + } + } + + private void restartHBaseCluster() throws Exception { + LOG.info("\n\nShutting down cluster"); + TEST_UTIL.shutdownMiniHBaseCluster(); + LOG.info("\n\nSleeping a bit"); + Thread.sleep(2000); + TEST_UTIL.restartHBaseCluster(NUM_SLAVES_BASE - 1); + initialize(); + } +} diff --git a/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsKillRS.java b/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsKillRS.java new file mode 100644 index 000000000000..bce4cc9af0af --- /dev/null +++ b/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsKillRS.java @@ -0,0 +1,149 @@ +/** + * 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.hadoop.hbase.rsgroup; + +import static org.junit.Assert.assertFalse; + +import java.util.Set; + +import org.apache.hadoop.hbase.HBaseClassTestRule; +import org.apache.hadoop.hbase.HColumnDescriptor; +import org.apache.hadoop.hbase.HTableDescriptor; +import org.apache.hadoop.hbase.NamespaceDescriptor; +import org.apache.hadoop.hbase.ServerName; +import org.apache.hadoop.hbase.TableName; +import org.apache.hadoop.hbase.Waiter; +import org.apache.hadoop.hbase.client.ClusterConnection; +import org.apache.hadoop.hbase.client.RegionInfo; +import org.apache.hadoop.hbase.net.Address; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil; +import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos; +import org.apache.hadoop.hbase.testclassification.MediumTests; +import org.apache.hbase.thirdparty.com.google.common.collect.Sets; + +@Category({MediumTests.class}) +public class TestRSGroupsKillRS extends TestRSGroupsBase { + + @ClassRule + public static final HBaseClassTestRule CLASS_RULE = + HBaseClassTestRule.forClass(TestRSGroupsKillRS.class); + + protected static final Logger LOG = LoggerFactory.getLogger(TestRSGroupsKillRS.class); + + @BeforeClass + public static void setUp() throws Exception { + setUpTestBeforeClass(); + } + + @AfterClass + public static void tearDown() throws Exception { + tearDownAfterClass(); + } + + @Before + public void beforeMethod() throws Exception { + setUpBeforeMethod(); + } + + @After + public void afterMethod() throws Exception { + tearDownAfterMethod(); + } + + @Test + public void testKillRS() throws Exception { + RSGroupInfo appInfo = addGroup("appInfo", 1); + + final TableName tableName = TableName.valueOf(tablePrefix+"_ns", name.getMethodName()); + admin.createNamespace( + NamespaceDescriptor.create(tableName.getNamespaceAsString()) + .addConfiguration(RSGroupInfo.NAMESPACE_DESC_PROP_GROUP, appInfo.getName()).build()); + final HTableDescriptor desc = new HTableDescriptor(tableName); + desc.addFamily(new HColumnDescriptor("f")); + admin.createTable(desc); + //wait for created table to be assigned + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + return getTableRegionMap().get(desc.getTableName()) != null; + } + }); + + ServerName targetServer = ServerName.parseServerName( + appInfo.getServers().iterator().next().toString()); + AdminProtos.AdminService.BlockingInterface targetRS = + ((ClusterConnection) admin.getConnection()).getAdmin(targetServer); + RegionInfo targetRegion = ProtobufUtil.getOnlineRegions(targetRS).get(0); + Assert.assertEquals(1, ProtobufUtil.getOnlineRegions(targetRS).size()); + + try { + //stopping may cause an exception + //due to the connection loss + targetRS.stopServer(null, + AdminProtos.StopServerRequest.newBuilder().setReason("Die").build()); + } catch(Exception e) { + } + assertFalse(cluster.getClusterMetrics().getLiveServerMetrics().containsKey(targetServer)); + + //wait for created table to be assigned + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + return cluster.getClusterMetrics().getRegionStatesInTransition().isEmpty(); + } + }); + Set
newServers = Sets.newHashSet(); + newServers.add( + rsGroupAdmin.getRSGroupInfo(RSGroupInfo.DEFAULT_GROUP).getServers().iterator().next()); + rsGroupAdmin.moveServers(newServers, appInfo.getName()); + + //Make sure all the table's regions get reassigned + //disabling the table guarantees no conflicting assign/unassign (ie SSH) happens + admin.disableTable(tableName); + admin.enableTable(tableName); + + //wait for region to be assigned + TEST_UTIL.waitFor(WAIT_TIMEOUT, new Waiter.Predicate() { + @Override + public boolean evaluate() throws Exception { + return cluster.getClusterMetrics().getRegionStatesInTransition().isEmpty(); + } + }); + + targetServer = ServerName.parseServerName( + newServers.iterator().next().toString()); + targetRS = + ((ClusterConnection) admin.getConnection()).getAdmin(targetServer); + Assert.assertEquals(1, ProtobufUtil.getOnlineRegions(targetRS).size()); + Assert.assertEquals(tableName, + ProtobufUtil.getOnlineRegions(targetRS).get(0).getTable()); + } + +} diff --git a/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsOfflineMode.java b/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsOfflineMode.java index 1d5881e2e439..b93de57bd8bb 100644 --- a/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsOfflineMode.java +++ b/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsOfflineMode.java @@ -36,6 +36,7 @@ import org.apache.hadoop.hbase.regionserver.HRegionServer; import org.apache.hadoop.hbase.testclassification.MediumTests; import org.apache.hadoop.hbase.util.Bytes; + import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; diff --git a/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsWithACL.java b/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsWithACL.java index a63626d99472..b6f6463ac9d7 100644 --- a/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsWithACL.java +++ b/hbase-rsgroup/src/test/java/org/apache/hadoop/hbase/rsgroup/TestRSGroupsWithACL.java @@ -41,6 +41,7 @@ import org.apache.hadoop.hbase.testclassification.MediumTests; import org.apache.hadoop.hbase.testclassification.SecurityTests; import org.apache.hadoop.hbase.util.Bytes; + import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.ClassRule; @@ -177,9 +178,10 @@ private static void setUpTableAndUserPermissions() throws Exception { try { assertEquals(4, AccessControlClient.getUserPermissions(systemUserConnection, TEST_TABLE.toString()).size()); + } catch (AssertionError e) { + fail(e.getMessage()); } catch (Throwable e) { LOG.error("error during call of AccessControlClient.getUserPermissions. ", e); - fail("error during call of AccessControlClient.getUserPermissions."); } } diff --git a/hbase-rsgroup/src/test/resources/log4j.properties b/hbase-rsgroup/src/test/resources/log4j.properties new file mode 100644 index 000000000000..c322699ced24 --- /dev/null +++ b/hbase-rsgroup/src/test/resources/log4j.properties @@ -0,0 +1,68 @@ +# 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. + +# Define some default values that can be overridden by system properties +hbase.root.logger=INFO,console +hbase.log.dir=. +hbase.log.file=hbase.log + +# Define the root logger to the system property "hbase.root.logger". +log4j.rootLogger=${hbase.root.logger} + +# Logging Threshold +log4j.threshold=ALL + +# +# Daily Rolling File Appender +# +log4j.appender.DRFA=org.apache.log4j.DailyRollingFileAppender +log4j.appender.DRFA.File=${hbase.log.dir}/${hbase.log.file} + +# Rollver at midnight +log4j.appender.DRFA.DatePattern=.yyyy-MM-dd + +# 30-day backup +#log4j.appender.DRFA.MaxBackupIndex=30 +log4j.appender.DRFA.layout=org.apache.log4j.PatternLayout +# Debugging Pattern format +log4j.appender.DRFA.layout.ConversionPattern=%d{ISO8601} %-5p [%t] %C{2}(%L): %m%n + + +# +# console +# Add "console" to rootlogger above if you want to use this +# +log4j.appender.console=org.apache.log4j.ConsoleAppender +log4j.appender.console.target=System.err +log4j.appender.console.layout=org.apache.log4j.PatternLayout +log4j.appender.console.layout.ConversionPattern=%d{ISO8601} %-5p [%t] %C{2}(%L): %m%n + +# Custom Logging levels + +#log4j.logger.org.apache.hadoop.fs.FSNamesystem=DEBUG + +log4j.logger.org.apache.hadoop=WARN +log4j.logger.org.apache.zookeeper=ERROR +log4j.logger.org.apache.hadoop.hbase=DEBUG + +#These settings are workarounds against spurious logs from the minicluster. +#See HBASE-4709 +log4j.logger.org.apache.hadoop.metrics2.impl.MetricsConfig=WARN +log4j.logger.org.apache.hadoop.metrics2.impl.MetricsSinkAdapter=WARN +log4j.logger.org.apache.hadoop.metrics2.impl.MetricsSystemImpl=WARN +log4j.logger.org.apache.hadoop.metrics2.util.MBeans=WARN +# Enable this to get detailed connection error/retry logging. +# log4j.logger.org.apache.hadoop.hbase.client.ConnectionImplementation=TRACE diff --git a/hbase-server/pom.xml b/hbase-server/pom.xml index 473b7409de74..54558f7f77cb 100644 --- a/hbase-server/pom.xml +++ b/hbase-server/pom.xml @@ -533,14 +533,20 @@ test - org.apache.hadoop - hadoop-minikdc + org.bouncycastle + bcprov-jdk15on test - org.bouncycastle - bcprov-jdk16 + org.apache.hadoop + hadoop-minikdc test + + + bouncycastle + bcprov-jdk15 + + org.apache.kerby diff --git a/hbase-server/src/main/jamon/org/apache/hadoop/hbase/tmpl/common/TaskMonitorTmpl.jamon b/hbase-server/src/main/jamon/org/apache/hadoop/hbase/tmpl/common/TaskMonitorTmpl.jamon index 9f27d4683046..c4dff1458f23 100644 --- a/hbase-server/src/main/jamon/org/apache/hadoop/hbase/tmpl/common/TaskMonitorTmpl.jamon +++ b/hbase-server/src/main/jamon/org/apache/hadoop/hbase/tmpl/common/TaskMonitorTmpl.jamon @@ -30,7 +30,7 @@ String format = "html"; <%if format.equals("json")%> <& renderTasks; filter=filter &> <%else> -

Tasks

+

Tasks