org.apache.hadoop.hbase.client.RawAsyncHBaseAdmin.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.hadoop.hbase.client.RawAsyncHBaseAdmin.java

Source

/**
 * 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;

import static org.apache.hadoop.hbase.HConstants.HIGH_QOS;
import static org.apache.hadoop.hbase.TableName.META_TABLE_NAME;
import static org.apache.hadoop.hbase.util.FutureUtils.addListener;
import static org.apache.hadoop.hbase.util.FutureUtils.unwrapCompletionException;

import com.google.protobuf.Message;
import com.google.protobuf.RpcChannel;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.io.IOUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.AsyncMetaTableAccessor;
import org.apache.hadoop.hbase.CacheEvictionStats;
import org.apache.hadoop.hbase.CacheEvictionStatsAggregator;
import org.apache.hadoop.hbase.ClusterMetrics;
import org.apache.hadoop.hbase.ClusterMetrics.Option;
import org.apache.hadoop.hbase.ClusterMetricsBuilder;
import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.HRegionLocation;
import org.apache.hadoop.hbase.MetaTableAccessor;
import org.apache.hadoop.hbase.MetaTableAccessor.QueryType;
import org.apache.hadoop.hbase.NamespaceDescriptor;
import org.apache.hadoop.hbase.RegionLocations;
import org.apache.hadoop.hbase.RegionMetrics;
import org.apache.hadoop.hbase.RegionMetricsBuilder;
import org.apache.hadoop.hbase.ServerName;
import org.apache.hadoop.hbase.TableExistsException;
import org.apache.hadoop.hbase.TableName;
import org.apache.hadoop.hbase.TableNotDisabledException;
import org.apache.hadoop.hbase.TableNotEnabledException;
import org.apache.hadoop.hbase.TableNotFoundException;
import org.apache.hadoop.hbase.UnknownRegionException;
import org.apache.hadoop.hbase.client.AsyncRpcRetryingCallerFactory.AdminRequestCallerBuilder;
import org.apache.hadoop.hbase.client.AsyncRpcRetryingCallerFactory.MasterRequestCallerBuilder;
import org.apache.hadoop.hbase.client.AsyncRpcRetryingCallerFactory.ServerRequestCallerBuilder;
import org.apache.hadoop.hbase.client.Scan.ReadType;
import org.apache.hadoop.hbase.client.replication.ReplicationPeerConfigUtil;
import org.apache.hadoop.hbase.client.replication.TableCFs;
import org.apache.hadoop.hbase.client.security.SecurityCapability;
import org.apache.hadoop.hbase.exceptions.DeserializationException;
import org.apache.hadoop.hbase.ipc.HBaseRpcController;
import org.apache.hadoop.hbase.quotas.QuotaFilter;
import org.apache.hadoop.hbase.quotas.QuotaSettings;
import org.apache.hadoop.hbase.quotas.QuotaTableUtil;
import org.apache.hadoop.hbase.quotas.SpaceQuotaSnapshot;
import org.apache.hadoop.hbase.replication.ReplicationException;
import org.apache.hadoop.hbase.replication.ReplicationPeerConfig;
import org.apache.hadoop.hbase.replication.ReplicationPeerDescription;
import org.apache.hadoop.hbase.replication.SyncReplicationState;
import org.apache.hadoop.hbase.security.access.GetUserPermissionsRequest;
import org.apache.hadoop.hbase.security.access.Permission;
import org.apache.hadoop.hbase.security.access.ShadedAccessControlUtil;
import org.apache.hadoop.hbase.security.access.UserPermission;
import org.apache.hadoop.hbase.snapshot.ClientSnapshotDescriptionUtils;
import org.apache.hadoop.hbase.snapshot.RestoreSnapshotException;
import org.apache.hadoop.hbase.snapshot.SnapshotCreationException;
import org.apache.hadoop.hbase.util.Bytes;
import org.apache.hadoop.hbase.util.EnvironmentEdgeManager;
import org.apache.hadoop.hbase.util.ForeignExceptionUtil;
import org.apache.yetus.audience.InterfaceAudience;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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.protobuf.RpcCallback;
import org.apache.hbase.thirdparty.io.netty.util.HashedWheelTimer;
import org.apache.hbase.thirdparty.io.netty.util.Timeout;
import org.apache.hbase.thirdparty.io.netty.util.TimerTask;

import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;
import org.apache.hadoop.hbase.shaded.protobuf.RequestConverter;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AccessControlProtos;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AccessControlProtos.GetUserPermissionsResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AccessControlProtos.GrantRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AccessControlProtos.GrantResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AccessControlProtos.HasUserPermissionsRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AccessControlProtos.HasUserPermissionsResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AccessControlProtos.RevokeRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AccessControlProtos.RevokeResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.AdminService;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.ClearCompactionQueuesRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.ClearCompactionQueuesResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.ClearRegionBlockCacheRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.ClearRegionBlockCacheResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.CompactRegionRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.CompactRegionResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.CompactionSwitchRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.CompactionSwitchResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.FlushRegionRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.FlushRegionResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.GetOnlineRegionRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.GetOnlineRegionResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.GetRegionInfoRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.GetRegionInfoResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.GetRegionLoadRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.GetRegionLoadResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.RollWALWriterRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.RollWALWriterResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.StopServerRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.StopServerResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.UpdateConfigurationRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.AdminProtos.UpdateConfigurationResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos;
import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.ProcedureDescription;
import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.RegionSpecifier.RegionSpecifierType;
import org.apache.hadoop.hbase.shaded.protobuf.generated.HBaseProtos.TableSchema;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.AbortProcedureRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.AbortProcedureResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.AddColumnRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.AddColumnResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.AssignRegionRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.AssignRegionResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.BalanceRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.BalanceResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.ClearDeadServersRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.ClearDeadServersResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.CreateNamespaceRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.CreateNamespaceResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.CreateTableRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.CreateTableResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.DecommissionRegionServersRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.DecommissionRegionServersResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.DeleteColumnRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.DeleteColumnResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.DeleteNamespaceRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.DeleteNamespaceResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.DeleteSnapshotRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.DeleteSnapshotResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.DeleteTableRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.DeleteTableResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.DisableTableRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.DisableTableResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.EnableCatalogJanitorRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.EnableCatalogJanitorResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.EnableTableRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.EnableTableResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.ExecProcedureRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.ExecProcedureResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.GetClusterStatusRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.GetClusterStatusResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.GetCompletedSnapshotsRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.GetCompletedSnapshotsResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.GetLocksRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.GetLocksResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.GetNamespaceDescriptorRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.GetNamespaceDescriptorResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.GetProcedureResultRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.GetProcedureResultResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.GetProceduresRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.GetProceduresResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.GetTableDescriptorsRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.GetTableDescriptorsResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.GetTableNamesRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.GetTableNamesResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.IsBalancerEnabledRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.IsBalancerEnabledResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.IsCatalogJanitorEnabledRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.IsCatalogJanitorEnabledResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.IsCleanerChoreEnabledRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.IsCleanerChoreEnabledResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.IsInMaintenanceModeRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.IsInMaintenanceModeResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.IsNormalizerEnabledRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.IsNormalizerEnabledResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.IsProcedureDoneRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.IsProcedureDoneResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.IsRpcThrottleEnabledRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.IsRpcThrottleEnabledResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.IsSnapshotDoneRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.IsSnapshotDoneResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.IsSplitOrMergeEnabledRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.IsSplitOrMergeEnabledResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.ListDecommissionedRegionServersRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.ListDecommissionedRegionServersResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.ListNamespaceDescriptorsRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.ListNamespaceDescriptorsResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.ListNamespacesRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.ListNamespacesResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.ListTableDescriptorsByNamespaceRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.ListTableDescriptorsByNamespaceResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.ListTableNamesByNamespaceRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.ListTableNamesByNamespaceResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.MajorCompactionTimestampForRegionRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.MajorCompactionTimestampRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.MajorCompactionTimestampResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.MasterService;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.MergeTableRegionsRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.MergeTableRegionsResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.ModifyColumnRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.ModifyColumnResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.ModifyNamespaceRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.ModifyNamespaceResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.ModifyTableRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.ModifyTableResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.MoveRegionRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.MoveRegionResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.NormalizeRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.NormalizeResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.OfflineRegionRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.OfflineRegionResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.RecommissionRegionServerRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.RecommissionRegionServerResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.RestoreSnapshotRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.RestoreSnapshotResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.RunCatalogScanRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.RunCatalogScanResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.RunCleanerChoreRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.RunCleanerChoreResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SecurityCapabilitiesRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SecurityCapabilitiesResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SetBalancerRunningRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SetBalancerRunningResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SetCleanerChoreRunningRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SetCleanerChoreRunningResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SetNormalizerRunningRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SetNormalizerRunningResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SetQuotaRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SetQuotaResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SetSplitOrMergeEnabledRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SetSplitOrMergeEnabledResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.ShutdownRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.ShutdownResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SnapshotRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SnapshotResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SplitTableRegionRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SplitTableRegionResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.StopMasterRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.StopMasterResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SwitchExceedThrottleQuotaRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SwitchExceedThrottleQuotaResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SwitchRpcThrottleRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.SwitchRpcThrottleResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.TruncateTableRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.TruncateTableResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.UnassignRegionRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.MasterProtos.UnassignRegionResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.GetQuotaStatesRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.GetQuotaStatesResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.GetSpaceQuotaRegionSizesRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.GetSpaceQuotaRegionSizesResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.GetSpaceQuotaRegionSizesResponse.RegionSizes;
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.GetSpaceQuotaSnapshotsRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.QuotaProtos.GetSpaceQuotaSnapshotsResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.ReplicationProtos.AddReplicationPeerRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.ReplicationProtos.AddReplicationPeerResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.ReplicationProtos.DisableReplicationPeerRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.ReplicationProtos.DisableReplicationPeerResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.ReplicationProtos.EnableReplicationPeerRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.ReplicationProtos.EnableReplicationPeerResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.ReplicationProtos.GetReplicationPeerConfigRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.ReplicationProtos.GetReplicationPeerConfigResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.ReplicationProtos.ListReplicationPeersRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.ReplicationProtos.ListReplicationPeersResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.ReplicationProtos.RemoveReplicationPeerRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.ReplicationProtos.RemoveReplicationPeerResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.ReplicationProtos.TransitReplicationPeerSyncReplicationStateRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.ReplicationProtos.TransitReplicationPeerSyncReplicationStateResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.ReplicationProtos.UpdateReplicationPeerConfigRequest;
import org.apache.hadoop.hbase.shaded.protobuf.generated.ReplicationProtos.UpdateReplicationPeerConfigResponse;
import org.apache.hadoop.hbase.shaded.protobuf.generated.SnapshotProtos;

/**
 * The implementation of AsyncAdmin.
 * <p>
 * The word 'Raw' means that this is a low level class. The returned {@link CompletableFuture} will
 * be finished inside the rpc framework thread, which means that the callbacks registered to the
 * {@link CompletableFuture} will also be executed inside the rpc framework thread. So users who use
 * this class should not try to do time consuming tasks in the callbacks.
 * @since 2.0.0
 * @see AsyncHBaseAdmin
 * @see AsyncConnection#getAdmin()
 * @see AsyncConnection#getAdminBuilder()
 */
@InterfaceAudience.Private
class RawAsyncHBaseAdmin implements AsyncAdmin {
    public static final String FLUSH_TABLE_PROCEDURE_SIGNATURE = "flush-table-proc";

    private static final Logger LOG = LoggerFactory.getLogger(AsyncHBaseAdmin.class);

    private final AsyncConnectionImpl connection;

    private final HashedWheelTimer retryTimer;

    private final AsyncTable<AdvancedScanResultConsumer> metaTable;

    private final long rpcTimeoutNs;

    private final long operationTimeoutNs;

    private final long pauseNs;

    private final long pauseForCQTBENs;

    private final int maxAttempts;

    private final int startLogErrorsCnt;

    private final NonceGenerator ng;

    RawAsyncHBaseAdmin(AsyncConnectionImpl connection, HashedWheelTimer retryTimer, AsyncAdminBuilderBase builder) {
        this.connection = connection;
        this.retryTimer = retryTimer;
        this.metaTable = connection.getTable(META_TABLE_NAME);
        this.rpcTimeoutNs = builder.rpcTimeoutNs;
        this.operationTimeoutNs = builder.operationTimeoutNs;
        this.pauseNs = builder.pauseNs;
        if (builder.pauseForCQTBENs < builder.pauseNs) {
            LOG.warn(
                    "Configured value of pauseForCQTBENs is {} ms, which is less than"
                            + " the normal pause value {} ms, use the greater one instead",
                    TimeUnit.NANOSECONDS.toMillis(builder.pauseForCQTBENs),
                    TimeUnit.NANOSECONDS.toMillis(builder.pauseNs));
            this.pauseForCQTBENs = builder.pauseNs;
        } else {
            this.pauseForCQTBENs = builder.pauseForCQTBENs;
        }
        this.maxAttempts = builder.maxAttempts;
        this.startLogErrorsCnt = builder.startLogErrorsCnt;
        this.ng = connection.getNonceGenerator();
    }

    private <T> MasterRequestCallerBuilder<T> newMasterCaller() {
        return this.connection.callerFactory.<T>masterRequest().rpcTimeout(rpcTimeoutNs, TimeUnit.NANOSECONDS)
                .operationTimeout(operationTimeoutNs, TimeUnit.NANOSECONDS).pause(pauseNs, TimeUnit.NANOSECONDS)
                .pauseForCQTBE(pauseForCQTBENs, TimeUnit.NANOSECONDS).maxAttempts(maxAttempts)
                .startLogErrorsCnt(startLogErrorsCnt);
    }

    private <T> AdminRequestCallerBuilder<T> newAdminCaller() {
        return this.connection.callerFactory.<T>adminRequest().rpcTimeout(rpcTimeoutNs, TimeUnit.NANOSECONDS)
                .operationTimeout(operationTimeoutNs, TimeUnit.NANOSECONDS).pause(pauseNs, TimeUnit.NANOSECONDS)
                .pauseForCQTBE(pauseForCQTBENs, TimeUnit.NANOSECONDS).maxAttempts(maxAttempts)
                .startLogErrorsCnt(startLogErrorsCnt);
    }

    @FunctionalInterface
    private interface MasterRpcCall<RESP, REQ> {
        void call(MasterService.Interface stub, HBaseRpcController controller, REQ req, RpcCallback<RESP> done);
    }

    @FunctionalInterface
    private interface AdminRpcCall<RESP, REQ> {
        void call(AdminService.Interface stub, HBaseRpcController controller, REQ req, RpcCallback<RESP> done);
    }

    @FunctionalInterface
    private interface Converter<D, S> {
        D convert(S src) throws IOException;
    }

    private <PREQ, PRESP, RESP> CompletableFuture<RESP> call(HBaseRpcController controller,
            MasterService.Interface stub, PREQ preq, MasterRpcCall<PRESP, PREQ> rpcCall,
            Converter<RESP, PRESP> respConverter) {
        CompletableFuture<RESP> future = new CompletableFuture<>();
        rpcCall.call(stub, controller, preq, new RpcCallback<PRESP>() {

            @Override
            public void run(PRESP resp) {
                if (controller.failed()) {
                    future.completeExceptionally(controller.getFailed());
                } else {
                    try {
                        future.complete(respConverter.convert(resp));
                    } catch (IOException e) {
                        future.completeExceptionally(e);
                    }
                }
            }
        });
        return future;
    }

    private <PREQ, PRESP, RESP> CompletableFuture<RESP> adminCall(HBaseRpcController controller,
            AdminService.Interface stub, PREQ preq, AdminRpcCall<PRESP, PREQ> rpcCall,
            Converter<RESP, PRESP> respConverter) {
        CompletableFuture<RESP> future = new CompletableFuture<>();
        rpcCall.call(stub, controller, preq, new RpcCallback<PRESP>() {

            @Override
            public void run(PRESP resp) {
                if (controller.failed()) {
                    future.completeExceptionally(controller.getFailed());
                } else {
                    try {
                        future.complete(respConverter.convert(resp));
                    } catch (IOException e) {
                        future.completeExceptionally(e);
                    }
                }
            }
        });
        return future;
    }

    private <PREQ, PRESP> CompletableFuture<Void> procedureCall(PREQ preq, MasterRpcCall<PRESP, PREQ> rpcCall,
            Converter<Long, PRESP> respConverter, ProcedureBiConsumer consumer) {
        return procedureCall(b -> {
        }, preq, rpcCall, respConverter, consumer);
    }

    private <PREQ, PRESP> CompletableFuture<Void> procedureCall(TableName tableName, PREQ preq,
            MasterRpcCall<PRESP, PREQ> rpcCall, Converter<Long, PRESP> respConverter,
            ProcedureBiConsumer consumer) {
        return procedureCall(b -> b.priority(tableName), preq, rpcCall, respConverter, consumer);
    }

    private <PREQ, PRESP> CompletableFuture<Void> procedureCall(
            Consumer<MasterRequestCallerBuilder<?>> prioritySetter, PREQ preq, MasterRpcCall<PRESP, PREQ> rpcCall,
            Converter<Long, PRESP> respConverter, ProcedureBiConsumer consumer) {
        MasterRequestCallerBuilder<Long> builder = this.<Long>newMasterCaller().action(
                (controller, stub) -> this.<PREQ, PRESP, Long>call(controller, stub, preq, rpcCall, respConverter));
        prioritySetter.accept(builder);
        CompletableFuture<Long> procFuture = builder.call();
        CompletableFuture<Void> future = waitProcedureResult(procFuture);
        addListener(future, consumer);
        return future;
    }

    @FunctionalInterface
    private interface TableOperator {
        CompletableFuture<Void> operate(TableName table);
    }

    @Override
    public CompletableFuture<Boolean> tableExists(TableName tableName) {
        if (TableName.isMetaTableName(tableName)) {
            return CompletableFuture.completedFuture(true);
        }
        return AsyncMetaTableAccessor.tableExists(metaTable, tableName);
    }

    @Override
    public CompletableFuture<List<TableDescriptor>> listTableDescriptors(boolean includeSysTables) {
        return getTableDescriptors(RequestConverter.buildGetTableDescriptorsRequest(null, includeSysTables));
    }

    /**
     * {@link #listTableDescriptors(boolean)}
     */
    @Override
    public CompletableFuture<List<TableDescriptor>> listTableDescriptors(Pattern pattern,
            boolean includeSysTables) {
        Preconditions.checkNotNull(pattern,
                "pattern is null. If you don't specify a pattern, use listTables(boolean) instead");
        return getTableDescriptors(RequestConverter.buildGetTableDescriptorsRequest(pattern, includeSysTables));
    }

    @Override
    public CompletableFuture<List<TableDescriptor>> listTableDescriptors(List<TableName> tableNames) {
        Preconditions.checkNotNull(tableNames,
                "tableNames is null. If you don't specify tableNames, " + "use listTables(boolean) instead");
        if (tableNames.isEmpty()) {
            return CompletableFuture.completedFuture(Collections.emptyList());
        }
        return getTableDescriptors(RequestConverter.buildGetTableDescriptorsRequest(tableNames));
    }

    private CompletableFuture<List<TableDescriptor>> getTableDescriptors(GetTableDescriptorsRequest request) {
        return this.<List<TableDescriptor>>newMasterCaller()
                .action((controller, stub) -> this
                        .<GetTableDescriptorsRequest, GetTableDescriptorsResponse, List<TableDescriptor>>call(
                                controller, stub, request, (s, c, req, done) -> s.getTableDescriptors(c, req, done),
                                (resp) -> ProtobufUtil.toTableDescriptorList(resp)))
                .call();
    }

    @Override
    public CompletableFuture<List<TableName>> listTableNames(boolean includeSysTables) {
        return getTableNames(RequestConverter.buildGetTableNamesRequest(null, includeSysTables));
    }

    @Override
    public CompletableFuture<List<TableName>> listTableNames(Pattern pattern, boolean includeSysTables) {
        Preconditions.checkNotNull(pattern,
                "pattern is null. If you don't specify a pattern, use listTableNames(boolean) instead");
        return getTableNames(RequestConverter.buildGetTableNamesRequest(pattern, includeSysTables));
    }

    private CompletableFuture<List<TableName>> getTableNames(GetTableNamesRequest request) {
        return this.<List<TableName>>newMasterCaller()
                .action((controller, stub) -> this
                        .<GetTableNamesRequest, GetTableNamesResponse, List<TableName>>call(controller, stub,
                                request, (s, c, req, done) -> s.getTableNames(c, req, done),
                                (resp) -> ProtobufUtil.toTableNameList(resp.getTableNamesList())))
                .call();
    }

    @Override
    public CompletableFuture<List<TableDescriptor>> listTableDescriptorsByNamespace(String name) {
        return this.<List<TableDescriptor>>newMasterCaller().action((controller, stub) -> this
                .<ListTableDescriptorsByNamespaceRequest, ListTableDescriptorsByNamespaceResponse, List<TableDescriptor>>call(
                        controller, stub,
                        ListTableDescriptorsByNamespaceRequest.newBuilder().setNamespaceName(name).build(),
                        (s, c, req, done) -> s.listTableDescriptorsByNamespace(c, req, done),
                        (resp) -> ProtobufUtil.toTableDescriptorList(resp)))
                .call();
    }

    @Override
    public CompletableFuture<List<TableName>> listTableNamesByNamespace(String name) {
        return this.<List<TableName>>newMasterCaller()
                .action((controller, stub) -> this
                        .<ListTableNamesByNamespaceRequest, ListTableNamesByNamespaceResponse, List<TableName>>call(
                                controller, stub,
                                ListTableNamesByNamespaceRequest.newBuilder().setNamespaceName(name).build(),
                                (s, c, req, done) -> s.listTableNamesByNamespace(c, req, done),
                                (resp) -> ProtobufUtil.toTableNameList(resp.getTableNameList())))
                .call();
    }

    @Override
    public CompletableFuture<TableDescriptor> getDescriptor(TableName tableName) {
        CompletableFuture<TableDescriptor> future = new CompletableFuture<>();
        addListener(this.<List<TableSchema>>newMasterCaller().priority(tableName)
                .action((controller, stub) -> this
                        .<GetTableDescriptorsRequest, GetTableDescriptorsResponse, List<TableSchema>>call(
                                controller, stub, RequestConverter.buildGetTableDescriptorsRequest(tableName),
                                (s, c, req, done) -> s.getTableDescriptors(c, req, done),
                                (resp) -> resp.getTableSchemaList()))
                .call(), (tableSchemas, error) -> {
                    if (error != null) {
                        future.completeExceptionally(error);
                        return;
                    }
                    if (!tableSchemas.isEmpty()) {
                        future.complete(ProtobufUtil.toTableDescriptor(tableSchemas.get(0)));
                    } else {
                        future.completeExceptionally(new TableNotFoundException(tableName.getNameAsString()));
                    }
                });
        return future;
    }

    @Override
    public CompletableFuture<Void> createTable(TableDescriptor desc) {
        return createTable(desc.getTableName(),
                RequestConverter.buildCreateTableRequest(desc, null, ng.getNonceGroup(), ng.newNonce()));
    }

    @Override
    public CompletableFuture<Void> createTable(TableDescriptor desc, byte[] startKey, byte[] endKey,
            int numRegions) {
        try {
            return createTable(desc, getSplitKeys(startKey, endKey, numRegions));
        } catch (IllegalArgumentException e) {
            return failedFuture(e);
        }
    }

    @Override
    public CompletableFuture<Void> createTable(TableDescriptor desc, byte[][] splitKeys) {
        Preconditions.checkNotNull(splitKeys,
                "splitKeys is null. If you don't specify splitKeys," + " use createTable(TableDescriptor) instead");
        try {
            verifySplitKeys(splitKeys);
            return createTable(desc.getTableName(),
                    RequestConverter.buildCreateTableRequest(desc, splitKeys, ng.getNonceGroup(), ng.newNonce()));
        } catch (IllegalArgumentException e) {
            return failedFuture(e);
        }
    }

    private CompletableFuture<Void> createTable(TableName tableName, CreateTableRequest request) {
        Preconditions.checkNotNull(tableName, "table name is null");
        return this.<CreateTableRequest, CreateTableResponse>procedureCall(tableName, request,
                (s, c, req, done) -> s.createTable(c, req, done), (resp) -> resp.getProcId(),
                new CreateTableProcedureBiConsumer(tableName));
    }

    @Override
    public CompletableFuture<Void> modifyTable(TableDescriptor desc) {
        return this.<ModifyTableRequest, ModifyTableResponse>procedureCall(desc.getTableName(),
                RequestConverter.buildModifyTableRequest(desc.getTableName(), desc, ng.getNonceGroup(),
                        ng.newNonce()),
                (s, c, req, done) -> s.modifyTable(c, req, done), (resp) -> resp.getProcId(),
                new ModifyTableProcedureBiConsumer(this, desc.getTableName()));
    }

    @Override
    public CompletableFuture<Void> deleteTable(TableName tableName) {
        return this.<DeleteTableRequest, DeleteTableResponse>procedureCall(tableName,
                RequestConverter.buildDeleteTableRequest(tableName, ng.getNonceGroup(), ng.newNonce()),
                (s, c, req, done) -> s.deleteTable(c, req, done), (resp) -> resp.getProcId(),
                new DeleteTableProcedureBiConsumer(tableName));
    }

    @Override
    public CompletableFuture<Void> truncateTable(TableName tableName, boolean preserveSplits) {
        return this.<TruncateTableRequest, TruncateTableResponse>procedureCall(tableName,
                RequestConverter.buildTruncateTableRequest(tableName, preserveSplits, ng.getNonceGroup(),
                        ng.newNonce()),
                (s, c, req, done) -> s.truncateTable(c, req, done), (resp) -> resp.getProcId(),
                new TruncateTableProcedureBiConsumer(tableName));
    }

    @Override
    public CompletableFuture<Void> enableTable(TableName tableName) {
        return this.<EnableTableRequest, EnableTableResponse>procedureCall(tableName,
                RequestConverter.buildEnableTableRequest(tableName, ng.getNonceGroup(), ng.newNonce()),
                (s, c, req, done) -> s.enableTable(c, req, done), (resp) -> resp.getProcId(),
                new EnableTableProcedureBiConsumer(tableName));
    }

    @Override
    public CompletableFuture<Void> disableTable(TableName tableName) {
        return this.<DisableTableRequest, DisableTableResponse>procedureCall(tableName,
                RequestConverter.buildDisableTableRequest(tableName, ng.getNonceGroup(), ng.newNonce()),
                (s, c, req, done) -> s.disableTable(c, req, done), (resp) -> resp.getProcId(),
                new DisableTableProcedureBiConsumer(tableName));
    }

    @Override
    public CompletableFuture<Boolean> isTableEnabled(TableName tableName) {
        if (TableName.isMetaTableName(tableName)) {
            return CompletableFuture.completedFuture(true);
        }
        CompletableFuture<Boolean> future = new CompletableFuture<>();
        addListener(AsyncMetaTableAccessor.getTableState(metaTable, tableName), (state, error) -> {
            if (error != null) {
                future.completeExceptionally(error);
                return;
            }
            if (state.isPresent()) {
                future.complete(state.get().inStates(TableState.State.ENABLED));
            } else {
                future.completeExceptionally(new TableNotFoundException(tableName));
            }
        });
        return future;
    }

    @Override
    public CompletableFuture<Boolean> isTableDisabled(TableName tableName) {
        if (TableName.isMetaTableName(tableName)) {
            return CompletableFuture.completedFuture(false);
        }
        CompletableFuture<Boolean> future = new CompletableFuture<>();
        addListener(AsyncMetaTableAccessor.getTableState(metaTable, tableName), (state, error) -> {
            if (error != null) {
                future.completeExceptionally(error);
                return;
            }
            if (state.isPresent()) {
                future.complete(state.get().inStates(TableState.State.DISABLED));
            } else {
                future.completeExceptionally(new TableNotFoundException(tableName));
            }
        });
        return future;
    }

    @Override
    public CompletableFuture<Boolean> isTableAvailable(TableName tableName) {
        return isTableAvailable(tableName, Optional.empty());
    }

    private CompletableFuture<Boolean> isTableAvailable(TableName tableName, Optional<byte[][]> splitKeys) {
        if (TableName.isMetaTableName(tableName)) {
            return connection.registry.getMetaRegionLocation().thenApply(locs -> Stream
                    .of(locs.getRegionLocations()).allMatch(loc -> loc != null && loc.getServerName() != null));
        }
        CompletableFuture<Boolean> future = new CompletableFuture<>();
        addListener(isTableEnabled(tableName), (enabled, error) -> {
            if (error != null) {
                if (error instanceof TableNotFoundException) {
                    future.complete(false);
                } else {
                    future.completeExceptionally(error);
                }
                return;
            }
            if (!enabled) {
                future.complete(false);
            } else {
                addListener(AsyncMetaTableAccessor.getTableHRegionLocations(metaTable, Optional.of(tableName)),
                        (locations, error1) -> {
                            if (error1 != null) {
                                future.completeExceptionally(error1);
                                return;
                            }
                            List<HRegionLocation> notDeployedRegions = locations.stream()
                                    .filter(loc -> loc.getServerName() == null).collect(Collectors.toList());
                            if (notDeployedRegions.size() > 0) {
                                if (LOG.isDebugEnabled()) {
                                    LOG.debug("Table " + tableName + " has " + notDeployedRegions.size()
                                            + " regions");
                                }
                                future.complete(false);
                                return;
                            }

                            Optional<Boolean> available = splitKeys
                                    .map(keys -> compareRegionsWithSplitKeys(locations, keys));
                            future.complete(available.orElse(true));
                        });
            }
        });
        return future;
    }

    private boolean compareRegionsWithSplitKeys(List<HRegionLocation> locations, byte[][] splitKeys) {
        int regionCount = 0;
        for (HRegionLocation location : locations) {
            RegionInfo info = location.getRegion();
            if (Bytes.equals(info.getStartKey(), HConstants.EMPTY_BYTE_ARRAY)) {
                regionCount++;
                continue;
            }
            for (byte[] splitKey : splitKeys) {
                // Just check if the splitkey is available
                if (Bytes.equals(info.getStartKey(), splitKey)) {
                    regionCount++;
                    break;
                }
            }
        }
        return regionCount == splitKeys.length + 1;
    }

    @Override
    public CompletableFuture<Void> addColumnFamily(TableName tableName, ColumnFamilyDescriptor columnFamily) {
        return this.<AddColumnRequest, AddColumnResponse>procedureCall(tableName,
                RequestConverter.buildAddColumnRequest(tableName, columnFamily, ng.getNonceGroup(), ng.newNonce()),
                (s, c, req, done) -> s.addColumn(c, req, done), (resp) -> resp.getProcId(),
                new AddColumnFamilyProcedureBiConsumer(tableName));
    }

    @Override
    public CompletableFuture<Void> deleteColumnFamily(TableName tableName, byte[] columnFamily) {
        return this.<DeleteColumnRequest, DeleteColumnResponse>procedureCall(tableName,
                RequestConverter.buildDeleteColumnRequest(tableName, columnFamily, ng.getNonceGroup(),
                        ng.newNonce()),
                (s, c, req, done) -> s.deleteColumn(c, req, done), (resp) -> resp.getProcId(),
                new DeleteColumnFamilyProcedureBiConsumer(tableName));
    }

    @Override
    public CompletableFuture<Void> modifyColumnFamily(TableName tableName, ColumnFamilyDescriptor columnFamily) {
        return this.<ModifyColumnRequest, ModifyColumnResponse>procedureCall(tableName,
                RequestConverter.buildModifyColumnRequest(tableName, columnFamily, ng.getNonceGroup(),
                        ng.newNonce()),
                (s, c, req, done) -> s.modifyColumn(c, req, done), (resp) -> resp.getProcId(),
                new ModifyColumnFamilyProcedureBiConsumer(tableName));
    }

    @Override
    public CompletableFuture<Void> createNamespace(NamespaceDescriptor descriptor) {
        return this.<CreateNamespaceRequest, CreateNamespaceResponse>procedureCall(
                RequestConverter.buildCreateNamespaceRequest(descriptor),
                (s, c, req, done) -> s.createNamespace(c, req, done), (resp) -> resp.getProcId(),
                new CreateNamespaceProcedureBiConsumer(descriptor.getName()));
    }

    @Override
    public CompletableFuture<Void> modifyNamespace(NamespaceDescriptor descriptor) {
        return this.<ModifyNamespaceRequest, ModifyNamespaceResponse>procedureCall(
                RequestConverter.buildModifyNamespaceRequest(descriptor),
                (s, c, req, done) -> s.modifyNamespace(c, req, done), (resp) -> resp.getProcId(),
                new ModifyNamespaceProcedureBiConsumer(descriptor.getName()));
    }

    @Override
    public CompletableFuture<Void> deleteNamespace(String name) {
        return this.<DeleteNamespaceRequest, DeleteNamespaceResponse>procedureCall(
                RequestConverter.buildDeleteNamespaceRequest(name),
                (s, c, req, done) -> s.deleteNamespace(c, req, done), (resp) -> resp.getProcId(),
                new DeleteNamespaceProcedureBiConsumer(name));
    }

    @Override
    public CompletableFuture<NamespaceDescriptor> getNamespaceDescriptor(String name) {
        return this.<NamespaceDescriptor>newMasterCaller()
                .action((controller, stub) -> this
                        .<GetNamespaceDescriptorRequest, GetNamespaceDescriptorResponse, NamespaceDescriptor>call(
                                controller, stub, RequestConverter.buildGetNamespaceDescriptorRequest(name),
                                (s, c, req, done) -> s.getNamespaceDescriptor(c, req, done),
                                (resp) -> ProtobufUtil.toNamespaceDescriptor(resp.getNamespaceDescriptor())))
                .call();
    }

    @Override
    public CompletableFuture<List<String>> listNamespaces() {
        return this.<List<String>>newMasterCaller().action(
                (controller, stub) -> this.<ListNamespacesRequest, ListNamespacesResponse, List<String>>call(
                        controller, stub, ListNamespacesRequest.newBuilder().build(),
                        (s, c, req, done) -> s.listNamespaces(c, req, done), (resp) -> resp.getNamespaceNameList()))
                .call();
    }

    @Override
    public CompletableFuture<List<NamespaceDescriptor>> listNamespaceDescriptors() {
        return this.<List<NamespaceDescriptor>>newMasterCaller().action((controller, stub) -> this
                .<ListNamespaceDescriptorsRequest, ListNamespaceDescriptorsResponse, List<NamespaceDescriptor>>call(
                        controller, stub, ListNamespaceDescriptorsRequest.newBuilder().build(),
                        (s, c, req, done) -> s.listNamespaceDescriptors(c, req, done),
                        (resp) -> ProtobufUtil.toNamespaceDescriptorList(resp)))
                .call();
    }

    @Override
    public CompletableFuture<List<RegionInfo>> getRegions(ServerName serverName) {
        return this.<List<RegionInfo>>newAdminCaller()
                .action((controller, stub) -> this
                        .<GetOnlineRegionRequest, GetOnlineRegionResponse, List<RegionInfo>>adminCall(controller,
                                stub, RequestConverter.buildGetOnlineRegionRequest(),
                                (s, c, req, done) -> s.getOnlineRegion(c, req, done),
                                resp -> ProtobufUtil.getRegionInfos(resp)))
                .serverName(serverName).call();
    }

    @Override
    public CompletableFuture<List<RegionInfo>> getRegions(TableName tableName) {
        if (tableName.equals(META_TABLE_NAME)) {
            return connection.registry.getMetaRegionLocation().thenApply(locs -> Stream
                    .of(locs.getRegionLocations()).map(HRegionLocation::getRegion).collect(Collectors.toList()));
        } else {
            return AsyncMetaTableAccessor.getTableHRegionLocations(metaTable, Optional.of(tableName))
                    .thenApply(locs -> locs.stream().map(HRegionLocation::getRegion).collect(Collectors.toList()));
        }
    }

    @Override
    public CompletableFuture<Void> flush(TableName tableName) {
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(tableExists(tableName), (exists, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
            } else if (!exists) {
                future.completeExceptionally(new TableNotFoundException(tableName));
            } else {
                addListener(isTableEnabled(tableName), (tableEnabled, err2) -> {
                    if (err2 != null) {
                        future.completeExceptionally(err2);
                    } else if (!tableEnabled) {
                        future.completeExceptionally(new TableNotEnabledException(tableName));
                    } else {
                        addListener(execProcedure(FLUSH_TABLE_PROCEDURE_SIGNATURE, tableName.getNameAsString(),
                                new HashMap<>()), (ret, err3) -> {
                                    if (err3 != null) {
                                        future.completeExceptionally(err3);
                                    } else {
                                        future.complete(ret);
                                    }
                                });
                    }
                });
            }
        });
        return future;
    }

    @Override
    public CompletableFuture<Void> flushRegion(byte[] regionName) {
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(getRegionLocation(regionName), (location, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            ServerName serverName = location.getServerName();
            if (serverName == null) {
                future.completeExceptionally(new NoServerForRegionException(Bytes.toStringBinary(regionName)));
                return;
            }
            addListener(flush(serverName, location.getRegion()), (ret, err2) -> {
                if (err2 != null) {
                    future.completeExceptionally(err2);
                } else {
                    future.complete(ret);
                }
            });
        });
        return future;
    }

    private CompletableFuture<Void> flush(final ServerName serverName, final RegionInfo regionInfo) {
        return this.<Void>newAdminCaller().serverName(serverName)
                .action((controller, stub) -> this.<FlushRegionRequest, FlushRegionResponse, Void>adminCall(
                        controller, stub, RequestConverter.buildFlushRegionRequest(regionInfo.getRegionName()),
                        (s, c, req, done) -> s.flushRegion(c, req, done), resp -> null))
                .call();
    }

    @Override
    public CompletableFuture<Void> flushRegionServer(ServerName sn) {
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(getRegions(sn), (hRegionInfos, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            List<CompletableFuture<Void>> compactFutures = new ArrayList<>();
            if (hRegionInfos != null) {
                hRegionInfos.forEach(region -> compactFutures.add(flush(sn, region)));
            }
            addListener(
                    CompletableFuture
                            .allOf(compactFutures.toArray(new CompletableFuture<?>[compactFutures.size()])),
                    (ret, err2) -> {
                        if (err2 != null) {
                            future.completeExceptionally(err2);
                        } else {
                            future.complete(ret);
                        }
                    });
        });
        return future;
    }

    @Override
    public CompletableFuture<Void> compact(TableName tableName, CompactType compactType) {
        return compact(tableName, null, false, compactType);
    }

    @Override
    public CompletableFuture<Void> compact(TableName tableName, byte[] columnFamily, CompactType compactType) {
        Preconditions.checkNotNull(columnFamily,
                "columnFamily is null. " + "If you don't specify a columnFamily, use compact(TableName) instead");
        return compact(tableName, columnFamily, false, compactType);
    }

    @Override
    public CompletableFuture<Void> compactRegion(byte[] regionName) {
        return compactRegion(regionName, null, false);
    }

    @Override
    public CompletableFuture<Void> compactRegion(byte[] regionName, byte[] columnFamily) {
        Preconditions.checkNotNull(columnFamily, "columnFamily is null."
                + " If you don't specify a columnFamily, use compactRegion(regionName) instead");
        return compactRegion(regionName, columnFamily, false);
    }

    @Override
    public CompletableFuture<Void> majorCompact(TableName tableName, CompactType compactType) {
        return compact(tableName, null, true, compactType);
    }

    @Override
    public CompletableFuture<Void> majorCompact(TableName tableName, byte[] columnFamily, CompactType compactType) {
        Preconditions.checkNotNull(columnFamily,
                "columnFamily is null." + "If you don't specify a columnFamily, use compact(TableName) instead");
        return compact(tableName, columnFamily, true, compactType);
    }

    @Override
    public CompletableFuture<Void> majorCompactRegion(byte[] regionName) {
        return compactRegion(regionName, null, true);
    }

    @Override
    public CompletableFuture<Void> majorCompactRegion(byte[] regionName, byte[] columnFamily) {
        Preconditions.checkNotNull(columnFamily, "columnFamily is null."
                + " If you don't specify a columnFamily, use majorCompactRegion(regionName) instead");
        return compactRegion(regionName, columnFamily, true);
    }

    @Override
    public CompletableFuture<Void> compactRegionServer(ServerName sn) {
        return compactRegionServer(sn, false);
    }

    @Override
    public CompletableFuture<Void> majorCompactRegionServer(ServerName sn) {
        return compactRegionServer(sn, true);
    }

    private CompletableFuture<Void> compactRegionServer(ServerName sn, boolean major) {
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(getRegions(sn), (hRegionInfos, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            List<CompletableFuture<Void>> compactFutures = new ArrayList<>();
            if (hRegionInfos != null) {
                hRegionInfos.forEach(region -> compactFutures.add(compact(sn, region, major, null)));
            }
            addListener(
                    CompletableFuture
                            .allOf(compactFutures.toArray(new CompletableFuture<?>[compactFutures.size()])),
                    (ret, err2) -> {
                        if (err2 != null) {
                            future.completeExceptionally(err2);
                        } else {
                            future.complete(ret);
                        }
                    });
        });
        return future;
    }

    private CompletableFuture<Void> compactRegion(byte[] regionName, byte[] columnFamily, boolean major) {
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(getRegionLocation(regionName), (location, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            ServerName serverName = location.getServerName();
            if (serverName == null) {
                future.completeExceptionally(new NoServerForRegionException(Bytes.toStringBinary(regionName)));
                return;
            }
            addListener(compact(location.getServerName(), location.getRegion(), major, columnFamily),
                    (ret, err2) -> {
                        if (err2 != null) {
                            future.completeExceptionally(err2);
                        } else {
                            future.complete(ret);
                        }
                    });
        });
        return future;
    }

    /**
     * List all region locations for the specific table.
     */
    private CompletableFuture<List<HRegionLocation>> getTableHRegionLocations(TableName tableName) {
        if (TableName.META_TABLE_NAME.equals(tableName)) {
            CompletableFuture<List<HRegionLocation>> future = new CompletableFuture<>();
            // For meta table, we use zk to fetch all locations.
            AsyncRegistry registry = AsyncRegistryFactory.getRegistry(connection.getConfiguration());
            addListener(registry.getMetaRegionLocation(), (metaRegions, err) -> {
                if (err != null) {
                    future.completeExceptionally(err);
                } else if (metaRegions == null || metaRegions.isEmpty()
                        || metaRegions.getDefaultRegionLocation() == null) {
                    future.completeExceptionally(new IOException("meta region does not found"));
                } else {
                    future.complete(Collections.singletonList(metaRegions.getDefaultRegionLocation()));
                }
                // close the registry.
                IOUtils.closeQuietly(registry);
            });
            return future;
        } else {
            // For non-meta table, we fetch all locations by scanning hbase:meta table
            return AsyncMetaTableAccessor.getTableHRegionLocations(metaTable, Optional.of(tableName));
        }
    }

    /**
     * Compact column family of a table, Asynchronous operation even if CompletableFuture.get()
     */
    private CompletableFuture<Void> compact(TableName tableName, byte[] columnFamily, boolean major,
            CompactType compactType) {
        CompletableFuture<Void> future = new CompletableFuture<>();

        switch (compactType) {
        case MOB:
            addListener(connection.registry.getMasterAddress(), (serverName, err) -> {
                if (err != null) {
                    future.completeExceptionally(err);
                    return;
                }
                RegionInfo regionInfo = RegionInfo.createMobRegionInfo(tableName);
                addListener(compact(serverName, regionInfo, major, columnFamily), (ret, err2) -> {
                    if (err2 != null) {
                        future.completeExceptionally(err2);
                    } else {
                        future.complete(ret);
                    }
                });
            });
            break;
        case NORMAL:
            addListener(getTableHRegionLocations(tableName), (locations, err) -> {
                if (err != null) {
                    future.completeExceptionally(err);
                    return;
                }
                if (locations == null || locations.isEmpty()) {
                    future.completeExceptionally(new TableNotFoundException(tableName));
                }
                CompletableFuture<?>[] compactFutures = locations.stream().filter(l -> l.getRegion() != null)
                        .filter(l -> !l.getRegion().isOffline()).filter(l -> l.getServerName() != null)
                        .map(l -> compact(l.getServerName(), l.getRegion(), major, columnFamily))
                        .toArray(CompletableFuture<?>[]::new);
                // future complete unless all of the compact futures are completed.
                addListener(CompletableFuture.allOf(compactFutures), (ret, err2) -> {
                    if (err2 != null) {
                        future.completeExceptionally(err2);
                    } else {
                        future.complete(ret);
                    }
                });
            });
            break;
        default:
            throw new IllegalArgumentException("Unknown compactType: " + compactType);
        }
        return future;
    }

    /**
     * Compact the region at specific region server.
     */
    private CompletableFuture<Void> compact(final ServerName sn, final RegionInfo hri, final boolean major,
            byte[] columnFamily) {
        return this.<Void>newAdminCaller().serverName(sn)
                .action((controller, stub) -> this.<CompactRegionRequest, CompactRegionResponse, Void>adminCall(
                        controller, stub,
                        RequestConverter.buildCompactRegionRequest(hri.getRegionName(), major, columnFamily),
                        (s, c, req, done) -> s.compactRegion(c, req, done), resp -> null))
                .call();
    }

    private byte[] toEncodeRegionName(byte[] regionName) {
        try {
            return RegionInfo.isEncodedRegionName(regionName) ? regionName
                    : Bytes.toBytes(RegionInfo.encodeRegionName(regionName));
        } catch (IOException e) {
            return regionName;
        }
    }

    private void checkAndGetTableName(byte[] encodeRegionName, AtomicReference<TableName> tableName,
            CompletableFuture<TableName> result) {
        addListener(getRegionLocation(encodeRegionName), (location, err) -> {
            if (err != null) {
                result.completeExceptionally(err);
                return;
            }
            RegionInfo regionInfo = location.getRegion();
            if (regionInfo.getReplicaId() != RegionInfo.DEFAULT_REPLICA_ID) {
                result.completeExceptionally(
                        new IllegalArgumentException("Can't invoke merge on non-default regions directly"));
                return;
            }
            if (!tableName.compareAndSet(null, regionInfo.getTable())) {
                if (!tableName.get().equals(regionInfo.getTable())) {
                    // tables of this two region should be same.
                    result.completeExceptionally(
                            new IllegalArgumentException("Cannot merge regions from two different tables "
                                    + tableName.get() + " and " + regionInfo.getTable()));
                } else {
                    result.complete(tableName.get());
                }
            }
        });
    }

    private CompletableFuture<TableName> checkRegionsAndGetTableName(byte[][] encodedRegionNames) {
        AtomicReference<TableName> tableNameRef = new AtomicReference<>();
        CompletableFuture<TableName> future = new CompletableFuture<>();
        for (byte[] encodedRegionName : encodedRegionNames) {
            checkAndGetTableName(encodedRegionName, tableNameRef, future);
        }
        return future;
    }

    @Override
    public CompletableFuture<Boolean> mergeSwitch(boolean enabled, boolean drainMerges) {
        return setSplitOrMergeOn(enabled, drainMerges, MasterSwitchType.MERGE);
    }

    @Override
    public CompletableFuture<Boolean> isMergeEnabled() {
        return isSplitOrMergeOn(MasterSwitchType.MERGE);
    }

    @Override
    public CompletableFuture<Boolean> splitSwitch(boolean enabled, boolean drainSplits) {
        return setSplitOrMergeOn(enabled, drainSplits, MasterSwitchType.SPLIT);
    }

    @Override
    public CompletableFuture<Boolean> isSplitEnabled() {
        return isSplitOrMergeOn(MasterSwitchType.SPLIT);
    }

    private CompletableFuture<Boolean> setSplitOrMergeOn(boolean enabled, boolean synchronous,
            MasterSwitchType switchType) {
        SetSplitOrMergeEnabledRequest request = RequestConverter.buildSetSplitOrMergeEnabledRequest(enabled,
                synchronous, switchType);
        return this.<Boolean>newMasterCaller()
                .action((controller, stub) -> this
                        .<SetSplitOrMergeEnabledRequest, SetSplitOrMergeEnabledResponse, Boolean>call(controller,
                                stub, request, (s, c, req, done) -> s.setSplitOrMergeEnabled(c, req, done),
                                (resp) -> resp.getPrevValueList().get(0)))
                .call();
    }

    private CompletableFuture<Boolean> isSplitOrMergeOn(MasterSwitchType switchType) {
        IsSplitOrMergeEnabledRequest request = RequestConverter.buildIsSplitOrMergeEnabledRequest(switchType);
        return this.<Boolean>newMasterCaller()
                .action((controller, stub) -> this
                        .<IsSplitOrMergeEnabledRequest, IsSplitOrMergeEnabledResponse, Boolean>call(controller,
                                stub, request, (s, c, req, done) -> s.isSplitOrMergeEnabled(c, req, done),
                                (resp) -> resp.getEnabled()))
                .call();
    }

    @Override
    public CompletableFuture<Void> mergeRegions(List<byte[]> nameOfRegionsToMerge, boolean forcible) {
        if (nameOfRegionsToMerge.size() < 2) {
            return failedFuture(
                    new IllegalArgumentException("Can not merge only " + nameOfRegionsToMerge.size() + " region"));
        }
        CompletableFuture<Void> future = new CompletableFuture<>();
        byte[][] encodedNameOfRegionsToMerge = nameOfRegionsToMerge.stream().map(this::toEncodeRegionName)
                .toArray(byte[][]::new);

        addListener(checkRegionsAndGetTableName(encodedNameOfRegionsToMerge), (tableName, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }

            MergeTableRegionsRequest request = null;
            try {
                request = RequestConverter.buildMergeTableRegionsRequest(encodedNameOfRegionsToMerge, forcible,
                        ng.getNonceGroup(), ng.newNonce());
            } catch (DeserializationException e) {
                future.completeExceptionally(e);
                return;
            }

            addListener(this.<MergeTableRegionsRequest, MergeTableRegionsResponse>procedureCall(tableName, request,
                    (s, c, req, done) -> s.mergeTableRegions(c, req, done), (resp) -> resp.getProcId(),
                    new MergeTableRegionProcedureBiConsumer(tableName)), (ret, err2) -> {
                        if (err2 != null) {
                            future.completeExceptionally(err2);
                        } else {
                            future.complete(ret);
                        }
                    });
        });
        return future;
    }

    @Override
    public CompletableFuture<Void> split(TableName tableName) {
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(tableExists(tableName), (exist, error) -> {
            if (error != null) {
                future.completeExceptionally(error);
                return;
            }
            if (!exist) {
                future.completeExceptionally(new TableNotFoundException(tableName));
                return;
            }
            addListener(
                    metaTable.scanAll(new Scan().setReadType(ReadType.PREAD).addFamily(HConstants.CATALOG_FAMILY)
                            .withStartRow(MetaTableAccessor.getTableStartRowForMeta(tableName, QueryType.REGION))
                            .withStopRow(MetaTableAccessor.getTableStopRowForMeta(tableName, QueryType.REGION))),
                    (results, err2) -> {
                        if (err2 != null) {
                            future.completeExceptionally(err2);
                            return;
                        }
                        if (results != null && !results.isEmpty()) {
                            List<CompletableFuture<Void>> splitFutures = new ArrayList<>();
                            for (Result r : results) {
                                if (r.isEmpty() || MetaTableAccessor.getRegionInfo(r) == null) {
                                    continue;
                                }
                                RegionLocations rl = MetaTableAccessor.getRegionLocations(r);
                                if (rl != null) {
                                    for (HRegionLocation h : rl.getRegionLocations()) {
                                        if (h != null && h.getServerName() != null) {
                                            RegionInfo hri = h.getRegion();
                                            if (hri == null || hri.isSplitParent()
                                                    || hri.getReplicaId() != RegionInfo.DEFAULT_REPLICA_ID) {
                                                continue;
                                            }
                                            splitFutures.add(split(hri, null));
                                        }
                                    }
                                }
                            }
                            addListener(
                                    CompletableFuture.allOf(
                                            splitFutures.toArray(new CompletableFuture<?>[splitFutures.size()])),
                                    (ret, exception) -> {
                                        if (exception != null) {
                                            future.completeExceptionally(exception);
                                            return;
                                        }
                                        future.complete(ret);
                                    });
                        } else {
                            future.complete(null);
                        }
                    });
        });
        return future;
    }

    @Override
    public CompletableFuture<Void> split(TableName tableName, byte[] splitPoint) {
        CompletableFuture<Void> result = new CompletableFuture<>();
        if (splitPoint == null) {
            return failedFuture(new IllegalArgumentException("splitPoint can not be null."));
        }
        addListener(connection.getRegionLocator(tableName).getRegionLocation(splitPoint, true), (loc, err) -> {
            if (err != null) {
                result.completeExceptionally(err);
            } else if (loc == null || loc.getRegion() == null) {
                result.completeExceptionally(new IllegalArgumentException(
                        "Region does not found: rowKey=" + Bytes.toStringBinary(splitPoint)));
            } else {
                addListener(splitRegion(loc.getRegion().getRegionName(), splitPoint), (ret, err2) -> {
                    if (err2 != null) {
                        result.completeExceptionally(err2);
                    } else {
                        result.complete(ret);
                    }

                });
            }
        });
        return result;
    }

    @Override
    public CompletableFuture<Void> splitRegion(byte[] regionName) {
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(getRegionLocation(regionName), (location, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            RegionInfo regionInfo = location.getRegion();
            if (regionInfo.getReplicaId() != RegionInfo.DEFAULT_REPLICA_ID) {
                future.completeExceptionally(new IllegalArgumentException("Can't split replicas directly. "
                        + "Replicas are auto-split when their primary is split."));
                return;
            }
            ServerName serverName = location.getServerName();
            if (serverName == null) {
                future.completeExceptionally(new NoServerForRegionException(Bytes.toStringBinary(regionName)));
                return;
            }
            addListener(split(regionInfo, null), (ret, err2) -> {
                if (err2 != null) {
                    future.completeExceptionally(err2);
                } else {
                    future.complete(ret);
                }
            });
        });
        return future;
    }

    @Override
    public CompletableFuture<Void> splitRegion(byte[] regionName, byte[] splitPoint) {
        Preconditions.checkNotNull(splitPoint,
                "splitPoint is null. If you don't specify a splitPoint, use splitRegion(byte[]) instead");
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(getRegionLocation(regionName), (location, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            RegionInfo regionInfo = location.getRegion();
            if (regionInfo.getReplicaId() != RegionInfo.DEFAULT_REPLICA_ID) {
                future.completeExceptionally(new IllegalArgumentException("Can't split replicas directly. "
                        + "Replicas are auto-split when their primary is split."));
                return;
            }
            ServerName serverName = location.getServerName();
            if (serverName == null) {
                future.completeExceptionally(new NoServerForRegionException(Bytes.toStringBinary(regionName)));
                return;
            }
            if (regionInfo.getStartKey() != null && Bytes.compareTo(regionInfo.getStartKey(), splitPoint) == 0) {
                future.completeExceptionally(
                        new IllegalArgumentException("should not give a splitkey which equals to startkey!"));
                return;
            }
            addListener(split(regionInfo, splitPoint), (ret, err2) -> {
                if (err2 != null) {
                    future.completeExceptionally(err2);
                } else {
                    future.complete(ret);
                }
            });
        });
        return future;
    }

    private CompletableFuture<Void> split(final RegionInfo hri, byte[] splitPoint) {
        CompletableFuture<Void> future = new CompletableFuture<>();
        TableName tableName = hri.getTable();
        SplitTableRegionRequest request = null;
        try {
            request = RequestConverter.buildSplitTableRegionRequest(hri, splitPoint, ng.getNonceGroup(),
                    ng.newNonce());
        } catch (DeserializationException e) {
            future.completeExceptionally(e);
            return future;
        }

        addListener(this.<SplitTableRegionRequest, SplitTableRegionResponse>procedureCall(tableName, request,
                (s, c, req, done) -> s.splitRegion(c, req, done), (resp) -> resp.getProcId(),
                new SplitTableRegionProcedureBiConsumer(tableName)), (ret, err2) -> {
                    if (err2 != null) {
                        future.completeExceptionally(err2);
                    } else {
                        future.complete(ret);
                    }
                });
        return future;
    }

    @Override
    public CompletableFuture<Void> assign(byte[] regionName) {
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(getRegionInfo(regionName), (regionInfo, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            addListener(this.<Void>newMasterCaller().priority(regionInfo.getTable())
                    .action(((controller, stub) -> this.<AssignRegionRequest, AssignRegionResponse, Void>call(
                            controller, stub, RequestConverter.buildAssignRegionRequest(regionInfo.getRegionName()),
                            (s, c, req, done) -> s.assignRegion(c, req, done), resp -> null)))
                    .call(), (ret, err2) -> {
                        if (err2 != null) {
                            future.completeExceptionally(err2);
                        } else {
                            future.complete(ret);
                        }
                    });
        });
        return future;
    }

    @Override
    public CompletableFuture<Void> unassign(byte[] regionName, boolean forcible) {
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(getRegionInfo(regionName), (regionInfo, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            addListener(this.<Void>newMasterCaller().priority(regionInfo.getTable())
                    .action(((controller, stub) -> this.<UnassignRegionRequest, UnassignRegionResponse, Void>call(
                            controller, stub,
                            RequestConverter.buildUnassignRegionRequest(regionInfo.getRegionName(), forcible),
                            (s, c, req, done) -> s.unassignRegion(c, req, done), resp -> null)))
                    .call(), (ret, err2) -> {
                        if (err2 != null) {
                            future.completeExceptionally(err2);
                        } else {
                            future.complete(ret);
                        }
                    });
        });
        return future;
    }

    @Override
    public CompletableFuture<Void> offline(byte[] regionName) {
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(getRegionInfo(regionName), (regionInfo, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            addListener(this.<Void>newMasterCaller().priority(regionInfo.getTable())
                    .action(((controller, stub) -> this.<OfflineRegionRequest, OfflineRegionResponse, Void>call(
                            controller, stub,
                            RequestConverter.buildOfflineRegionRequest(regionInfo.getRegionName()),
                            (s, c, req, done) -> s.offlineRegion(c, req, done), resp -> null)))
                    .call(), (ret, err2) -> {
                        if (err2 != null) {
                            future.completeExceptionally(err2);
                        } else {
                            future.complete(ret);
                        }
                    });
        });
        return future;
    }

    @Override
    public CompletableFuture<Void> move(byte[] regionName) {
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(getRegionInfo(regionName), (regionInfo, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            addListener(
                    moveRegion(regionInfo,
                            RequestConverter.buildMoveRegionRequest(regionInfo.getEncodedNameAsBytes(), null)),
                    (ret, err2) -> {
                        if (err2 != null) {
                            future.completeExceptionally(err2);
                        } else {
                            future.complete(ret);
                        }
                    });
        });
        return future;
    }

    @Override
    public CompletableFuture<Void> move(byte[] regionName, ServerName destServerName) {
        Preconditions.checkNotNull(destServerName,
                "destServerName is null. If you don't specify a destServerName, use move(byte[]) instead");
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(getRegionInfo(regionName), (regionInfo, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            addListener(
                    moveRegion(regionInfo, RequestConverter
                            .buildMoveRegionRequest(regionInfo.getEncodedNameAsBytes(), destServerName)),
                    (ret, err2) -> {
                        if (err2 != null) {
                            future.completeExceptionally(err2);
                        } else {
                            future.complete(ret);
                        }
                    });
        });
        return future;
    }

    private CompletableFuture<Void> moveRegion(RegionInfo regionInfo, MoveRegionRequest request) {
        return this.<Void>newMasterCaller().priority(regionInfo.getTable())
                .action((controller, stub) -> this.<MoveRegionRequest, MoveRegionResponse, Void>call(controller,
                        stub, request, (s, c, req, done) -> s.moveRegion(c, req, done), resp -> null))
                .call();
    }

    @Override
    public CompletableFuture<Void> setQuota(QuotaSettings quota) {
        return this.<Void>newMasterCaller()
                .action((controller, stub) -> this.<SetQuotaRequest, SetQuotaResponse, Void>call(controller, stub,
                        QuotaSettings.buildSetQuotaRequestProto(quota),
                        (s, c, req, done) -> s.setQuota(c, req, done), (resp) -> null))
                .call();
    }

    @Override
    public CompletableFuture<List<QuotaSettings>> getQuota(QuotaFilter filter) {
        CompletableFuture<List<QuotaSettings>> future = new CompletableFuture<>();
        Scan scan = QuotaTableUtil.makeScan(filter);
        this.connection.getTableBuilder(QuotaTableUtil.QUOTA_TABLE_NAME).build().scan(scan,
                new AdvancedScanResultConsumer() {
                    List<QuotaSettings> settings = new ArrayList<>();

                    @Override
                    public void onNext(Result[] results, ScanController controller) {
                        for (Result result : results) {
                            try {
                                QuotaTableUtil.parseResultToCollection(result, settings);
                            } catch (IOException e) {
                                controller.terminate();
                                future.completeExceptionally(e);
                            }
                        }
                    }

                    @Override
                    public void onError(Throwable error) {
                        future.completeExceptionally(error);
                    }

                    @Override
                    public void onComplete() {
                        future.complete(settings);
                    }
                });
        return future;
    }

    @Override
    public CompletableFuture<Void> addReplicationPeer(String peerId, ReplicationPeerConfig peerConfig,
            boolean enabled) {
        return this.<AddReplicationPeerRequest, AddReplicationPeerResponse>procedureCall(
                RequestConverter.buildAddReplicationPeerRequest(peerId, peerConfig, enabled),
                (s, c, req, done) -> s.addReplicationPeer(c, req, done), (resp) -> resp.getProcId(),
                new ReplicationProcedureBiConsumer(peerId, () -> "ADD_REPLICATION_PEER"));
    }

    @Override
    public CompletableFuture<Void> removeReplicationPeer(String peerId) {
        return this.<RemoveReplicationPeerRequest, RemoveReplicationPeerResponse>procedureCall(
                RequestConverter.buildRemoveReplicationPeerRequest(peerId),
                (s, c, req, done) -> s.removeReplicationPeer(c, req, done), (resp) -> resp.getProcId(),
                new ReplicationProcedureBiConsumer(peerId, () -> "REMOVE_REPLICATION_PEER"));
    }

    @Override
    public CompletableFuture<Void> enableReplicationPeer(String peerId) {
        return this.<EnableReplicationPeerRequest, EnableReplicationPeerResponse>procedureCall(
                RequestConverter.buildEnableReplicationPeerRequest(peerId),
                (s, c, req, done) -> s.enableReplicationPeer(c, req, done), (resp) -> resp.getProcId(),
                new ReplicationProcedureBiConsumer(peerId, () -> "ENABLE_REPLICATION_PEER"));
    }

    @Override
    public CompletableFuture<Void> disableReplicationPeer(String peerId) {
        return this.<DisableReplicationPeerRequest, DisableReplicationPeerResponse>procedureCall(
                RequestConverter.buildDisableReplicationPeerRequest(peerId),
                (s, c, req, done) -> s.disableReplicationPeer(c, req, done), (resp) -> resp.getProcId(),
                new ReplicationProcedureBiConsumer(peerId, () -> "DISABLE_REPLICATION_PEER"));
    }

    @Override
    public CompletableFuture<ReplicationPeerConfig> getReplicationPeerConfig(String peerId) {
        return this.<ReplicationPeerConfig>newMasterCaller().action((controller, stub) -> this
                .<GetReplicationPeerConfigRequest, GetReplicationPeerConfigResponse, ReplicationPeerConfig>call(
                        controller, stub, RequestConverter.buildGetReplicationPeerConfigRequest(peerId),
                        (s, c, req, done) -> s.getReplicationPeerConfig(c, req, done),
                        (resp) -> ReplicationPeerConfigUtil.convert(resp.getPeerConfig())))
                .call();
    }

    @Override
    public CompletableFuture<Void> updateReplicationPeerConfig(String peerId, ReplicationPeerConfig peerConfig) {
        return this.<UpdateReplicationPeerConfigRequest, UpdateReplicationPeerConfigResponse>procedureCall(
                RequestConverter.buildUpdateReplicationPeerConfigRequest(peerId, peerConfig),
                (s, c, req, done) -> s.updateReplicationPeerConfig(c, req, done), (resp) -> resp.getProcId(),
                new ReplicationProcedureBiConsumer(peerId, () -> "UPDATE_REPLICATION_PEER_CONFIG"));
    }

    @Override
    public CompletableFuture<Void> transitReplicationPeerSyncReplicationState(String peerId,
            SyncReplicationState clusterState) {
        return this
                .<TransitReplicationPeerSyncReplicationStateRequest, TransitReplicationPeerSyncReplicationStateResponse>procedureCall(
                        RequestConverter.buildTransitReplicationPeerSyncReplicationStateRequest(peerId,
                                clusterState),
                        (s, c, req, done) -> s.transitReplicationPeerSyncReplicationState(c, req, done),
                        (resp) -> resp.getProcId(), new ReplicationProcedureBiConsumer(peerId,
                                () -> "TRANSIT_REPLICATION_PEER_SYNCHRONOUS_REPLICATION_STATE"));
    }

    @Override
    public CompletableFuture<Void> appendReplicationPeerTableCFs(String id, Map<TableName, List<String>> tableCfs) {
        if (tableCfs == null) {
            return failedFuture(new ReplicationException("tableCfs is null"));
        }

        CompletableFuture<Void> future = new CompletableFuture<Void>();
        addListener(getReplicationPeerConfig(id), (peerConfig, error) -> {
            if (!completeExceptionally(future, error)) {
                ReplicationPeerConfig newPeerConfig = ReplicationPeerConfigUtil
                        .appendTableCFsToReplicationPeerConfig(tableCfs, peerConfig);
                addListener(updateReplicationPeerConfig(id, newPeerConfig), (result, err) -> {
                    if (!completeExceptionally(future, error)) {
                        future.complete(result);
                    }
                });
            }
        });
        return future;
    }

    @Override
    public CompletableFuture<Void> removeReplicationPeerTableCFs(String id, Map<TableName, List<String>> tableCfs) {
        if (tableCfs == null) {
            return failedFuture(new ReplicationException("tableCfs is null"));
        }

        CompletableFuture<Void> future = new CompletableFuture<Void>();
        addListener(getReplicationPeerConfig(id), (peerConfig, error) -> {
            if (!completeExceptionally(future, error)) {
                ReplicationPeerConfig newPeerConfig = null;
                try {
                    newPeerConfig = ReplicationPeerConfigUtil.removeTableCFsFromReplicationPeerConfig(tableCfs,
                            peerConfig, id);
                } catch (ReplicationException e) {
                    future.completeExceptionally(e);
                    return;
                }
                addListener(updateReplicationPeerConfig(id, newPeerConfig), (result, err) -> {
                    if (!completeExceptionally(future, error)) {
                        future.complete(result);
                    }
                });
            }
        });
        return future;
    }

    @Override
    public CompletableFuture<List<ReplicationPeerDescription>> listReplicationPeers() {
        return listReplicationPeers(RequestConverter.buildListReplicationPeersRequest(null));
    }

    @Override
    public CompletableFuture<List<ReplicationPeerDescription>> listReplicationPeers(Pattern pattern) {
        Preconditions.checkNotNull(pattern,
                "pattern is null. If you don't specify a pattern, use listReplicationPeers() instead");
        return listReplicationPeers(RequestConverter.buildListReplicationPeersRequest(pattern));
    }

    private CompletableFuture<List<ReplicationPeerDescription>> listReplicationPeers(
            ListReplicationPeersRequest request) {
        return this.<List<ReplicationPeerDescription>>newMasterCaller().action((controller, stub) -> this
                .<ListReplicationPeersRequest, ListReplicationPeersResponse, List<ReplicationPeerDescription>>call(
                        controller, stub, request, (s, c, req, done) -> s.listReplicationPeers(c, req, done),
                        (resp) -> resp.getPeerDescList().stream()
                                .map(ReplicationPeerConfigUtil::toReplicationPeerDescription)
                                .collect(Collectors.toList())))
                .call();
    }

    @Override
    public CompletableFuture<List<TableCFs>> listReplicatedTableCFs() {
        CompletableFuture<List<TableCFs>> future = new CompletableFuture<List<TableCFs>>();
        addListener(listTableDescriptors(), (tables, error) -> {
            if (!completeExceptionally(future, error)) {
                List<TableCFs> replicatedTableCFs = new ArrayList<>();
                tables.forEach(table -> {
                    Map<String, Integer> cfs = new HashMap<>();
                    Stream.of(table.getColumnFamilies())
                            .filter(column -> column.getScope() != HConstants.REPLICATION_SCOPE_LOCAL)
                            .forEach(column -> {
                                cfs.put(column.getNameAsString(), column.getScope());
                            });
                    if (!cfs.isEmpty()) {
                        replicatedTableCFs.add(new TableCFs(table.getTableName(), cfs));
                    }
                });
                future.complete(replicatedTableCFs);
            }
        });
        return future;
    }

    @Override
    public CompletableFuture<Void> snapshot(SnapshotDescription snapshotDesc) {
        SnapshotProtos.SnapshotDescription snapshot = ProtobufUtil.createHBaseProtosSnapshotDesc(snapshotDesc);
        try {
            ClientSnapshotDescriptionUtils.assertSnapshotRequestIsValid(snapshot);
        } catch (IllegalArgumentException e) {
            return failedFuture(e);
        }
        CompletableFuture<Void> future = new CompletableFuture<>();
        final SnapshotRequest request = SnapshotRequest.newBuilder().setSnapshot(snapshot).build();
        addListener(this.<Long>newMasterCaller()
                .action((controller, stub) -> this.<SnapshotRequest, SnapshotResponse, Long>call(controller, stub,
                        request, (s, c, req, done) -> s.snapshot(c, req, done), resp -> resp.getExpectedTimeout()))
                .call(), (expectedTimeout, err) -> {
                    if (err != null) {
                        future.completeExceptionally(err);
                        return;
                    }
                    TimerTask pollingTask = new TimerTask() {
                        int tries = 0;
                        long startTime = EnvironmentEdgeManager.currentTime();
                        long endTime = startTime + expectedTimeout;
                        long maxPauseTime = expectedTimeout / maxAttempts;

                        @Override
                        public void run(Timeout timeout) throws Exception {
                            if (EnvironmentEdgeManager.currentTime() < endTime) {
                                addListener(isSnapshotFinished(snapshotDesc), (done, err2) -> {
                                    if (err2 != null) {
                                        future.completeExceptionally(err2);
                                    } else if (done) {
                                        future.complete(null);
                                    } else {
                                        // retry again after pauseTime.
                                        long pauseTime = ConnectionUtils
                                                .getPauseTime(TimeUnit.NANOSECONDS.toMillis(pauseNs), ++tries);
                                        pauseTime = Math.min(pauseTime, maxPauseTime);
                                        AsyncConnectionImpl.RETRY_TIMER.newTimeout(this, pauseTime,
                                                TimeUnit.MILLISECONDS);
                                    }
                                });
                            } else {
                                future.completeExceptionally(new SnapshotCreationException(
                                        "Snapshot '" + snapshot.getName() + "' wasn't completed in expectedTime:"
                                                + expectedTimeout + " ms",
                                        snapshotDesc));
                            }
                        }
                    };
                    AsyncConnectionImpl.RETRY_TIMER.newTimeout(pollingTask, 1, TimeUnit.MILLISECONDS);
                });
        return future;
    }

    @Override
    public CompletableFuture<Boolean> isSnapshotFinished(SnapshotDescription snapshot) {
        return this.<Boolean>newMasterCaller()
                .action((controller, stub) -> this.<IsSnapshotDoneRequest, IsSnapshotDoneResponse, Boolean>call(
                        controller, stub,
                        IsSnapshotDoneRequest.newBuilder()
                                .setSnapshot(ProtobufUtil.createHBaseProtosSnapshotDesc(snapshot)).build(),
                        (s, c, req, done) -> s.isSnapshotDone(c, req, done), resp -> resp.getDone()))
                .call();
    }

    @Override
    public CompletableFuture<Void> restoreSnapshot(String snapshotName) {
        boolean takeFailSafeSnapshot = this.connection.getConfiguration().getBoolean(
                HConstants.SNAPSHOT_RESTORE_TAKE_FAILSAFE_SNAPSHOT,
                HConstants.DEFAULT_SNAPSHOT_RESTORE_TAKE_FAILSAFE_SNAPSHOT);
        return restoreSnapshot(snapshotName, takeFailSafeSnapshot);
    }

    @Override
    public CompletableFuture<Void> restoreSnapshot(String snapshotName, boolean takeFailSafeSnapshot,
            boolean restoreAcl) {
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(listSnapshots(Pattern.compile(snapshotName)), (snapshotDescriptions, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            TableName tableName = null;
            if (snapshotDescriptions != null && !snapshotDescriptions.isEmpty()) {
                for (SnapshotDescription snap : snapshotDescriptions) {
                    if (snap.getName().equals(snapshotName)) {
                        tableName = snap.getTableName();
                        break;
                    }
                }
            }
            if (tableName == null) {
                future.completeExceptionally(
                        new RestoreSnapshotException("Unable to find the table name for snapshot=" + snapshotName));
                return;
            }
            final TableName finalTableName = tableName;
            addListener(tableExists(finalTableName), (exists, err2) -> {
                if (err2 != null) {
                    future.completeExceptionally(err2);
                } else if (!exists) {
                    // if table does not exist, then just clone snapshot into new table.
                    completeConditionalOnFuture(future,
                            internalRestoreSnapshot(snapshotName, finalTableName, restoreAcl));
                } else {
                    addListener(isTableDisabled(finalTableName), (disabled, err4) -> {
                        if (err4 != null) {
                            future.completeExceptionally(err4);
                        } else if (!disabled) {
                            future.completeExceptionally(new TableNotDisabledException(finalTableName));
                        } else {
                            completeConditionalOnFuture(future, restoreSnapshot(snapshotName, finalTableName,
                                    takeFailSafeSnapshot, restoreAcl));
                        }
                    });
                }
            });
        });
        return future;
    }

    private CompletableFuture<Void> restoreSnapshot(String snapshotName, TableName tableName,
            boolean takeFailSafeSnapshot, boolean restoreAcl) {
        if (takeFailSafeSnapshot) {
            CompletableFuture<Void> future = new CompletableFuture<>();
            // Step.1 Take a snapshot of the current state
            String failSafeSnapshotSnapshotNameFormat = this.connection.getConfiguration().get(
                    HConstants.SNAPSHOT_RESTORE_FAILSAFE_NAME, HConstants.DEFAULT_SNAPSHOT_RESTORE_FAILSAFE_NAME);
            final String failSafeSnapshotSnapshotName = failSafeSnapshotSnapshotNameFormat
                    .replace("{snapshot.name}", snapshotName)
                    .replace("{table.name}", tableName.toString().replace(TableName.NAMESPACE_DELIM, '.'))
                    .replace("{restore.timestamp}", String.valueOf(EnvironmentEdgeManager.currentTime()));
            LOG.info("Taking restore-failsafe snapshot: " + failSafeSnapshotSnapshotName);
            addListener(snapshot(failSafeSnapshotSnapshotName, tableName), (ret, err) -> {
                if (err != null) {
                    future.completeExceptionally(err);
                } else {
                    // Step.2 Restore snapshot
                    addListener(internalRestoreSnapshot(snapshotName, tableName, restoreAcl), (void2, err2) -> {
                        if (err2 != null) {
                            // Step.3.a Something went wrong during the restore and try to rollback.
                            addListener(
                                    internalRestoreSnapshot(failSafeSnapshotSnapshotName, tableName, restoreAcl),
                                    (void3, err3) -> {
                                        if (err3 != null) {
                                            future.completeExceptionally(err3);
                                        } else {
                                            String msg = "Restore snapshot=" + snapshotName
                                                    + " failed. Rollback to snapshot="
                                                    + failSafeSnapshotSnapshotName + " succeeded.";
                                            future.completeExceptionally(new RestoreSnapshotException(msg, err2));
                                        }
                                    });
                        } else {
                            // Step.3.b If the restore is succeeded, delete the pre-restore snapshot.
                            LOG.info("Deleting restore-failsafe snapshot: " + failSafeSnapshotSnapshotName);
                            addListener(deleteSnapshot(failSafeSnapshotSnapshotName), (ret3, err3) -> {
                                if (err3 != null) {
                                    LOG.error("Unable to remove the failsafe snapshot: "
                                            + failSafeSnapshotSnapshotName, err3);
                                    future.completeExceptionally(err3);
                                } else {
                                    future.complete(ret3);
                                }
                            });
                        }
                    });
                }
            });
            return future;
        } else {
            return internalRestoreSnapshot(snapshotName, tableName, restoreAcl);
        }
    }

    private <T> void completeConditionalOnFuture(CompletableFuture<T> dependentFuture,
            CompletableFuture<T> parentFuture) {
        addListener(parentFuture, (res, err) -> {
            if (err != null) {
                dependentFuture.completeExceptionally(err);
            } else {
                dependentFuture.complete(res);
            }
        });
    }

    @Override
    public CompletableFuture<Void> cloneSnapshot(String snapshotName, TableName tableName, boolean restoreAcl) {
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(tableExists(tableName), (exists, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
            } else if (exists) {
                future.completeExceptionally(new TableExistsException(tableName));
            } else {
                completeConditionalOnFuture(future, internalRestoreSnapshot(snapshotName, tableName, restoreAcl));
            }
        });
        return future;
    }

    private CompletableFuture<Void> internalRestoreSnapshot(String snapshotName, TableName tableName,
            boolean restoreAcl) {
        SnapshotProtos.SnapshotDescription snapshot = SnapshotProtos.SnapshotDescription.newBuilder()
                .setName(snapshotName).setTable(tableName.getNameAsString()).build();
        try {
            ClientSnapshotDescriptionUtils.assertSnapshotRequestIsValid(snapshot);
        } catch (IllegalArgumentException e) {
            return failedFuture(e);
        }
        return waitProcedureResult(this.<Long>newMasterCaller()
                .action((controller, stub) -> this.<RestoreSnapshotRequest, RestoreSnapshotResponse, Long>call(
                        controller, stub,
                        RestoreSnapshotRequest.newBuilder().setSnapshot(snapshot).setNonceGroup(ng.getNonceGroup())
                                .setNonce(ng.newNonce()).setRestoreACL(restoreAcl).build(),
                        (s, c, req, done) -> s.restoreSnapshot(c, req, done), (resp) -> resp.getProcId()))
                .call());
    }

    @Override
    public CompletableFuture<List<SnapshotDescription>> listSnapshots() {
        return getCompletedSnapshots(null);
    }

    @Override
    public CompletableFuture<List<SnapshotDescription>> listSnapshots(Pattern pattern) {
        Preconditions.checkNotNull(pattern,
                "pattern is null. If you don't specify a pattern, use listSnapshots() instead");
        return getCompletedSnapshots(pattern);
    }

    private CompletableFuture<List<SnapshotDescription>> getCompletedSnapshots(Pattern pattern) {
        return this.<List<SnapshotDescription>>newMasterCaller().action((controller, stub) -> this
                .<GetCompletedSnapshotsRequest, GetCompletedSnapshotsResponse, List<SnapshotDescription>>call(
                        controller, stub, GetCompletedSnapshotsRequest.newBuilder().build(),
                        (s, c, req, done) -> s.getCompletedSnapshots(c, req, done),
                        resp -> ProtobufUtil.toSnapshotDescriptionList(resp, pattern)))
                .call();
    }

    @Override
    public CompletableFuture<List<SnapshotDescription>> listTableSnapshots(Pattern tableNamePattern) {
        Preconditions.checkNotNull(tableNamePattern, "tableNamePattern is null."
                + " If you don't specify a tableNamePattern, use listSnapshots() instead");
        return getCompletedSnapshots(tableNamePattern, null);
    }

    @Override
    public CompletableFuture<List<SnapshotDescription>> listTableSnapshots(Pattern tableNamePattern,
            Pattern snapshotNamePattern) {
        Preconditions.checkNotNull(tableNamePattern, "tableNamePattern is null."
                + " If you don't specify a tableNamePattern, use listSnapshots(Pattern) instead");
        Preconditions.checkNotNull(snapshotNamePattern, "snapshotNamePattern is null."
                + " If you don't specify a snapshotNamePattern, use listTableSnapshots(Pattern) instead");
        return getCompletedSnapshots(tableNamePattern, snapshotNamePattern);
    }

    private CompletableFuture<List<SnapshotDescription>> getCompletedSnapshots(Pattern tableNamePattern,
            Pattern snapshotNamePattern) {
        CompletableFuture<List<SnapshotDescription>> future = new CompletableFuture<>();
        addListener(listTableNames(tableNamePattern, false), (tableNames, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            if (tableNames == null || tableNames.size() <= 0) {
                future.complete(Collections.emptyList());
                return;
            }
            addListener(getCompletedSnapshots(snapshotNamePattern), (snapshotDescList, err2) -> {
                if (err2 != null) {
                    future.completeExceptionally(err2);
                    return;
                }
                if (snapshotDescList == null || snapshotDescList.isEmpty()) {
                    future.complete(Collections.emptyList());
                    return;
                }
                future.complete(snapshotDescList.stream()
                        .filter(snap -> (snap != null && tableNames.contains(snap.getTableName())))
                        .collect(Collectors.toList()));
            });
        });
        return future;
    }

    @Override
    public CompletableFuture<Void> deleteSnapshot(String snapshotName) {
        return internalDeleteSnapshot(new SnapshotDescription(snapshotName));
    }

    @Override
    public CompletableFuture<Void> deleteSnapshots() {
        return internalDeleteSnapshots(null, null);
    }

    @Override
    public CompletableFuture<Void> deleteSnapshots(Pattern snapshotNamePattern) {
        Preconditions.checkNotNull(snapshotNamePattern, "snapshotNamePattern is null."
                + " If you don't specify a snapshotNamePattern, use deleteSnapshots() instead");
        return internalDeleteSnapshots(null, snapshotNamePattern);
    }

    @Override
    public CompletableFuture<Void> deleteTableSnapshots(Pattern tableNamePattern) {
        Preconditions.checkNotNull(tableNamePattern, "tableNamePattern is null."
                + " If you don't specify a tableNamePattern, use deleteSnapshots() instead");
        return internalDeleteSnapshots(tableNamePattern, null);
    }

    @Override
    public CompletableFuture<Void> deleteTableSnapshots(Pattern tableNamePattern, Pattern snapshotNamePattern) {
        Preconditions.checkNotNull(tableNamePattern, "tableNamePattern is null."
                + " If you don't specify a tableNamePattern, use deleteSnapshots(Pattern) instead");
        Preconditions.checkNotNull(snapshotNamePattern, "snapshotNamePattern is null."
                + " If you don't specify a snapshotNamePattern, use deleteSnapshots(Pattern) instead");
        return internalDeleteSnapshots(tableNamePattern, snapshotNamePattern);
    }

    private CompletableFuture<Void> internalDeleteSnapshots(Pattern tableNamePattern, Pattern snapshotNamePattern) {
        CompletableFuture<List<SnapshotDescription>> listSnapshotsFuture;
        if (tableNamePattern == null) {
            listSnapshotsFuture = getCompletedSnapshots(snapshotNamePattern);
        } else {
            listSnapshotsFuture = getCompletedSnapshots(tableNamePattern, snapshotNamePattern);
        }
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(listSnapshotsFuture, ((snapshotDescriptions, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            if (snapshotDescriptions == null || snapshotDescriptions.isEmpty()) {
                future.complete(null);
                return;
            }
            addListener(CompletableFuture.allOf(snapshotDescriptions.stream().map(this::internalDeleteSnapshot)
                    .toArray(CompletableFuture[]::new)), (v, e) -> {
                        if (e != null) {
                            future.completeExceptionally(e);
                        } else {
                            future.complete(v);
                        }
                    });
        }));
        return future;
    }

    private CompletableFuture<Void> internalDeleteSnapshot(SnapshotDescription snapshot) {
        return this.<Void>newMasterCaller()
                .action((controller, stub) -> this.<DeleteSnapshotRequest, DeleteSnapshotResponse, Void>call(
                        controller, stub,
                        DeleteSnapshotRequest.newBuilder()
                                .setSnapshot(ProtobufUtil.createHBaseProtosSnapshotDesc(snapshot)).build(),
                        (s, c, req, done) -> s.deleteSnapshot(c, req, done), resp -> null))
                .call();
    }

    @Override
    public CompletableFuture<Void> execProcedure(String signature, String instance, Map<String, String> props) {
        CompletableFuture<Void> future = new CompletableFuture<>();
        ProcedureDescription procDesc = ProtobufUtil.buildProcedureDescription(signature, instance, props);
        addListener(this.<Long>newMasterCaller()
                .action((controller, stub) -> this.<ExecProcedureRequest, ExecProcedureResponse, Long>call(
                        controller, stub, ExecProcedureRequest.newBuilder().setProcedure(procDesc).build(),
                        (s, c, req, done) -> s.execProcedure(c, req, done), resp -> resp.getExpectedTimeout()))
                .call(), (expectedTimeout, err) -> {
                    if (err != null) {
                        future.completeExceptionally(err);
                        return;
                    }
                    TimerTask pollingTask = new TimerTask() {
                        int tries = 0;
                        long startTime = EnvironmentEdgeManager.currentTime();
                        long endTime = startTime + expectedTimeout;
                        long maxPauseTime = expectedTimeout / maxAttempts;

                        @Override
                        public void run(Timeout timeout) throws Exception {
                            if (EnvironmentEdgeManager.currentTime() < endTime) {
                                addListener(isProcedureFinished(signature, instance, props), (done, err2) -> {
                                    if (err2 != null) {
                                        future.completeExceptionally(err2);
                                        return;
                                    }
                                    if (done) {
                                        future.complete(null);
                                    } else {
                                        // retry again after pauseTime.
                                        long pauseTime = ConnectionUtils
                                                .getPauseTime(TimeUnit.NANOSECONDS.toMillis(pauseNs), ++tries);
                                        pauseTime = Math.min(pauseTime, maxPauseTime);
                                        AsyncConnectionImpl.RETRY_TIMER.newTimeout(this, pauseTime,
                                                TimeUnit.MICROSECONDS);
                                    }
                                });
                            } else {
                                future.completeExceptionally(
                                        new IOException("Procedure '" + signature + " : " + instance
                                                + "' wasn't completed in expectedTime:" + expectedTimeout + " ms"));
                            }
                        }
                    };
                    // Queue the polling task into RETRY_TIMER to poll procedure state asynchronously.
                    AsyncConnectionImpl.RETRY_TIMER.newTimeout(pollingTask, 1, TimeUnit.MILLISECONDS);
                });
        return future;
    }

    @Override
    public CompletableFuture<byte[]> execProcedureWithReturn(String signature, String instance,
            Map<String, String> props) {
        ProcedureDescription proDesc = ProtobufUtil.buildProcedureDescription(signature, instance, props);
        return this.<byte[]>newMasterCaller()
                .action((controller, stub) -> this.<ExecProcedureRequest, ExecProcedureResponse, byte[]>call(
                        controller, stub, ExecProcedureRequest.newBuilder().setProcedure(proDesc).build(),
                        (s, c, req, done) -> s.execProcedureWithRet(c, req, done),
                        resp -> resp.hasReturnData() ? resp.getReturnData().toByteArray() : null))
                .call();
    }

    @Override
    public CompletableFuture<Boolean> isProcedureFinished(String signature, String instance,
            Map<String, String> props) {
        ProcedureDescription proDesc = ProtobufUtil.buildProcedureDescription(signature, instance, props);
        return this.<Boolean>newMasterCaller()
                .action((controller, stub) -> this.<IsProcedureDoneRequest, IsProcedureDoneResponse, Boolean>call(
                        controller, stub, IsProcedureDoneRequest.newBuilder().setProcedure(proDesc).build(),
                        (s, c, req, done) -> s.isProcedureDone(c, req, done), resp -> resp.getDone()))
                .call();
    }

    @Override
    public CompletableFuture<Boolean> abortProcedure(long procId, boolean mayInterruptIfRunning) {
        return this.<Boolean>newMasterCaller()
                .action((controller, stub) -> this.<AbortProcedureRequest, AbortProcedureResponse, Boolean>call(
                        controller, stub, AbortProcedureRequest.newBuilder().setProcId(procId).build(),
                        (s, c, req, done) -> s.abortProcedure(c, req, done), resp -> resp.getIsProcedureAborted()))
                .call();
    }

    @Override
    public CompletableFuture<String> getProcedures() {
        return this.<String>newMasterCaller()
                .action((controller, stub) -> this.<GetProceduresRequest, GetProceduresResponse, String>call(
                        controller, stub, GetProceduresRequest.newBuilder().build(),
                        (s, c, req, done) -> s.getProcedures(c, req, done),
                        resp -> ProtobufUtil.toProcedureJson(resp.getProcedureList())))
                .call();
    }

    @Override
    public CompletableFuture<String> getLocks() {
        return this.<String>newMasterCaller()
                .action((controller, stub) -> this.<GetLocksRequest, GetLocksResponse, String>call(controller, stub,
                        GetLocksRequest.newBuilder().build(), (s, c, req, done) -> s.getLocks(c, req, done),
                        resp -> ProtobufUtil.toLockJson(resp.getLockList())))
                .call();
    }

    @Override
    public CompletableFuture<Void> decommissionRegionServers(List<ServerName> servers, boolean offload) {
        return this.<Void>newMasterCaller()
                .action((controller, stub) -> this
                        .<DecommissionRegionServersRequest, DecommissionRegionServersResponse, Void>call(controller,
                                stub, RequestConverter.buildDecommissionRegionServersRequest(servers, offload),
                                (s, c, req, done) -> s.decommissionRegionServers(c, req, done), resp -> null))
                .call();
    }

    @Override
    public CompletableFuture<List<ServerName>> listDecommissionedRegionServers() {
        return this.<List<ServerName>>newMasterCaller().action((controller, stub) -> this
                .<ListDecommissionedRegionServersRequest, ListDecommissionedRegionServersResponse, List<ServerName>>call(
                        controller, stub, ListDecommissionedRegionServersRequest.newBuilder().build(),
                        (s, c, req, done) -> s.listDecommissionedRegionServers(c, req, done),
                        resp -> resp.getServerNameList().stream().map(ProtobufUtil::toServerName)
                                .collect(Collectors.toList())))
                .call();
    }

    @Override
    public CompletableFuture<Void> recommissionRegionServer(ServerName server, List<byte[]> encodedRegionNames) {
        return this.<Void>newMasterCaller().action((controller, stub) -> this
                .<RecommissionRegionServerRequest, RecommissionRegionServerResponse, Void>call(controller, stub,
                        RequestConverter.buildRecommissionRegionServerRequest(server, encodedRegionNames),
                        (s, c, req, done) -> s.recommissionRegionServer(c, req, done), resp -> null))
                .call();
    }

    /**
     * Get the region location for the passed region name. The region name may be a full region name
     * or encoded region name. If the region does not found, then it'll throw an
     * UnknownRegionException wrapped by a {@link CompletableFuture}
     * @param regionNameOrEncodedRegionName region name or encoded region name
     * @return region location, wrapped by a {@link CompletableFuture}
     */
    @VisibleForTesting
    CompletableFuture<HRegionLocation> getRegionLocation(byte[] regionNameOrEncodedRegionName) {
        if (regionNameOrEncodedRegionName == null) {
            return failedFuture(new IllegalArgumentException("Passed region name can't be null"));
        }
        try {
            CompletableFuture<Optional<HRegionLocation>> future;
            if (RegionInfo.isEncodedRegionName(regionNameOrEncodedRegionName)) {
                String encodedName = Bytes.toString(regionNameOrEncodedRegionName);
                if (encodedName.length() < RegionInfo.MD5_HEX_LENGTH) {
                    // old format encodedName, should be meta region
                    future = connection.registry.getMetaRegionLocation()
                            .thenApply(locs -> Stream.of(locs.getRegionLocations())
                                    .filter(loc -> loc.getRegion().getEncodedName().equals(encodedName))
                                    .findFirst());
                } else {
                    future = AsyncMetaTableAccessor.getRegionLocationWithEncodedName(metaTable,
                            regionNameOrEncodedRegionName);
                }
            } else {
                RegionInfo regionInfo = MetaTableAccessor
                        .parseRegionInfoFromRegionName(regionNameOrEncodedRegionName);
                if (regionInfo.isMetaRegion()) {
                    future = connection.registry.getMetaRegionLocation()
                            .thenApply(locs -> Stream.of(locs.getRegionLocations())
                                    .filter(loc -> loc.getRegion().getReplicaId() == regionInfo.getReplicaId())
                                    .findFirst());
                } else {
                    future = AsyncMetaTableAccessor.getRegionLocation(metaTable, regionNameOrEncodedRegionName);
                }
            }

            CompletableFuture<HRegionLocation> returnedFuture = new CompletableFuture<>();
            addListener(future, (location, err) -> {
                if (err != null) {
                    returnedFuture.completeExceptionally(err);
                    return;
                }
                if (!location.isPresent() || location.get().getRegion() == null) {
                    returnedFuture.completeExceptionally(
                            new UnknownRegionException("Invalid region name or encoded region name: "
                                    + Bytes.toStringBinary(regionNameOrEncodedRegionName)));
                } else {
                    returnedFuture.complete(location.get());
                }
            });
            return returnedFuture;
        } catch (IOException e) {
            return failedFuture(e);
        }
    }

    /**
     * Get the region info for the passed region name. The region name may be a full region name or
     * encoded region name. If the region does not found, then it'll throw an UnknownRegionException
     * wrapped by a {@link CompletableFuture}
     * @param regionNameOrEncodedRegionName
     * @return region info, wrapped by a {@link CompletableFuture}
     */
    private CompletableFuture<RegionInfo> getRegionInfo(byte[] regionNameOrEncodedRegionName) {
        if (regionNameOrEncodedRegionName == null) {
            return failedFuture(new IllegalArgumentException("Passed region name can't be null"));
        }

        if (Bytes.equals(regionNameOrEncodedRegionName, RegionInfoBuilder.FIRST_META_REGIONINFO.getRegionName())
                || Bytes.equals(regionNameOrEncodedRegionName,
                        RegionInfoBuilder.FIRST_META_REGIONINFO.getEncodedNameAsBytes())) {
            return CompletableFuture.completedFuture(RegionInfoBuilder.FIRST_META_REGIONINFO);
        }

        CompletableFuture<RegionInfo> future = new CompletableFuture<>();
        addListener(getRegionLocation(regionNameOrEncodedRegionName), (location, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
            } else {
                future.complete(location.getRegion());
            }
        });
        return future;
    }

    private byte[][] getSplitKeys(byte[] startKey, byte[] endKey, int numRegions) {
        if (numRegions < 3) {
            throw new IllegalArgumentException("Must create at least three regions");
        } else if (Bytes.compareTo(startKey, endKey) >= 0) {
            throw new IllegalArgumentException("Start key must be smaller than end key");
        }
        if (numRegions == 3) {
            return new byte[][] { startKey, endKey };
        }
        byte[][] splitKeys = Bytes.split(startKey, endKey, numRegions - 3);
        if (splitKeys == null || splitKeys.length != numRegions - 1) {
            throw new IllegalArgumentException("Unable to split key range into enough regions");
        }
        return splitKeys;
    }

    private void verifySplitKeys(byte[][] splitKeys) {
        Arrays.sort(splitKeys, Bytes.BYTES_COMPARATOR);
        // Verify there are no duplicate split keys
        byte[] lastKey = null;
        for (byte[] splitKey : splitKeys) {
            if (Bytes.compareTo(splitKey, HConstants.EMPTY_BYTE_ARRAY) == 0) {
                throw new IllegalArgumentException("Empty split key must not be passed in the split keys.");
            }
            if (lastKey != null && Bytes.equals(splitKey, lastKey)) {
                throw new IllegalArgumentException("All split keys must be unique, " + "found duplicate: "
                        + Bytes.toStringBinary(splitKey) + ", " + Bytes.toStringBinary(lastKey));
            }
            lastKey = splitKey;
        }
    }

    private static abstract class ProcedureBiConsumer implements BiConsumer<Void, Throwable> {

        abstract void onFinished();

        abstract void onError(Throwable error);

        @Override
        public void accept(Void v, Throwable error) {
            if (error != null) {
                onError(error);
                return;
            }
            onFinished();
        }
    }

    private static abstract class TableProcedureBiConsumer extends ProcedureBiConsumer {
        protected final TableName tableName;

        TableProcedureBiConsumer(TableName tableName) {
            this.tableName = tableName;
        }

        abstract String getOperationType();

        String getDescription() {
            return "Operation: " + getOperationType() + ", " + "Table Name: "
                    + tableName.getNameWithNamespaceInclAsString();
        }

        @Override
        void onFinished() {
            LOG.info(getDescription() + " completed");
        }

        @Override
        void onError(Throwable error) {
            LOG.info(getDescription() + " failed with " + error.getMessage());
        }
    }

    private static abstract class NamespaceProcedureBiConsumer extends ProcedureBiConsumer {
        protected final String namespaceName;

        NamespaceProcedureBiConsumer(String namespaceName) {
            this.namespaceName = namespaceName;
        }

        abstract String getOperationType();

        String getDescription() {
            return "Operation: " + getOperationType() + ", Namespace: " + namespaceName;
        }

        @Override
        void onFinished() {
            LOG.info(getDescription() + " completed");
        }

        @Override
        void onError(Throwable error) {
            LOG.info(getDescription() + " failed with " + error.getMessage());
        }
    }

    private static class CreateTableProcedureBiConsumer extends TableProcedureBiConsumer {

        CreateTableProcedureBiConsumer(TableName tableName) {
            super(tableName);
        }

        @Override
        String getOperationType() {
            return "CREATE";
        }
    }

    private static class ModifyTableProcedureBiConsumer extends TableProcedureBiConsumer {

        ModifyTableProcedureBiConsumer(AsyncAdmin admin, TableName tableName) {
            super(tableName);
        }

        @Override
        String getOperationType() {
            return "ENABLE";
        }
    }

    private class DeleteTableProcedureBiConsumer extends TableProcedureBiConsumer {

        DeleteTableProcedureBiConsumer(TableName tableName) {
            super(tableName);
        }

        @Override
        String getOperationType() {
            return "DELETE";
        }

        @Override
        void onFinished() {
            connection.getLocator().clearCache(this.tableName);
            super.onFinished();
        }
    }

    private static class TruncateTableProcedureBiConsumer extends TableProcedureBiConsumer {

        TruncateTableProcedureBiConsumer(TableName tableName) {
            super(tableName);
        }

        @Override
        String getOperationType() {
            return "TRUNCATE";
        }
    }

    private static class EnableTableProcedureBiConsumer extends TableProcedureBiConsumer {

        EnableTableProcedureBiConsumer(TableName tableName) {
            super(tableName);
        }

        @Override
        String getOperationType() {
            return "ENABLE";
        }
    }

    private static class DisableTableProcedureBiConsumer extends TableProcedureBiConsumer {

        DisableTableProcedureBiConsumer(TableName tableName) {
            super(tableName);
        }

        @Override
        String getOperationType() {
            return "DISABLE";
        }
    }

    private static class AddColumnFamilyProcedureBiConsumer extends TableProcedureBiConsumer {

        AddColumnFamilyProcedureBiConsumer(TableName tableName) {
            super(tableName);
        }

        @Override
        String getOperationType() {
            return "ADD_COLUMN_FAMILY";
        }
    }

    private static class DeleteColumnFamilyProcedureBiConsumer extends TableProcedureBiConsumer {

        DeleteColumnFamilyProcedureBiConsumer(TableName tableName) {
            super(tableName);
        }

        @Override
        String getOperationType() {
            return "DELETE_COLUMN_FAMILY";
        }
    }

    private static class ModifyColumnFamilyProcedureBiConsumer extends TableProcedureBiConsumer {

        ModifyColumnFamilyProcedureBiConsumer(TableName tableName) {
            super(tableName);
        }

        @Override
        String getOperationType() {
            return "MODIFY_COLUMN_FAMILY";
        }
    }

    private static class CreateNamespaceProcedureBiConsumer extends NamespaceProcedureBiConsumer {

        CreateNamespaceProcedureBiConsumer(String namespaceName) {
            super(namespaceName);
        }

        @Override
        String getOperationType() {
            return "CREATE_NAMESPACE";
        }
    }

    private static class DeleteNamespaceProcedureBiConsumer extends NamespaceProcedureBiConsumer {

        DeleteNamespaceProcedureBiConsumer(String namespaceName) {
            super(namespaceName);
        }

        @Override
        String getOperationType() {
            return "DELETE_NAMESPACE";
        }
    }

    private static class ModifyNamespaceProcedureBiConsumer extends NamespaceProcedureBiConsumer {

        ModifyNamespaceProcedureBiConsumer(String namespaceName) {
            super(namespaceName);
        }

        @Override
        String getOperationType() {
            return "MODIFY_NAMESPACE";
        }
    }

    private static class MergeTableRegionProcedureBiConsumer extends TableProcedureBiConsumer {

        MergeTableRegionProcedureBiConsumer(TableName tableName) {
            super(tableName);
        }

        @Override
        String getOperationType() {
            return "MERGE_REGIONS";
        }
    }

    private static class SplitTableRegionProcedureBiConsumer extends TableProcedureBiConsumer {

        SplitTableRegionProcedureBiConsumer(TableName tableName) {
            super(tableName);
        }

        @Override
        String getOperationType() {
            return "SPLIT_REGION";
        }
    }

    private static class ReplicationProcedureBiConsumer extends ProcedureBiConsumer {
        private final String peerId;
        private final Supplier<String> getOperation;

        ReplicationProcedureBiConsumer(String peerId, Supplier<String> getOperation) {
            this.peerId = peerId;
            this.getOperation = getOperation;
        }

        String getDescription() {
            return "Operation: " + getOperation.get() + ", peerId: " + peerId;
        }

        @Override
        void onFinished() {
            LOG.info(getDescription() + " completed");
        }

        @Override
        void onError(Throwable error) {
            LOG.info(getDescription() + " failed with " + error.getMessage());
        }
    }

    private CompletableFuture<Void> waitProcedureResult(CompletableFuture<Long> procFuture) {
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(procFuture, (procId, error) -> {
            if (error != null) {
                future.completeExceptionally(error);
                return;
            }
            getProcedureResult(procId, future, 0);
        });
        return future;
    }

    private void getProcedureResult(long procId, CompletableFuture<Void> future, int retries) {
        addListener(
                this.<GetProcedureResultResponse>newMasterCaller().action((controller, stub) -> this
                        .<GetProcedureResultRequest, GetProcedureResultResponse, GetProcedureResultResponse>call(
                                controller, stub, GetProcedureResultRequest.newBuilder().setProcId(procId).build(),
                                (s, c, req, done) -> s.getProcedureResult(c, req, done), (resp) -> resp))
                        .call(),
                (response, error) -> {
                    if (error != null) {
                        LOG.warn("failed to get the procedure result procId={}", procId,
                                ConnectionUtils.translateException(error));
                        retryTimer.newTimeout(t -> getProcedureResult(procId, future, retries + 1),
                                ConnectionUtils.getPauseTime(pauseNs, retries), TimeUnit.NANOSECONDS);
                        return;
                    }
                    if (response.getState() == GetProcedureResultResponse.State.RUNNING) {
                        retryTimer.newTimeout(t -> getProcedureResult(procId, future, retries + 1),
                                ConnectionUtils.getPauseTime(pauseNs, retries), TimeUnit.NANOSECONDS);
                        return;
                    }
                    if (response.hasException()) {
                        IOException ioe = ForeignExceptionUtil.toIOException(response.getException());
                        future.completeExceptionally(ioe);
                    } else {
                        future.complete(null);
                    }
                });
    }

    private <T> CompletableFuture<T> failedFuture(Throwable error) {
        CompletableFuture<T> future = new CompletableFuture<>();
        future.completeExceptionally(error);
        return future;
    }

    private <T> boolean completeExceptionally(CompletableFuture<T> future, Throwable error) {
        if (error != null) {
            future.completeExceptionally(error);
            return true;
        }
        return false;
    }

    @Override
    public CompletableFuture<ClusterMetrics> getClusterMetrics() {
        return getClusterMetrics(EnumSet.allOf(Option.class));
    }

    @Override
    public CompletableFuture<ClusterMetrics> getClusterMetrics(EnumSet<Option> options) {
        return this.<ClusterMetrics>newMasterCaller()
                .action((controller, stub) -> this
                        .<GetClusterStatusRequest, GetClusterStatusResponse, ClusterMetrics>call(controller, stub,
                                RequestConverter.buildGetClusterStatusRequest(options),
                                (s, c, req, done) -> s.getClusterStatus(c, req, done),
                                resp -> ClusterMetricsBuilder.toClusterMetrics(resp.getClusterStatus())))
                .call();
    }

    @Override
    public CompletableFuture<Void> shutdown() {
        return this.<Void>newMasterCaller().priority(HIGH_QOS)
                .action((controller, stub) -> this.<ShutdownRequest, ShutdownResponse, Void>call(controller, stub,
                        ShutdownRequest.newBuilder().build(), (s, c, req, done) -> s.shutdown(c, req, done),
                        resp -> null))
                .call();
    }

    @Override
    public CompletableFuture<Void> stopMaster() {
        return this.<Void>newMasterCaller().priority(HIGH_QOS)
                .action((controller, stub) -> this.<StopMasterRequest, StopMasterResponse, Void>call(controller,
                        stub, StopMasterRequest.newBuilder().build(),
                        (s, c, req, done) -> s.stopMaster(c, req, done), resp -> null))
                .call();
    }

    @Override
    public CompletableFuture<Void> stopRegionServer(ServerName serverName) {
        StopServerRequest request = RequestConverter
                .buildStopServerRequest("Called by admin client " + this.connection.toString());
        return this.<Void>newAdminCaller().priority(HIGH_QOS)
                .action((controller, stub) -> this.<StopServerRequest, StopServerResponse, Void>adminCall(
                        controller, stub, request, (s, c, req, done) -> s.stopServer(controller, req, done),
                        resp -> null))
                .serverName(serverName).call();
    }

    @Override
    public CompletableFuture<Void> updateConfiguration(ServerName serverName) {
        return this.<Void>newAdminCaller()
                .action((controller, stub) -> this
                        .<UpdateConfigurationRequest, UpdateConfigurationResponse, Void>adminCall(controller, stub,
                                UpdateConfigurationRequest.getDefaultInstance(),
                                (s, c, req, done) -> s.updateConfiguration(controller, req, done), resp -> null))
                .serverName(serverName).call();
    }

    @Override
    public CompletableFuture<Void> updateConfiguration() {
        CompletableFuture<Void> future = new CompletableFuture<Void>();
        addListener(getClusterMetrics(EnumSet.of(Option.LIVE_SERVERS, Option.MASTER, Option.BACKUP_MASTERS)),
                (status, err) -> {
                    if (err != null) {
                        future.completeExceptionally(err);
                    } else {
                        List<CompletableFuture<Void>> futures = new ArrayList<>();
                        status.getLiveServerMetrics().keySet()
                                .forEach(server -> futures.add(updateConfiguration(server)));
                        futures.add(updateConfiguration(status.getMasterName()));
                        status.getBackupMasterNames().forEach(master -> futures.add(updateConfiguration(master)));
                        addListener(
                                CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[futures.size()])),
                                (result, err2) -> {
                                    if (err2 != null) {
                                        future.completeExceptionally(err2);
                                    } else {
                                        future.complete(result);
                                    }
                                });
                    }
                });
        return future;
    }

    @Override
    public CompletableFuture<Void> rollWALWriter(ServerName serverName) {
        return this.<Void>newAdminCaller()
                .action((controller, stub) -> this.<RollWALWriterRequest, RollWALWriterResponse, Void>adminCall(
                        controller, stub, RequestConverter.buildRollWALWriterRequest(),
                        (s, c, req, done) -> s.rollWALWriter(controller, req, done), resp -> null))
                .serverName(serverName).call();
    }

    @Override
    public CompletableFuture<Void> clearCompactionQueues(ServerName serverName, Set<String> queues) {
        return this.<Void>newAdminCaller()
                .action((controller, stub) -> this
                        .<ClearCompactionQueuesRequest, ClearCompactionQueuesResponse, Void>adminCall(controller,
                                stub, RequestConverter.buildClearCompactionQueuesRequest(queues),
                                (s, c, req, done) -> s.clearCompactionQueues(controller, req, done), resp -> null))
                .serverName(serverName).call();
    }

    @Override
    public CompletableFuture<List<SecurityCapability>> getSecurityCapabilities() {
        return this.<List<SecurityCapability>>newMasterCaller()
                .action((controller, stub) -> this
                        .<SecurityCapabilitiesRequest, SecurityCapabilitiesResponse, List<SecurityCapability>>call(
                                controller, stub, SecurityCapabilitiesRequest.newBuilder().build(),
                                (s, c, req, done) -> s.getSecurityCapabilities(c, req, done),
                                (resp) -> ProtobufUtil.toSecurityCapabilityList(resp.getCapabilitiesList())))
                .call();
    }

    @Override
    public CompletableFuture<List<RegionMetrics>> getRegionMetrics(ServerName serverName) {
        return getRegionMetrics(GetRegionLoadRequest.newBuilder().build(), serverName);
    }

    @Override
    public CompletableFuture<List<RegionMetrics>> getRegionMetrics(ServerName serverName, TableName tableName) {
        Preconditions.checkNotNull(tableName,
                "tableName is null. If you don't specify a tableName, use getRegionLoads() instead");
        return getRegionMetrics(RequestConverter.buildGetRegionLoadRequest(tableName), serverName);
    }

    private CompletableFuture<List<RegionMetrics>> getRegionMetrics(GetRegionLoadRequest request,
            ServerName serverName) {
        return this.<List<RegionMetrics>>newAdminCaller()
                .action((controller, stub) -> this
                        .<GetRegionLoadRequest, GetRegionLoadResponse, List<RegionMetrics>>adminCall(controller,
                                stub, request, (s, c, req, done) -> s.getRegionLoad(controller, req, done),
                                RegionMetricsBuilder::toRegionMetrics))
                .serverName(serverName).call();
    }

    @Override
    public CompletableFuture<Boolean> isMasterInMaintenanceMode() {
        return this.<Boolean>newMasterCaller()
                .action((controller, stub) -> this
                        .<IsInMaintenanceModeRequest, IsInMaintenanceModeResponse, Boolean>call(controller, stub,
                                IsInMaintenanceModeRequest.newBuilder().build(),
                                (s, c, req, done) -> s.isMasterInMaintenanceMode(c, req, done),
                                resp -> resp.getInMaintenanceMode()))
                .call();
    }

    @Override
    public CompletableFuture<CompactionState> getCompactionState(TableName tableName, CompactType compactType) {
        CompletableFuture<CompactionState> future = new CompletableFuture<>();

        switch (compactType) {
        case MOB:
            addListener(connection.registry.getMasterAddress(), (serverName, err) -> {
                if (err != null) {
                    future.completeExceptionally(err);
                    return;
                }
                RegionInfo regionInfo = RegionInfo.createMobRegionInfo(tableName);

                addListener(this.<GetRegionInfoResponse>newAdminCaller().serverName(serverName)
                        .action((controller, stub) -> this
                                .<GetRegionInfoRequest, GetRegionInfoResponse, GetRegionInfoResponse>adminCall(
                                        controller, stub,
                                        RequestConverter.buildGetRegionInfoRequest(regionInfo.getRegionName(),
                                                true),
                                        (s, c, req, done) -> s.getRegionInfo(controller, req, done), resp -> resp))
                        .call(), (resp2, err2) -> {
                            if (err2 != null) {
                                future.completeExceptionally(err2);
                            } else {
                                if (resp2.hasCompactionState()) {
                                    future.complete(ProtobufUtil.createCompactionState(resp2.getCompactionState()));
                                } else {
                                    future.complete(CompactionState.NONE);
                                }
                            }
                        });
            });
            break;
        case NORMAL:
            addListener(getTableHRegionLocations(tableName), (locations, err) -> {
                if (err != null) {
                    future.completeExceptionally(err);
                    return;
                }
                ConcurrentLinkedQueue<CompactionState> regionStates = new ConcurrentLinkedQueue<>();
                List<CompletableFuture<CompactionState>> futures = new ArrayList<>();
                locations.stream().filter(loc -> loc.getServerName() != null).filter(loc -> loc.getRegion() != null)
                        .filter(loc -> !loc.getRegion().isOffline()).map(loc -> loc.getRegion().getRegionName())
                        .forEach(region -> {
                            futures.add(getCompactionStateForRegion(region).whenComplete((regionState, err2) -> {
                                // If any region compaction state is MAJOR_AND_MINOR
                                // the table compaction state is MAJOR_AND_MINOR, too.
                                if (err2 != null) {
                                    future.completeExceptionally(unwrapCompletionException(err2));
                                } else if (regionState == CompactionState.MAJOR_AND_MINOR) {
                                    future.complete(regionState);
                                } else {
                                    regionStates.add(regionState);
                                }
                            }));
                        });
                addListener(CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[futures.size()])),
                        (ret, err3) -> {
                            // If future not completed, check all regions's compaction state
                            if (!future.isCompletedExceptionally() && !future.isDone()) {
                                CompactionState state = CompactionState.NONE;
                                for (CompactionState regionState : regionStates) {
                                    switch (regionState) {
                                    case MAJOR:
                                        if (state == CompactionState.MINOR) {
                                            future.complete(CompactionState.MAJOR_AND_MINOR);
                                        } else {
                                            state = CompactionState.MAJOR;
                                        }
                                        break;
                                    case MINOR:
                                        if (state == CompactionState.MAJOR) {
                                            future.complete(CompactionState.MAJOR_AND_MINOR);
                                        } else {
                                            state = CompactionState.MINOR;
                                        }
                                        break;
                                    case NONE:
                                    default:
                                    }
                                }
                                if (!future.isDone()) {
                                    future.complete(state);
                                }
                            }
                        });
            });
            break;
        default:
            throw new IllegalArgumentException("Unknown compactType: " + compactType);
        }

        return future;
    }

    @Override
    public CompletableFuture<CompactionState> getCompactionStateForRegion(byte[] regionName) {
        CompletableFuture<CompactionState> future = new CompletableFuture<>();
        addListener(getRegionLocation(regionName), (location, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            ServerName serverName = location.getServerName();
            if (serverName == null) {
                future.completeExceptionally(new NoServerForRegionException(Bytes.toStringBinary(regionName)));
                return;
            }
            addListener(this.<GetRegionInfoResponse>newAdminCaller().action((controller, stub) -> this
                    .<GetRegionInfoRequest, GetRegionInfoResponse, GetRegionInfoResponse>adminCall(controller, stub,
                            RequestConverter.buildGetRegionInfoRequest(location.getRegion().getRegionName(), true),
                            (s, c, req, done) -> s.getRegionInfo(controller, req, done), resp -> resp))
                    .serverName(serverName).call(), (resp2, err2) -> {
                        if (err2 != null) {
                            future.completeExceptionally(err2);
                        } else {
                            if (resp2.hasCompactionState()) {
                                future.complete(ProtobufUtil.createCompactionState(resp2.getCompactionState()));
                            } else {
                                future.complete(CompactionState.NONE);
                            }
                        }
                    });
        });
        return future;
    }

    @Override
    public CompletableFuture<Optional<Long>> getLastMajorCompactionTimestamp(TableName tableName) {
        MajorCompactionTimestampRequest request = MajorCompactionTimestampRequest.newBuilder()
                .setTableName(ProtobufUtil.toProtoTableName(tableName)).build();
        return this.<Optional<Long>>newMasterCaller().action((controller, stub) -> this
                .<MajorCompactionTimestampRequest, MajorCompactionTimestampResponse, Optional<Long>>call(controller,
                        stub, request, (s, c, req, done) -> s.getLastMajorCompactionTimestamp(c, req, done),
                        ProtobufUtil::toOptionalTimestamp))
                .call();
    }

    @Override
    public CompletableFuture<Optional<Long>> getLastMajorCompactionTimestampForRegion(byte[] regionName) {
        CompletableFuture<Optional<Long>> future = new CompletableFuture<>();
        // regionName may be a full region name or encoded region name, so getRegionInfo(byte[]) first
        addListener(getRegionInfo(regionName), (region, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            MajorCompactionTimestampForRegionRequest.Builder builder = MajorCompactionTimestampForRegionRequest
                    .newBuilder();
            builder.setRegion(RequestConverter.buildRegionSpecifier(RegionSpecifierType.REGION_NAME, regionName));
            addListener(this.<Optional<Long>>newMasterCaller().action((controller, stub) -> this
                    .<MajorCompactionTimestampForRegionRequest, MajorCompactionTimestampResponse, Optional<Long>>call(
                            controller, stub, builder.build(),
                            (s, c, req, done) -> s.getLastMajorCompactionTimestampForRegion(c, req, done),
                            ProtobufUtil::toOptionalTimestamp))
                    .call(), (timestamp, err2) -> {
                        if (err2 != null) {
                            future.completeExceptionally(err2);
                        } else {
                            future.complete(timestamp);
                        }
                    });
        });
        return future;
    }

    @Override
    public CompletableFuture<Map<ServerName, Boolean>> compactionSwitch(boolean switchState,
            List<String> serverNamesList) {
        CompletableFuture<Map<ServerName, Boolean>> future = new CompletableFuture<>();
        addListener(getRegionServerList(serverNamesList), (serverNames, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            // Accessed by multiple threads.
            Map<ServerName, Boolean> serverStates = new ConcurrentHashMap<>(serverNames.size());
            List<CompletableFuture<Boolean>> futures = new ArrayList<>(serverNames.size());
            serverNames.stream().forEach(serverName -> {
                futures.add(switchCompact(serverName, switchState).whenComplete((serverState, err2) -> {
                    if (err2 != null) {
                        future.completeExceptionally(unwrapCompletionException(err2));
                    } else {
                        serverStates.put(serverName, serverState);
                    }
                }));
            });
            addListener(CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[futures.size()])),
                    (ret, err3) -> {
                        if (!future.isCompletedExceptionally()) {
                            if (err3 != null) {
                                future.completeExceptionally(err3);
                            } else {
                                future.complete(serverStates);
                            }
                        }
                    });
        });
        return future;
    }

    private CompletableFuture<List<ServerName>> getRegionServerList(List<String> serverNamesList) {
        CompletableFuture<List<ServerName>> future = new CompletableFuture<>();
        if (serverNamesList.isEmpty()) {
            CompletableFuture<ClusterMetrics> clusterMetricsCompletableFuture = getClusterMetrics(
                    EnumSet.of(Option.LIVE_SERVERS));
            addListener(clusterMetricsCompletableFuture, (clusterMetrics, err) -> {
                if (err != null) {
                    future.completeExceptionally(err);
                } else {
                    future.complete(new ArrayList<>(clusterMetrics.getLiveServerMetrics().keySet()));
                }
            });
            return future;
        } else {
            List<ServerName> serverList = new ArrayList<>();
            for (String regionServerName : serverNamesList) {
                ServerName serverName = null;
                try {
                    serverName = ServerName.valueOf(regionServerName);
                } catch (Exception e) {
                    future.completeExceptionally(
                            new IllegalArgumentException(String.format("ServerName format: %s", regionServerName)));
                }
                if (serverName == null) {
                    future.completeExceptionally(
                            new IllegalArgumentException(String.format("Null ServerName: %s", regionServerName)));
                }
            }
            future.complete(serverList);
        }
        return future;
    }

    private CompletableFuture<Boolean> switchCompact(ServerName serverName, boolean onOrOff) {
        return this.<Boolean>newAdminCaller().serverName(serverName)
                .action((controller, stub) -> this
                        .<CompactionSwitchRequest, CompactionSwitchResponse, Boolean>adminCall(controller, stub,
                                CompactionSwitchRequest.newBuilder().setEnabled(onOrOff).build(),
                                (s, c, req, done) -> s.compactionSwitch(c, req, done), resp -> resp.getPrevState()))
                .call();
    }

    @Override
    public CompletableFuture<Boolean> balancerSwitch(boolean on, boolean drainRITs) {
        return this.<Boolean>newMasterCaller()
                .action((controller, stub) -> this
                        .<SetBalancerRunningRequest, SetBalancerRunningResponse, Boolean>call(controller, stub,
                                RequestConverter.buildSetBalancerRunningRequest(on, drainRITs),
                                (s, c, req, done) -> s.setBalancerRunning(c, req, done),
                                (resp) -> resp.getPrevBalanceValue()))
                .call();
    }

    @Override
    public CompletableFuture<Boolean> balance(boolean forcible) {
        return this.<Boolean>newMasterCaller()
                .action((controller, stub) -> this.<BalanceRequest, BalanceResponse, Boolean>call(controller, stub,
                        RequestConverter.buildBalanceRequest(forcible),
                        (s, c, req, done) -> s.balance(c, req, done), (resp) -> resp.getBalancerRan()))
                .call();
    }

    @Override
    public CompletableFuture<Boolean> isBalancerEnabled() {
        return this.<Boolean>newMasterCaller().action(
                (controller, stub) -> this.<IsBalancerEnabledRequest, IsBalancerEnabledResponse, Boolean>call(
                        controller, stub, RequestConverter.buildIsBalancerEnabledRequest(),
                        (s, c, req, done) -> s.isBalancerEnabled(c, req, done), (resp) -> resp.getEnabled()))
                .call();
    }

    @Override
    public CompletableFuture<Boolean> normalizerSwitch(boolean on) {
        return this.<Boolean>newMasterCaller()
                .action((controller, stub) -> this
                        .<SetNormalizerRunningRequest, SetNormalizerRunningResponse, Boolean>call(controller, stub,
                                RequestConverter.buildSetNormalizerRunningRequest(on),
                                (s, c, req, done) -> s.setNormalizerRunning(c, req, done),
                                (resp) -> resp.getPrevNormalizerValue()))
                .call();
    }

    @Override
    public CompletableFuture<Boolean> isNormalizerEnabled() {
        return this.<Boolean>newMasterCaller().action(
                (controller, stub) -> this.<IsNormalizerEnabledRequest, IsNormalizerEnabledResponse, Boolean>call(
                        controller, stub, RequestConverter.buildIsNormalizerEnabledRequest(),
                        (s, c, req, done) -> s.isNormalizerEnabled(c, req, done), (resp) -> resp.getEnabled()))
                .call();
    }

    @Override
    public CompletableFuture<Boolean> normalize() {
        return this.<Boolean>newMasterCaller()
                .action((controller, stub) -> this.<NormalizeRequest, NormalizeResponse, Boolean>call(controller,
                        stub, RequestConverter.buildNormalizeRequest(),
                        (s, c, req, done) -> s.normalize(c, req, done), (resp) -> resp.getNormalizerRan()))
                .call();
    }

    @Override
    public CompletableFuture<Boolean> cleanerChoreSwitch(boolean enabled) {
        return this.<Boolean>newMasterCaller()
                .action((controller, stub) -> this
                        .<SetCleanerChoreRunningRequest, SetCleanerChoreRunningResponse, Boolean>call(controller,
                                stub, RequestConverter.buildSetCleanerChoreRunningRequest(enabled),
                                (s, c, req, done) -> s.setCleanerChoreRunning(c, req, done),
                                (resp) -> resp.getPrevValue()))
                .call();
    }

    @Override
    public CompletableFuture<Boolean> isCleanerChoreEnabled() {
        return this.<Boolean>newMasterCaller()
                .action((controller, stub) -> this
                        .<IsCleanerChoreEnabledRequest, IsCleanerChoreEnabledResponse, Boolean>call(controller,
                                stub, RequestConverter.buildIsCleanerChoreEnabledRequest(),
                                (s, c, req, done) -> s.isCleanerChoreEnabled(c, req, done),
                                (resp) -> resp.getValue()))
                .call();
    }

    @Override
    public CompletableFuture<Boolean> runCleanerChore() {
        return this.<Boolean>newMasterCaller()
                .action((controller, stub) -> this.<RunCleanerChoreRequest, RunCleanerChoreResponse, Boolean>call(
                        controller, stub, RequestConverter.buildRunCleanerChoreRequest(),
                        (s, c, req, done) -> s.runCleanerChore(c, req, done), (resp) -> resp.getCleanerChoreRan()))
                .call();
    }

    @Override
    public CompletableFuture<Boolean> catalogJanitorSwitch(boolean enabled) {
        return this.<Boolean>newMasterCaller().action(
                (controller, stub) -> this.<EnableCatalogJanitorRequest, EnableCatalogJanitorResponse, Boolean>call(
                        controller, stub, RequestConverter.buildEnableCatalogJanitorRequest(enabled),
                        (s, c, req, done) -> s.enableCatalogJanitor(c, req, done), (resp) -> resp.getPrevValue()))
                .call();
    }

    @Override
    public CompletableFuture<Boolean> isCatalogJanitorEnabled() {
        return this.<Boolean>newMasterCaller()
                .action((controller, stub) -> this
                        .<IsCatalogJanitorEnabledRequest, IsCatalogJanitorEnabledResponse, Boolean>call(controller,
                                stub, RequestConverter.buildIsCatalogJanitorEnabledRequest(),
                                (s, c, req, done) -> s.isCatalogJanitorEnabled(c, req, done),
                                (resp) -> resp.getValue()))
                .call();
    }

    @Override
    public CompletableFuture<Integer> runCatalogJanitor() {
        return this.<Integer>newMasterCaller()
                .action((controller, stub) -> this.<RunCatalogScanRequest, RunCatalogScanResponse, Integer>call(
                        controller, stub, RequestConverter.buildCatalogScanRequest(),
                        (s, c, req, done) -> s.runCatalogScan(c, req, done), (resp) -> resp.getScanResult()))
                .call();
    }

    @Override
    public <S, R> CompletableFuture<R> coprocessorService(Function<RpcChannel, S> stubMaker,
            ServiceCaller<S, R> callable) {
        MasterCoprocessorRpcChannelImpl channel = new MasterCoprocessorRpcChannelImpl(
                this.<Message>newMasterCaller());
        S stub = stubMaker.apply(channel);
        CompletableFuture<R> future = new CompletableFuture<>();
        ClientCoprocessorRpcController controller = new ClientCoprocessorRpcController();
        callable.call(stub, controller, resp -> {
            if (controller.failed()) {
                future.completeExceptionally(controller.getFailed());
            } else {
                future.complete(resp);
            }
        });
        return future;
    }

    @Override
    public <S, R> CompletableFuture<R> coprocessorService(Function<RpcChannel, S> stubMaker,
            ServiceCaller<S, R> callable, ServerName serverName) {
        RegionServerCoprocessorRpcChannelImpl channel = new RegionServerCoprocessorRpcChannelImpl(
                this.<Message>newServerCaller().serverName(serverName));
        S stub = stubMaker.apply(channel);
        CompletableFuture<R> future = new CompletableFuture<>();
        ClientCoprocessorRpcController controller = new ClientCoprocessorRpcController();
        callable.call(stub, controller, resp -> {
            if (controller.failed()) {
                future.completeExceptionally(controller.getFailed());
            } else {
                future.complete(resp);
            }
        });
        return future;
    }

    @Override
    public CompletableFuture<List<ServerName>> clearDeadServers(List<ServerName> servers) {
        return this.<List<ServerName>>newMasterCaller()
                .action((controller, stub) -> this
                        .<ClearDeadServersRequest, ClearDeadServersResponse, List<ServerName>>call(controller, stub,
                                RequestConverter.buildClearDeadServersRequest(servers),
                                (s, c, req, done) -> s.clearDeadServers(c, req, done),
                                (resp) -> ProtobufUtil.toServerNameList(resp.getServerNameList())))
                .call();
    }

    private <T> ServerRequestCallerBuilder<T> newServerCaller() {
        return this.connection.callerFactory.<T>serverRequest().rpcTimeout(rpcTimeoutNs, TimeUnit.NANOSECONDS)
                .operationTimeout(operationTimeoutNs, TimeUnit.NANOSECONDS).pause(pauseNs, TimeUnit.NANOSECONDS)
                .pauseForCQTBE(pauseForCQTBENs, TimeUnit.NANOSECONDS).maxAttempts(maxAttempts)
                .startLogErrorsCnt(startLogErrorsCnt);
    }

    @Override
    public CompletableFuture<Void> enableTableReplication(TableName tableName) {
        if (tableName == null) {
            return failedFuture(new IllegalArgumentException("Table name is null"));
        }
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(tableExists(tableName), (exist, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            if (!exist) {
                future.completeExceptionally(
                        new TableNotFoundException("Table '" + tableName.getNameAsString() + "' does not exists."));
                return;
            }
            addListener(getTableSplits(tableName), (splits, err1) -> {
                if (err1 != null) {
                    future.completeExceptionally(err1);
                } else {
                    addListener(checkAndSyncTableToPeerClusters(tableName, splits), (result, err2) -> {
                        if (err2 != null) {
                            future.completeExceptionally(err2);
                        } else {
                            addListener(setTableReplication(tableName, true), (result3, err3) -> {
                                if (err3 != null) {
                                    future.completeExceptionally(err3);
                                } else {
                                    future.complete(result3);
                                }
                            });
                        }
                    });
                }
            });
        });
        return future;
    }

    @Override
    public CompletableFuture<Void> disableTableReplication(TableName tableName) {
        if (tableName == null) {
            return failedFuture(new IllegalArgumentException("Table name is null"));
        }
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(tableExists(tableName), (exist, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            if (!exist) {
                future.completeExceptionally(
                        new TableNotFoundException("Table '" + tableName.getNameAsString() + "' does not exists."));
                return;
            }
            addListener(setTableReplication(tableName, false), (result, err2) -> {
                if (err2 != null) {
                    future.completeExceptionally(err2);
                } else {
                    future.complete(result);
                }
            });
        });
        return future;
    }

    private CompletableFuture<byte[][]> getTableSplits(TableName tableName) {
        CompletableFuture<byte[][]> future = new CompletableFuture<>();
        addListener(
                getRegions(tableName).thenApply(regions -> regions.stream()
                        .filter(RegionReplicaUtil::isDefaultReplica).collect(Collectors.toList())),
                (regions, err2) -> {
                    if (err2 != null) {
                        future.completeExceptionally(err2);
                        return;
                    }
                    if (regions.size() == 1) {
                        future.complete(null);
                    } else {
                        byte[][] splits = new byte[regions.size() - 1][];
                        for (int i = 1; i < regions.size(); i++) {
                            splits[i - 1] = regions.get(i).getStartKey();
                        }
                        future.complete(splits);
                    }
                });
        return future;
    }

    /**
     * Connect to peer and check the table descriptor on peer:
     * <ol>
     * <li>Create the same table on peer when not exist.</li>
     * <li>Throw an exception if the table already has replication enabled on any of the column
     * families.</li>
     * <li>Throw an exception if the table exists on peer cluster but descriptors are not same.</li>
     * </ol>
     * @param tableName name of the table to sync to the peer
     * @param splits table split keys
     */
    private CompletableFuture<Void> checkAndSyncTableToPeerClusters(TableName tableName, byte[][] splits) {
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(listReplicationPeers(), (peers, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            if (peers == null || peers.size() <= 0) {
                future.completeExceptionally(
                        new IllegalArgumentException("Found no peer cluster for replication."));
                return;
            }
            List<CompletableFuture<Void>> futures = new ArrayList<>();
            peers.stream().filter(peer -> peer.getPeerConfig().needToReplicate(tableName)).forEach(peer -> {
                futures.add(trySyncTableToPeerCluster(tableName, splits, peer));
            });
            addListener(CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[futures.size()])),
                    (result, err2) -> {
                        if (err2 != null) {
                            future.completeExceptionally(err2);
                        } else {
                            future.complete(result);
                        }
                    });
        });
        return future;
    }

    private CompletableFuture<Void> trySyncTableToPeerCluster(TableName tableName, byte[][] splits,
            ReplicationPeerDescription peer) {
        Configuration peerConf = null;
        try {
            peerConf = ReplicationPeerConfigUtil.getPeerClusterConfiguration(connection.getConfiguration(), peer);
        } catch (IOException e) {
            return failedFuture(e);
        }
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(ConnectionFactory.createAsyncConnection(peerConf), (conn, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            addListener(getDescriptor(tableName), (tableDesc, err1) -> {
                if (err1 != null) {
                    future.completeExceptionally(err1);
                    return;
                }
                AsyncAdmin peerAdmin = conn.getAdmin();
                addListener(peerAdmin.tableExists(tableName), (exist, err2) -> {
                    if (err2 != null) {
                        future.completeExceptionally(err2);
                        return;
                    }
                    if (!exist) {
                        CompletableFuture<Void> createTableFuture = null;
                        if (splits == null) {
                            createTableFuture = peerAdmin.createTable(tableDesc);
                        } else {
                            createTableFuture = peerAdmin.createTable(tableDesc, splits);
                        }
                        addListener(createTableFuture, (result, err3) -> {
                            if (err3 != null) {
                                future.completeExceptionally(err3);
                            } else {
                                future.complete(result);
                            }
                        });
                    } else {
                        addListener(compareTableWithPeerCluster(tableName, tableDesc, peer, peerAdmin),
                                (result, err4) -> {
                                    if (err4 != null) {
                                        future.completeExceptionally(err4);
                                    } else {
                                        future.complete(result);
                                    }
                                });
                    }
                });
            });
        });
        return future;
    }

    private CompletableFuture<Void> compareTableWithPeerCluster(TableName tableName, TableDescriptor tableDesc,
            ReplicationPeerDescription peer, AsyncAdmin peerAdmin) {
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(peerAdmin.getDescriptor(tableName), (peerTableDesc, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            if (peerTableDesc == null) {
                future.completeExceptionally(
                        new IllegalArgumentException("Failed to get table descriptor for table "
                                + tableName.getNameAsString() + " from peer cluster " + peer.getPeerId()));
                return;
            }
            if (TableDescriptor.COMPARATOR_IGNORE_REPLICATION.compare(peerTableDesc, tableDesc) != 0) {
                future.completeExceptionally(new IllegalArgumentException(
                        "Table " + tableName.getNameAsString() + " exists in peer cluster " + peer.getPeerId()
                                + ", but the table descriptors are not same when compared with source cluster."
                                + " Thus can not enable the table's replication switch."));
                return;
            }
            future.complete(null);
        });
        return future;
    }

    /**
     * Set the table's replication switch if the table's replication switch is already not set.
     * @param tableName name of the table
     * @param enableRep is replication switch enable or disable
     */
    private CompletableFuture<Void> setTableReplication(TableName tableName, boolean enableRep) {
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(getDescriptor(tableName), (tableDesc, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            if (!tableDesc.matchReplicationScope(enableRep)) {
                int scope = enableRep ? HConstants.REPLICATION_SCOPE_GLOBAL : HConstants.REPLICATION_SCOPE_LOCAL;
                TableDescriptor newTableDesc = TableDescriptorBuilder.newBuilder(tableDesc)
                        .setReplicationScope(scope).build();
                addListener(modifyTable(newTableDesc), (result, err2) -> {
                    if (err2 != null) {
                        future.completeExceptionally(err2);
                    } else {
                        future.complete(result);
                    }
                });
            } else {
                future.complete(null);
            }
        });
        return future;
    }

    @Override
    public CompletableFuture<CacheEvictionStats> clearBlockCache(TableName tableName) {
        CompletableFuture<CacheEvictionStats> future = new CompletableFuture<>();
        addListener(getTableHRegionLocations(tableName), (locations, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            Map<ServerName, List<RegionInfo>> regionInfoByServerName = locations.stream()
                    .filter(l -> l.getRegion() != null).filter(l -> !l.getRegion().isOffline())
                    .filter(l -> l.getServerName() != null).collect(Collectors.groupingBy(l -> l.getServerName(),
                            Collectors.mapping(l -> l.getRegion(), Collectors.toList())));
            List<CompletableFuture<CacheEvictionStats>> futures = new ArrayList<>();
            CacheEvictionStatsAggregator aggregator = new CacheEvictionStatsAggregator();
            for (Map.Entry<ServerName, List<RegionInfo>> entry : regionInfoByServerName.entrySet()) {
                futures.add(clearBlockCache(entry.getKey(), entry.getValue()).whenComplete((stats, err2) -> {
                    if (err2 != null) {
                        future.completeExceptionally(unwrapCompletionException(err2));
                    } else {
                        aggregator.append(stats);
                    }
                }));
            }
            addListener(CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()])),
                    (ret, err3) -> {
                        if (err3 != null) {
                            future.completeExceptionally(unwrapCompletionException(err3));
                        } else {
                            future.complete(aggregator.sum());
                        }
                    });
        });
        return future;
    }

    @Override
    public CompletableFuture<Void> cloneTableSchema(TableName tableName, TableName newTableName,
            boolean preserveSplits) {
        CompletableFuture<Void> future = new CompletableFuture<>();
        addListener(tableExists(tableName), (exist, err) -> {
            if (err != null) {
                future.completeExceptionally(err);
                return;
            }
            if (!exist) {
                future.completeExceptionally(new TableNotFoundException(tableName));
                return;
            }
            addListener(tableExists(newTableName), (exist1, err1) -> {
                if (err1 != null) {
                    future.completeExceptionally(err1);
                    return;
                }
                if (exist1) {
                    future.completeExceptionally(new TableExistsException(newTableName));
                    return;
                }
                addListener(getDescriptor(tableName), (tableDesc, err2) -> {
                    if (err2 != null) {
                        future.completeExceptionally(err2);
                        return;
                    }
                    TableDescriptor newTableDesc = TableDescriptorBuilder.copy(newTableName, tableDesc);
                    if (preserveSplits) {
                        addListener(getTableSplits(tableName), (splits, err3) -> {
                            if (err3 != null) {
                                future.completeExceptionally(err3);
                            } else {
                                addListener(splits != null ? createTable(newTableDesc, splits)
                                        : createTable(newTableDesc), (result, err4) -> {
                                            if (err4 != null) {
                                                future.completeExceptionally(err4);
                                            } else {
                                                future.complete(result);
                                            }
                                        });
                            }
                        });
                    } else {
                        addListener(createTable(newTableDesc), (result, err5) -> {
                            if (err5 != null) {
                                future.completeExceptionally(err5);
                            } else {
                                future.complete(result);
                            }
                        });
                    }
                });
            });
        });
        return future;
    }

    private CompletableFuture<CacheEvictionStats> clearBlockCache(ServerName serverName, List<RegionInfo> hris) {
        return this.<CacheEvictionStats>newAdminCaller()
                .action((controller, stub) -> this
                        .<ClearRegionBlockCacheRequest, ClearRegionBlockCacheResponse, CacheEvictionStats>adminCall(
                                controller, stub, RequestConverter.buildClearRegionBlockCacheRequest(hris),
                                (s, c, req, done) -> s.clearRegionBlockCache(controller, req, done),
                                resp -> ProtobufUtil.toCacheEvictionStats(resp.getStats())))
                .serverName(serverName).call();
    }

    @Override
    public CompletableFuture<Boolean> switchRpcThrottle(boolean enable) {
        CompletableFuture<Boolean> future = this.<Boolean>newMasterCaller()
                .action((controller, stub) -> this
                        .<SwitchRpcThrottleRequest, SwitchRpcThrottleResponse, Boolean>call(controller, stub,
                                SwitchRpcThrottleRequest.newBuilder().setRpcThrottleEnabled(enable).build(),
                                (s, c, req, done) -> s.switchRpcThrottle(c, req, done),
                                resp -> resp.getPreviousRpcThrottleEnabled()))
                .call();
        return future;
    }

    @Override
    public CompletableFuture<Boolean> isRpcThrottleEnabled() {
        CompletableFuture<Boolean> future = this.<Boolean>newMasterCaller()
                .action((controller, stub) -> this
                        .<IsRpcThrottleEnabledRequest, IsRpcThrottleEnabledResponse, Boolean>call(controller, stub,
                                IsRpcThrottleEnabledRequest.newBuilder().build(),
                                (s, c, req, done) -> s.isRpcThrottleEnabled(c, req, done),
                                resp -> resp.getRpcThrottleEnabled()))
                .call();
        return future;
    }

    @Override
    public CompletableFuture<Boolean> exceedThrottleQuotaSwitch(boolean enable) {
        CompletableFuture<Boolean> future = this.<Boolean>newMasterCaller()
                .action((controller, stub) -> this
                        .<SwitchExceedThrottleQuotaRequest, SwitchExceedThrottleQuotaResponse, Boolean>call(
                                controller, stub,
                                SwitchExceedThrottleQuotaRequest.newBuilder().setExceedThrottleQuotaEnabled(enable)
                                        .build(),
                                (s, c, req, done) -> s.switchExceedThrottleQuota(c, req, done),
                                resp -> resp.getPreviousExceedThrottleQuotaEnabled()))
                .call();
        return future;
    }

    @Override
    public CompletableFuture<Map<TableName, Long>> getSpaceQuotaTableSizes() {
        return this.<Map<TableName, Long>>newMasterCaller().action((controller, stub) -> this
                .<GetSpaceQuotaRegionSizesRequest, GetSpaceQuotaRegionSizesResponse, Map<TableName, Long>>call(
                        controller, stub, RequestConverter.buildGetSpaceQuotaRegionSizesRequest(),
                        (s, c, req, done) -> s.getSpaceQuotaRegionSizes(c, req, done),
                        resp -> resp.getSizesList().stream().collect(Collectors.toMap(
                                sizes -> ProtobufUtil.toTableName(sizes.getTableName()), RegionSizes::getSize))))
                .call();
    }

    @Override
    public CompletableFuture<Map<TableName, SpaceQuotaSnapshot>> getRegionServerSpaceQuotaSnapshots(
            ServerName serverName) {
        return this.<Map<TableName, SpaceQuotaSnapshot>>newAdminCaller().action((controller, stub) -> this
                .<GetSpaceQuotaSnapshotsRequest, GetSpaceQuotaSnapshotsResponse, Map<TableName, SpaceQuotaSnapshot>>adminCall(
                        controller, stub, RequestConverter.buildGetSpaceQuotaSnapshotsRequest(),
                        (s, c, req, done) -> s.getSpaceQuotaSnapshots(controller, req, done),
                        resp -> resp.getSnapshotsList().stream().collect(Collectors.toMap(
                                snapshot -> ProtobufUtil.toTableName(snapshot.getTableName()),
                                snapshot -> SpaceQuotaSnapshot.toSpaceQuotaSnapshot(snapshot.getSnapshot())))))
                .serverName(serverName).call();
    }

    private CompletableFuture<SpaceQuotaSnapshot> getCurrentSpaceQuotaSnapshot(
            Converter<SpaceQuotaSnapshot, GetQuotaStatesResponse> converter) {
        return this.<SpaceQuotaSnapshot>newMasterCaller()
                .action((controller, stub) -> this
                        .<GetQuotaStatesRequest, GetQuotaStatesResponse, SpaceQuotaSnapshot>call(controller, stub,
                                RequestConverter.buildGetQuotaStatesRequest(),
                                (s, c, req, done) -> s.getQuotaStates(c, req, done), converter))
                .call();
    }

    @Override
    public CompletableFuture<SpaceQuotaSnapshot> getCurrentSpaceQuotaSnapshot(String namespace) {
        return getCurrentSpaceQuotaSnapshot(resp -> resp.getNsSnapshotsList().stream()
                .filter(s -> s.getNamespace().equals(namespace)).findFirst()
                .map(s -> SpaceQuotaSnapshot.toSpaceQuotaSnapshot(s.getSnapshot())).orElse(null));
    }

    @Override
    public CompletableFuture<SpaceQuotaSnapshot> getCurrentSpaceQuotaSnapshot(TableName tableName) {
        HBaseProtos.TableName protoTableName = ProtobufUtil.toProtoTableName(tableName);
        return getCurrentSpaceQuotaSnapshot(resp -> resp.getTableSnapshotsList().stream()
                .filter(s -> s.getTableName().equals(protoTableName)).findFirst()
                .map(s -> SpaceQuotaSnapshot.toSpaceQuotaSnapshot(s.getSnapshot())).orElse(null));
    }

    @Override
    public CompletableFuture<Void> grant(UserPermission userPermission, boolean mergeExistingPermissions) {
        return this.<Void>newMasterCaller()
                .action((controller, stub) -> this.<GrantRequest, GrantResponse, Void>call(controller, stub,
                        ShadedAccessControlUtil.buildGrantRequest(userPermission, mergeExistingPermissions),
                        (s, c, req, done) -> s.grant(c, req, done), resp -> null))
                .call();
    }

    @Override
    public CompletableFuture<Void> revoke(UserPermission userPermission) {
        return this.<Void>newMasterCaller()
                .action((controller, stub) -> this.<RevokeRequest, RevokeResponse, Void>call(controller, stub,
                        ShadedAccessControlUtil.buildRevokeRequest(userPermission),
                        (s, c, req, done) -> s.revoke(c, req, done), resp -> null))
                .call();
    }

    @Override
    public CompletableFuture<List<UserPermission>> getUserPermissions(
            GetUserPermissionsRequest getUserPermissionsRequest) {
        return this.<List<UserPermission>>newMasterCaller().action((controller,
                stub) -> this.<AccessControlProtos.GetUserPermissionsRequest, GetUserPermissionsResponse, List<UserPermission>>call(
                        controller, stub,
                        ShadedAccessControlUtil.buildGetUserPermissionsRequest(getUserPermissionsRequest),
                        (s, c, req, done) -> s.getUserPermissions(c, req, done),
                        resp -> resp.getUserPermissionList().stream()
                                .map(uPerm -> ShadedAccessControlUtil.toUserPermission(uPerm))
                                .collect(Collectors.toList())))
                .call();
    }

    @Override
    public CompletableFuture<List<Boolean>> hasUserPermissions(String userName, List<Permission> permissions) {
        return this.<List<Boolean>>newMasterCaller()
                .action((controller, stub) -> this
                        .<HasUserPermissionsRequest, HasUserPermissionsResponse, List<Boolean>>call(controller,
                                stub, ShadedAccessControlUtil.buildHasUserPermissionsRequest(userName, permissions),
                                (s, c, req, done) -> s.hasUserPermissions(c, req, done),
                                resp -> resp.getHasUserPermissionList()))
                .call();
    }
}