001/**
002 *
003 * Licensed to the Apache Software Foundation (ASF) under one
004 * or more contributor license agreements.  See the NOTICE file
005 * distributed with this work for additional information
006 * regarding copyright ownership.  The ASF licenses this file
007 * to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance
009 * with the License.  You may obtain a copy of the License at
010 *
011 *     http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing, software
014 * distributed under the License is distributed on an "AS IS" BASIS,
015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
016 * See the License for the specific language governing permissions and
017 * limitations under the License.
018 */
019
020package org.apache.hadoop.hbase.util;
021
022import java.io.BufferedInputStream;
023import java.io.BufferedOutputStream;
024import java.io.Closeable;
025import java.io.DataInputStream;
026import java.io.DataOutputStream;
027import java.io.File;
028import java.io.FileInputStream;
029import java.io.FileOutputStream;
030import java.io.IOException;
031import java.nio.file.Files;
032import java.nio.file.Paths;
033import java.util.ArrayList;
034import java.util.Collections;
035import java.util.EnumSet;
036import java.util.HashSet;
037import java.util.Iterator;
038import java.util.List;
039import java.util.Locale;
040import java.util.Set;
041import java.util.concurrent.CancellationException;
042import java.util.concurrent.ExecutionException;
043import java.util.concurrent.ExecutorService;
044import java.util.concurrent.Executors;
045import java.util.concurrent.Future;
046import java.util.concurrent.TimeUnit;
047import java.util.concurrent.TimeoutException;
048import java.util.function.Predicate;
049import org.apache.commons.io.IOUtils;
050import org.apache.hadoop.conf.Configuration;
051import org.apache.hadoop.hbase.ClusterMetrics.Option;
052import org.apache.hadoop.hbase.HBaseConfiguration;
053import org.apache.hadoop.hbase.HConstants;
054import org.apache.hadoop.hbase.ServerName;
055import org.apache.hadoop.hbase.UnknownRegionException;
056import org.apache.hadoop.hbase.client.Admin;
057import org.apache.hadoop.hbase.client.Connection;
058import org.apache.hadoop.hbase.client.ConnectionFactory;
059import org.apache.hadoop.hbase.client.DoNotRetryRegionException;
060import org.apache.hadoop.hbase.client.RegionInfo;
061import org.apache.hadoop.hbase.master.assignment.AssignmentManager;
062import org.apache.yetus.audience.InterfaceAudience;
063import org.slf4j.Logger;
064import org.slf4j.LoggerFactory;
065
066import org.apache.hbase.thirdparty.com.google.common.annotations.VisibleForTesting;
067import org.apache.hbase.thirdparty.org.apache.commons.cli.CommandLine;
068import org.apache.hbase.thirdparty.org.apache.commons.collections4.CollectionUtils;
069
070/**
071 * Tool for loading/unloading regions to/from given regionserver This tool can be run from Command
072 * line directly as a utility. Supports Ack/No Ack mode for loading/unloading operations.Ack mode
073 * acknowledges if regions are online after movement while noAck mode is best effort mode that
074 * improves performance but will still move on if region is stuck/not moved. Motivation behind noAck
075 * mode being RS shutdown where even if a Region is stuck, upon shutdown master will move it
076 * anyways. This can also be used by constructiong an Object using the builder and then calling
077 * {@link #load()} or {@link #unload()} methods for the desired operations.
078 */
079@InterfaceAudience.Public
080public class RegionMover extends AbstractHBaseTool implements Closeable {
081  public static final String MOVE_RETRIES_MAX_KEY = "hbase.move.retries.max";
082  public static final String MOVE_WAIT_MAX_KEY = "hbase.move.wait.max";
083  public static final String SERVERSTART_WAIT_MAX_KEY = "hbase.serverstart.wait.max";
084  public static final int DEFAULT_MOVE_RETRIES_MAX = 5;
085  public static final int DEFAULT_MOVE_WAIT_MAX = 60;
086  public static final int DEFAULT_SERVERSTART_WAIT_MAX = 180;
087
088  private static final Logger LOG = LoggerFactory.getLogger(RegionMover.class);
089
090  private RegionMoverBuilder rmbuilder;
091  private boolean ack = true;
092  private int maxthreads = 1;
093  private int timeout;
094  private String loadUnload;
095  private String hostname;
096  private String filename;
097  private String excludeFile;
098  private int port;
099  private Connection conn;
100  private Admin admin;
101
102  private RegionMover(RegionMoverBuilder builder) throws IOException {
103    this.hostname = builder.hostname;
104    this.filename = builder.filename;
105    this.excludeFile = builder.excludeFile;
106    this.maxthreads = builder.maxthreads;
107    this.ack = builder.ack;
108    this.port = builder.port;
109    this.timeout = builder.timeout;
110    setConf(builder.conf);
111    this.conn = ConnectionFactory.createConnection(conf);
112    this.admin = conn.getAdmin();
113  }
114
115  private RegionMover() {
116  }
117
118  @Override
119  public void close() {
120    IOUtils.closeQuietly(this.admin, e -> LOG.warn("failed to close admin", e));
121    IOUtils.closeQuietly(this.conn, e -> LOG.warn("failed to close conn", e));
122  }
123
124  /**
125   * Builder for Region mover. Use the {@link #build()} method to create RegionMover object. Has
126   * {@link #filename(String)}, {@link #excludeFile(String)}, {@link #maxthreads(int)},
127   * {@link #ack(boolean)}, {@link #timeout(int)} methods to set the corresponding options
128   */
129  public static class RegionMoverBuilder {
130    private boolean ack = true;
131    private int maxthreads = 1;
132    private int timeout = Integer.MAX_VALUE;
133    private String hostname;
134    private String filename;
135    private String excludeFile = null;
136    private String defaultDir = System.getProperty("java.io.tmpdir");
137    @VisibleForTesting
138    final int port;
139    private final Configuration conf;
140
141    public RegionMoverBuilder(String hostname) {
142      this(hostname, createConf());
143    }
144
145    /**
146     * Creates a new configuration and sets region mover specific overrides
147     */
148    private static Configuration createConf() {
149      Configuration conf = HBaseConfiguration.create();
150      conf.setInt("hbase.client.prefetch.limit", 1);
151      conf.setInt("hbase.client.pause", 500);
152      conf.setInt("hbase.client.retries.number", 100);
153      return conf;
154    }
155
156    /**
157     * @param hostname Hostname to unload regions from or load regions to. Can be either hostname
158     *     or hostname:port.
159     * @param conf Configuration object
160     */
161    public RegionMoverBuilder(String hostname, Configuration conf) {
162      String[] splitHostname = hostname.toLowerCase().split(":");
163      this.hostname = splitHostname[0];
164      if (splitHostname.length == 2) {
165        this.port = Integer.parseInt(splitHostname[1]);
166      } else {
167        this.port = conf.getInt(HConstants.REGIONSERVER_PORT, HConstants.DEFAULT_REGIONSERVER_PORT);
168      }
169      this.filename = defaultDir + File.separator + System.getProperty("user.name") + this.hostname
170        + ":" + Integer.toString(this.port);
171      this.conf = conf;
172    }
173
174    /**
175     * Path of file where regions will be written to during unloading/read from during loading
176     * @param filename
177     * @return RegionMoverBuilder object
178     */
179    public RegionMoverBuilder filename(String filename) {
180      this.filename = filename;
181      return this;
182    }
183
184    /**
185     * Set the max number of threads that will be used to move regions
186     */
187    public RegionMoverBuilder maxthreads(int threads) {
188      this.maxthreads = threads;
189      return this;
190    }
191
192    /**
193     * Path of file containing hostnames to be excluded during region movement. Exclude file should
194     * have 'host:port' per line. Port is mandatory here as we can have many RS running on a single
195     * host.
196     */
197    public RegionMoverBuilder excludeFile(String excludefile) {
198      this.excludeFile = excludefile;
199      return this;
200    }
201
202    /**
203     * Set ack/noAck mode.
204     * <p>
205     * In ack mode regions are acknowledged before and after moving and the move is retried
206     * hbase.move.retries.max times, if unsuccessful we quit with exit code 1.No Ack mode is a best
207     * effort mode,each region movement is tried once.This can be used during graceful shutdown as
208     * even if we have a stuck region,upon shutdown it'll be reassigned anyway.
209     * <p>
210     * @param ack
211     * @return RegionMoverBuilder object
212     */
213    public RegionMoverBuilder ack(boolean ack) {
214      this.ack = ack;
215      return this;
216    }
217
218    /**
219     * Set the timeout for Load/Unload operation in seconds.This is a global timeout,threadpool for
220     * movers also have a separate time which is hbase.move.wait.max * number of regions to
221     * load/unload
222     * @param timeout in seconds
223     * @return RegionMoverBuilder object
224     */
225    public RegionMoverBuilder timeout(int timeout) {
226      this.timeout = timeout;
227      return this;
228    }
229
230    /**
231     * This method builds the appropriate RegionMover object which can then be used to load/unload
232     * using load and unload methods
233     * @return RegionMover object
234     */
235    public RegionMover build() throws IOException {
236      return new RegionMover(this);
237    }
238  }
239
240  /**
241   * Loads the specified {@link #hostname} with regions listed in the {@link #filename} RegionMover
242   * Object has to be created using {@link #RegionMover(RegionMoverBuilder)}
243   * @return true if loading succeeded, false otherwise
244   */
245  public boolean load() throws ExecutionException, InterruptedException, TimeoutException {
246    ExecutorService loadPool = Executors.newFixedThreadPool(1);
247    Future<Boolean> loadTask = loadPool.submit(() -> {
248      try {
249        List<RegionInfo> regionsToMove = readRegionsFromFile(filename);
250        if (regionsToMove.isEmpty()) {
251          LOG.info("No regions to load.Exiting");
252          return true;
253        }
254        loadRegions(regionsToMove);
255      } catch (Exception e) {
256        LOG.error("Error while loading regions to " + hostname, e);
257        return false;
258      }
259      return true;
260    });
261    return waitTaskToFinish(loadPool, loadTask, "loading");
262  }
263
264  private void loadRegions(List<RegionInfo> regionsToMove)
265      throws Exception {
266    ServerName server = getTargetServer();
267    List<RegionInfo> movedRegions = Collections.synchronizedList(new ArrayList<>());
268    LOG.info(
269        "Moving " + regionsToMove.size() + " regions to " + server + " using " + this.maxthreads
270            + " threads.Ack mode:" + this.ack);
271
272    final ExecutorService moveRegionsPool = Executors.newFixedThreadPool(this.maxthreads);
273    List<Future<Boolean>> taskList = new ArrayList<>();
274    int counter = 0;
275    while (counter < regionsToMove.size()) {
276      RegionInfo region = regionsToMove.get(counter);
277      ServerName currentServer = MoveWithAck.getServerNameForRegion(region, admin, conn);
278      if (currentServer == null) {
279        LOG.warn(
280            "Could not get server for Region:" + region.getRegionNameAsString() + " moving on");
281        counter++;
282        continue;
283      } else if (server.equals(currentServer)) {
284        LOG.info(
285            "Region " + region.getRegionNameAsString() + " is already on target server=" + server);
286        counter++;
287        continue;
288      }
289      if (ack) {
290        Future<Boolean> task = moveRegionsPool
291          .submit(new MoveWithAck(conn, region, currentServer, server, movedRegions));
292        taskList.add(task);
293      } else {
294        Future<Boolean> task = moveRegionsPool
295          .submit(new MoveWithoutAck(admin, region, currentServer, server, movedRegions));
296        taskList.add(task);
297      }
298      counter++;
299    }
300
301    moveRegionsPool.shutdown();
302    long timeoutInSeconds = regionsToMove.size() * admin.getConfiguration()
303        .getLong(MOVE_WAIT_MAX_KEY, DEFAULT_MOVE_WAIT_MAX);
304    waitMoveTasksToFinish(moveRegionsPool, taskList, timeoutInSeconds);
305  }
306
307  /**
308   * Unload regions from given {@link #hostname} using ack/noAck mode and {@link #maxthreads}.In
309   * noAck mode we do not make sure that region is successfully online on the target region
310   * server,hence it is best effort.We do not unload regions to hostnames given in
311   * {@link #excludeFile}.
312   * @return true if unloading succeeded, false otherwise
313   */
314  public boolean unload() throws InterruptedException, ExecutionException, TimeoutException {
315    deleteFile(this.filename);
316    ExecutorService unloadPool = Executors.newFixedThreadPool(1);
317    Future<Boolean> unloadTask = unloadPool.submit(() -> {
318      List<RegionInfo> movedRegions = Collections.synchronizedList(new ArrayList<>());
319      try {
320        // Get Online RegionServers
321        List<ServerName> regionServers = new ArrayList<>();
322        regionServers.addAll(admin.getRegionServers());
323        // Remove the host Region server from target Region Servers list
324        ServerName server = stripServer(regionServers, hostname, port);
325        if (server == null) {
326          LOG.info("Could not find server '{}:{}' in the set of region servers. giving up.",
327              hostname, port);
328          LOG.debug("List of region servers: {}", regionServers);
329          return false;
330        }
331        // Remove RS present in the exclude file
332        stripExcludes(regionServers);
333
334        // Remove decommissioned RS
335        Set<ServerName> decommissionedRS = new HashSet<>(admin.listDecommissionedRegionServers());
336        if (CollectionUtils.isNotEmpty(decommissionedRS)) {
337          regionServers.removeIf(decommissionedRS::contains);
338          LOG.debug("Excluded RegionServers from unloading regions to because they " +
339            "are marked as decommissioned. Servers: {}", decommissionedRS);
340        }
341
342        stripMaster(regionServers);
343        if (regionServers.isEmpty()) {
344          LOG.warn("No Regions were moved - no servers available");
345          return false;
346        }
347        unloadRegions(server, regionServers, movedRegions);
348      } catch (Exception e) {
349        LOG.error("Error while unloading regions ", e);
350        return false;
351      } finally {
352        if (movedRegions != null) {
353          writeFile(filename, movedRegions);
354        }
355      }
356      return true;
357    });
358    return waitTaskToFinish(unloadPool, unloadTask, "unloading");
359  }
360
361  private void unloadRegions(ServerName server, List<ServerName> regionServers,
362      List<RegionInfo> movedRegions) throws Exception {
363    while (true) {
364      List<RegionInfo> regionsToMove = admin.getRegions(server);
365      regionsToMove.removeAll(movedRegions);
366      if (regionsToMove.isEmpty()) {
367        LOG.info("No Regions to move....Quitting now");
368        break;
369      }
370      LOG.info("Moving {} regions from {} to {} servers using {} threads .Ack Mode: {}",
371        regionsToMove.size(), this.hostname, regionServers.size(), this.maxthreads, ack);
372      final ExecutorService moveRegionsPool = Executors.newFixedThreadPool(this.maxthreads);
373      List<Future<Boolean>> taskList = new ArrayList<>();
374      int serverIndex = 0;
375      for (RegionInfo regionToMove : regionsToMove) {
376        if (ack) {
377          Future<Boolean> task = moveRegionsPool.submit(
378            new MoveWithAck(conn, regionToMove, server, regionServers.get(serverIndex),
379              movedRegions));
380          taskList.add(task);
381        } else {
382          Future<Boolean> task = moveRegionsPool.submit(
383            new MoveWithoutAck(admin, regionToMove, server, regionServers.get(serverIndex),
384              movedRegions));
385          taskList.add(task);
386        }
387        serverIndex = (serverIndex + 1) % regionServers.size();
388      }
389      moveRegionsPool.shutdown();
390      long timeoutInSeconds = regionsToMove.size() * admin.getConfiguration()
391          .getLong(MOVE_WAIT_MAX_KEY, DEFAULT_MOVE_WAIT_MAX);
392      waitMoveTasksToFinish(moveRegionsPool, taskList, timeoutInSeconds);
393    }
394  }
395
396  private boolean waitTaskToFinish(ExecutorService pool, Future<Boolean> task, String operation)
397      throws TimeoutException, InterruptedException, ExecutionException {
398    pool.shutdown();
399    try {
400      if (!pool.awaitTermination((long) this.timeout, TimeUnit.SECONDS)) {
401        LOG.warn(
402            "Timed out before finishing the " + operation + " operation. Timeout: " + this.timeout
403                + "sec");
404        pool.shutdownNow();
405      }
406    } catch (InterruptedException e) {
407      pool.shutdownNow();
408      Thread.currentThread().interrupt();
409    }
410    try {
411      return task.get(5, TimeUnit.SECONDS);
412    } catch (InterruptedException e) {
413      LOG.warn("Interrupted while " + operation + " Regions on " + this.hostname, e);
414      throw e;
415    } catch (ExecutionException e) {
416      LOG.error("Error while " + operation + " regions on RegionServer " + this.hostname, e);
417      throw e;
418    }
419  }
420
421  private void waitMoveTasksToFinish(ExecutorService moveRegionsPool,
422      List<Future<Boolean>> taskList, long timeoutInSeconds) throws Exception {
423    try {
424      if (!moveRegionsPool.awaitTermination(timeoutInSeconds, TimeUnit.SECONDS)) {
425        moveRegionsPool.shutdownNow();
426      }
427    } catch (InterruptedException e) {
428      moveRegionsPool.shutdownNow();
429      Thread.currentThread().interrupt();
430    }
431    for (Future<Boolean> future : taskList) {
432      try {
433        // if even after shutdownNow threads are stuck we wait for 5 secs max
434        if (!future.get(5, TimeUnit.SECONDS)) {
435          LOG.error("Was Not able to move region....Exiting Now");
436          throw new Exception("Could not move region Exception");
437        }
438      } catch (InterruptedException e) {
439        LOG.error("Interrupted while waiting for Thread to Complete " + e.getMessage(), e);
440        throw e;
441      } catch (ExecutionException e) {
442        boolean ignoreFailure = ignoreRegionMoveFailure(e);
443        if (ignoreFailure) {
444          LOG.debug("Ignore region move failure, it might have been split/merged.", e);
445        } else {
446          LOG.error("Got Exception From Thread While moving region {}", e.getMessage(), e);
447          throw e;
448        }
449      } catch (CancellationException e) {
450        LOG.error("Thread for moving region cancelled. Timeout for cancellation:" + timeoutInSeconds
451            + "secs", e);
452        throw e;
453      }
454    }
455  }
456
457  private boolean ignoreRegionMoveFailure(ExecutionException e) {
458    boolean ignoreFailure = false;
459    if (e.getCause() instanceof UnknownRegionException) {
460      // region does not exist anymore
461      ignoreFailure = true;
462    } else if (e.getCause() instanceof DoNotRetryRegionException
463        && e.getCause().getMessage() != null && e.getCause().getMessage()
464        .contains(AssignmentManager.UNEXPECTED_STATE_REGION + "state=SPLIT,")) {
465      // region is recently split
466      ignoreFailure = true;
467    }
468    return ignoreFailure;
469  }
470
471  private ServerName getTargetServer() throws Exception {
472    ServerName server = null;
473    int maxWaitInSeconds =
474        admin.getConfiguration().getInt(SERVERSTART_WAIT_MAX_KEY, DEFAULT_SERVERSTART_WAIT_MAX);
475    long maxWait = EnvironmentEdgeManager.currentTime() + maxWaitInSeconds * 1000;
476    while (EnvironmentEdgeManager.currentTime() < maxWait) {
477      try {
478        List<ServerName> regionServers = new ArrayList<>();
479        regionServers.addAll(admin.getRegionServers());
480        // Remove the host Region server from target Region Servers list
481        server = stripServer(regionServers, hostname, port);
482        if (server != null) {
483          break;
484        } else {
485          LOG.warn("Server " + hostname + ":" + port + " is not up yet, waiting");
486        }
487      } catch (IOException e) {
488        LOG.warn("Could not get list of region servers", e);
489      }
490      Thread.sleep(500);
491    }
492    if (server == null) {
493      LOG.error("Server " + hostname + ":" + port + " is not up. Giving up.");
494      throw new Exception("Server " + hostname + ":" + port + " to load regions not online");
495    }
496    return server;
497  }
498
499  private List<RegionInfo> readRegionsFromFile(String filename) throws IOException {
500    List<RegionInfo> regions = new ArrayList<>();
501    File f = new File(filename);
502    if (!f.exists()) {
503      return regions;
504    }
505    try (DataInputStream dis = new DataInputStream(
506        new BufferedInputStream(new FileInputStream(f)))) {
507      int numRegions = dis.readInt();
508      int index = 0;
509      while (index < numRegions) {
510        regions.add(RegionInfo.parseFromOrNull(Bytes.readByteArray(dis)));
511        index++;
512      }
513    } catch (IOException e) {
514      LOG.error("Error while reading regions from file:" + filename, e);
515      throw e;
516    }
517    return regions;
518  }
519
520  /**
521   * Write the number of regions moved in the first line followed by regions moved in subsequent
522   * lines
523   */
524  private void writeFile(String filename, List<RegionInfo> movedRegions) throws IOException {
525    try (DataOutputStream dos = new DataOutputStream(
526        new BufferedOutputStream(new FileOutputStream(filename)))) {
527      dos.writeInt(movedRegions.size());
528      for (RegionInfo region : movedRegions) {
529        Bytes.writeByteArray(dos, RegionInfo.toByteArray(region));
530      }
531    } catch (IOException e) {
532      LOG.error(
533          "ERROR: Was Not able to write regions moved to output file but moved " + movedRegions
534              .size() + " regions", e);
535      throw e;
536    }
537  }
538
539  private void deleteFile(String filename) {
540    File f = new File(filename);
541    if (f.exists()) {
542      f.delete();
543    }
544  }
545
546  /**
547   * @return List of servers from the exclude file in format 'hostname:port'.
548   */
549  private List<String> readExcludes(String excludeFile) throws IOException {
550    List<String> excludeServers = new ArrayList<>();
551    if (excludeFile == null) {
552      return excludeServers;
553    } else {
554      try {
555        Files.readAllLines(Paths.get(excludeFile)).stream().map(String::trim)
556            .filter(((Predicate<String>) String::isEmpty).negate()).map(String::toLowerCase)
557            .forEach(excludeServers::add);
558      } catch (IOException e) {
559        LOG.warn("Exception while reading excludes file, continuing anyways", e);
560      }
561      return excludeServers;
562    }
563  }
564
565  /**
566   * Excludes the servername whose hostname and port portion matches the list given in exclude file
567   */
568  private void stripExcludes(List<ServerName> regionServers) throws IOException {
569    if (excludeFile != null) {
570      List<String> excludes = readExcludes(excludeFile);
571      Iterator<ServerName> i = regionServers.iterator();
572      while (i.hasNext()) {
573        String rs = i.next().getServerName();
574        String rsPort = rs.split(ServerName.SERVERNAME_SEPARATOR)[0].toLowerCase() + ":" + rs
575            .split(ServerName.SERVERNAME_SEPARATOR)[1];
576        if (excludes.contains(rsPort)) {
577          i.remove();
578        }
579      }
580      LOG.info("Valid Region server targets are:" + regionServers.toString());
581      LOG.info("Excluded Servers are" + excludes.toString());
582    }
583  }
584
585  /**
586   * Exclude master from list of RSs to move regions to
587   */
588  private void stripMaster(List<ServerName> regionServers) throws IOException {
589    ServerName master = admin.getClusterMetrics(EnumSet.of(Option.MASTER)).getMasterName();
590    stripServer(regionServers, master.getHostname(), master.getPort());
591  }
592
593  /**
594   * Remove the servername whose hostname and port portion matches from the passed array of servers.
595   * Returns as side-effect the servername removed.
596   * @return server removed from list of Region Servers
597   */
598  private ServerName stripServer(List<ServerName> regionServers, String hostname, int port) {
599    for (Iterator<ServerName> iter = regionServers.iterator(); iter.hasNext();) {
600      ServerName server = iter.next();
601      if (server.getAddress().getHostname().equalsIgnoreCase(hostname) &&
602        server.getAddress().getPort() == port) {
603        iter.remove();
604        return server;
605      }
606    }
607    return null;
608  }
609
610  @Override
611  protected void addOptions() {
612    this.addRequiredOptWithArg("r", "regionserverhost", "region server <hostname>|<hostname:port>");
613    this.addRequiredOptWithArg("o", "operation", "Expected: load/unload");
614    this.addOptWithArg("m", "maxthreads",
615        "Define the maximum number of threads to use to unload and reload the regions");
616    this.addOptWithArg("x", "excludefile",
617        "File with <hostname:port> per line to exclude as unload targets; default excludes only "
618            + "target host; useful for rack decommisioning.");
619    this.addOptWithArg("f", "filename",
620        "File to save regions list into unloading, or read from loading; "
621            + "default /tmp/<usernamehostname:port>");
622    this.addOptNoArg("n", "noack",
623        "Turn on No-Ack mode(default: false) which won't check if region is online on target "
624            + "RegionServer, hence best effort. This is more performant in unloading and loading "
625            + "but might lead to region being unavailable for some time till master reassigns it "
626            + "in case the move failed");
627    this.addOptWithArg("t", "timeout", "timeout in seconds after which the tool will exit "
628        + "irrespective of whether it finished or not;default Integer.MAX_VALUE");
629  }
630
631  @Override
632  protected void processOptions(CommandLine cmd) {
633    String hostname = cmd.getOptionValue("r");
634    rmbuilder = new RegionMoverBuilder(hostname);
635    if (cmd.hasOption('m')) {
636      rmbuilder.maxthreads(Integer.parseInt(cmd.getOptionValue('m')));
637    }
638    if (cmd.hasOption('n')) {
639      rmbuilder.ack(false);
640    }
641    if (cmd.hasOption('f')) {
642      rmbuilder.filename(cmd.getOptionValue('f'));
643    }
644    if (cmd.hasOption('x')) {
645      rmbuilder.excludeFile(cmd.getOptionValue('x'));
646    }
647    if (cmd.hasOption('t')) {
648      rmbuilder.timeout(Integer.parseInt(cmd.getOptionValue('t')));
649    }
650    this.loadUnload = cmd.getOptionValue("o").toLowerCase(Locale.ROOT);
651  }
652
653  @Override
654  protected int doWork() throws Exception {
655    boolean success;
656    try (RegionMover rm = rmbuilder.build()) {
657      if (loadUnload.equalsIgnoreCase("load")) {
658        success = rm.load();
659      } else if (loadUnload.equalsIgnoreCase("unload")) {
660        success = rm.unload();
661      } else {
662        printUsage();
663        success = false;
664      }
665    }
666    return (success ? 0 : 1);
667  }
668
669  public static void main(String[] args) {
670    try (RegionMover mover = new RegionMover()) {
671      mover.doStaticMain(args);
672    }
673  }
674}